# category神秘消失?

89 阅读5分钟

背景:

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中。

Binary格式.png

__objc_classlist:存放了所有的class。 __objc_catlist:存放了所有的category。 __objc_nlclasslist:即no lazy class list,存放了所有含load方法的class。 __objc_nlcatlist:即no lazy category list,存放了所有含load方法的category。

image1).png 其中__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方法。

image2).png 通过MachoView去查看,发现section中没有nlcatlist,只有nlclslist。也就是说没有load方法的分类。

image3).png 从红框内可以看到,nlclslist第一个就是hippyBaseListView,但是从源码可知,该类是没有load方法,Category中才有load方法。所以,应该nlcatlist中有hippyBaseListView,而不是nlclslist中有hippyBaseListView。

image4).png

image5).png 从二进制汇编来看,也是有load方法的。由此可知,是在编译的过程中,把category和class合并了,将category中的方法移到了class中。导致最后没有生成nlcatlist。

image6).png 当我给HippyBaseListView的class中加一个load,这个现象就不会出现了,此时出现了nlcatlist。

image7).png

image8).png

category为什么会消失呢?

  1. 为什么有load方法的category会被合并到class呢?
  2. 为什么当class中有load方法的时候就不会被合并呢?
  3. category是在什么时候被合并的?
  4. category合并的原因是什么呢?

相信你们也会有这几个问题,那让我们继续深挖。 我们先来探究问题3:

category是在什么时候被合并的?

目前我们可知,源码是有load和category的。但是编译后的产物却没有category只有load。那么肯定是在编译的过程中被合并了。那么到底是编译阶段还是链接阶段呢?

image9).png 我们先来看看编译的.o产物中是否含有load和category。通过编译日志找到编译产物目录。通过otool命令查看结果如下

image10).png 可以很明显看到编译产物中是含有load和category信息的,那么就是linker阶段把category合并到了class中。

category合并的原因是什么呢?

找到了合并的时机,那么接下来我们就看为什么会被合并。所幸Xcode的linker是开源的,可以在ld中找到。 通过查阅源码,我发现个可疑的函数。

image11).png 如果load方法的数量<2则跳过。否则,就把categories数组清空。

image12).png 这里是进行merge操作,merge完方法之后如果categories为空,则跳过。

image13).png

image14).png 这两个函数中可以看到,如果categories为空,则不进行merge。

image15).png 这几段代码的意思是,如果load的数量大于等于2,则不进行优化。 那么现在可以回答我们当初的问题:

  1. 为什么有load方法的category会被合并到class呢?

    a. 因为此时class没有load方法,当load的数量小于2,会被合并。

  2. 为什么当class中有load方法的时候就不会被合并呢?

    a. 因为此时category+class的load方法数量为2,不会被合并优化。

  3. category是在什么时候被合并的?

    a. 在linker的时候被合并。

  4. category合并的原因是什么呢?

    a. Apple在linker阶段做的优化,减少section,减少runtime的初始化,加快启动速度,减少包大小。