背景:
OC中有Class和Category。Category可以认为是一个类的扩展,可以给类新增方法。 runtime初始化的时候会把Class所有的Category合并到主类中。这个阶段会有一定的性能损耗。 所有的Class和Category都会在Binary的DATA Segment中对应的__objc_classlist Section和__objc_catlist Section中。Category变多会增加一定体积包大小。 load方法是OC中比较特殊的一个方法,他是由dyld在init阶段就会通过函数指针去调用。因此,如果一个Class或者Category如果有load方法,就会在另一个__objc_nlclasslist Section和__objc_nlcatlist Section中。
__objc_classlist:存放了所有的class。 __objc_catlist:存放了所有的category。 __objc_nlclasslist:即no lazy class list,存放了所有含load方法的class。 __objc_nlcatlist:即no lazy category list,存放了所有含load方法的category。
其中__objc_nlclasslist和__objc_nlcatlist是__objc_classlist和__objc_categorylist的子集。
起因:
笔者最近做了启动速度防劣化,在监控load的时候通过分析macho文件的符号表来统计应用中的load方法数量,可是由于部分宿主在蓝盾包中裁剪了符号,最后换成了统计macho文件的objc_nlcatlist和objc_nlclslist section来统计应用中的load方法数量。 最后发现部分category中的load方法出现在了class中,同时对应的objc_nlcatlist和objc_catlist都找不到这个category。 基于此我构建了一个Demo用于复现,现在让我们来到案发现场。 在分类中给HippyBaseListView添加了一个分类,并新增一个load方法。
通过MachoView去查看,发现section中没有nlcatlist,只有nlclslist。也就是说没有load方法的分类。
从红框内可以看到,nlclslist第一个就是hippyBaseListView,但是从源码可知,该类是没有load方法,Category中才有load方法。所以,应该nlcatlist中有hippyBaseListView,而不是nlclslist中有hippyBaseListView。
从二进制汇编来看,也是有load方法的。由此可知,是在编译的过程中,把category和class合并了,将category中的方法移到了class中。导致最后没有生成nlcatlist。
当我给HippyBaseListView的class中加一个load,这个现象就不会出现了,此时出现了nlcatlist。
category为什么会消失呢?
- 为什么有load方法的category会被合并到class呢?
- 为什么当class中有load方法的时候就不会被合并呢?
- category是在什么时候被合并的?
- category合并的原因是什么呢?
相信你们也会有这几个问题,那让我们继续深挖。 我们先来探究问题3:
category是在什么时候被合并的?
目前我们可知,源码是有load和category的。但是编译后的产物却没有category只有load。那么肯定是在编译的过程中被合并了。那么到底是编译阶段还是链接阶段呢?
我们先来看看编译的.o产物中是否含有load和category。通过编译日志找到编译产物目录。通过otool命令查看结果如下
可以很明显看到编译产物中是含有load和category信息的,那么就是linker阶段把category合并到了class中。
category合并的原因是什么呢?
找到了合并的时机,那么接下来我们就看为什么会被合并。所幸Xcode的linker是开源的,可以在ld中找到。 通过查阅源码,我发现个可疑的函数。
如果load方法的数量<2则跳过。否则,就把categories数组清空。
这里是进行merge操作,merge完方法之后如果categories为空,则跳过。
这两个函数中可以看到,如果categories为空,则不进行merge。
这几段代码的意思是,如果load的数量大于等于2,则不进行优化。
那么现在可以回答我们当初的问题:
-
为什么有load方法的category会被合并到class呢?
a. 因为此时class没有load方法,当load的数量小于2,会被合并。
-
为什么当class中有load方法的时候就不会被合并呢?
a. 因为此时category+class的load方法数量为2,不会被合并优化。
-
category是在什么时候被合并的?
a. 在linker的时候被合并。
-
category合并的原因是什么呢?
a. Apple在linker阶段做的优化,减少section,减少runtime的初始化,加快启动速度,减少包大小。