本篇是解决上一篇的第一个问题的;
读过我实例对象、类对象和元类对象是如何分工的的朋友应该知道一个类的实例方法是存放在类对象中的,类方法是存在元类对象中,这些是在编译阶段就能确定的,但是我们并没有发现分类的方法,也就有了下面的疑问:
- 分类的加载时机;
- 分类中的方法存放的地方;
- 如果分类和原来的类拥有共同的方法,调用结果如何;
- 如果同一个类的多个分类拥有同一个方法,调用结果如何;
- load方法是如何加载的;
- 继承中的load方法是如何调用的;
- initialize方法如何加载的;
- 综合结论
本文的篇幅有些长,请读者耐心看,看本篇文章前请先读实例对象、类对象和元类对象是如何分工的,建议读者按照我的顺序自己新建类
准备工作
新建项目
1 | //Animal.h |
通过clang命令拿到Animal+Test的cpp代码
1 | struct _category_t { |
这里只取一些关键的代码,有段下面的代码
1 | static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= { |
L_OBJC_LABEL_CATEGORY_$ [1]也就是_category_t的cls字段,所以上面的意思就是,这个分类是_OBJC_$_CATEGORY_Animal_$_Test类的,并且存储在__DATA的__objc_catlist中。
一、分类的加载时机
这里需要查看runtime的源码,这里给出加载的分类的方法查找路径
* void _objc_init(void)
* map_images
* map_images_nolock
* _read_images
* remethodizeClass
* attachCategories
读者可以顺着这个查看下源码,这里会给出一些关键性的代码,看到_read_images中的
1 | category_t **catlist = _getObjc2CategoryList(hi, &count); |
_getObjc2CategoryList的内容是
1 | GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist"); |
之前我们的分类信息是存放在__DATA的__objc_catlist中,这里然后从这里读取
从而说明:
分类是在runtime加载的
二、分类中的方法存放的地方
上面得出分类的信息是在runtime阶段加载的,下面看看加载具体方式,进入attachCategories中,为了方便查看,这里只留下了方法相关的代码,读者可以对比源码
1 | static void |
cats->list是某个类的所有分类mlist是某个分类的方法列表while (i--)中的i--说明是反向遍历,后编译的会在数组的最前面;mlists[mcount++] = mlist;说明mlists是一个二维数组,
mlists最后相当于以下结构
1 | mlists = [ |
接着通过
1 | rw->methods.attachLists(mlists, mcount); |
将分类的方法附在原来类的方法列表上
attachLists内容如下
1 | void attachLists(List* const * addedLists, uint32_t addedCount) { |
这里运行了2个c语言的函数memmove和memcpy,这2个函数做的事情如下

从而说明:
分类的方法还是加到原来的类对象和元类对象的方法列表中的;
三、如果分类和原来的类拥有共同的方法,调用结果如何
编译之前的工程,会发现输出
1 | Animal(Test)--eat |
会发现是输出分类的打印,由上面二的分析,很容易知道是这个答案,在加载分类的方法是反向遍历的,从而导致了分类的方法会调用,原来的方法不会调用
所以
如果分类和原来的类拥有共同的方法,调用结果如何?
答案是:调用分类的方法
四、如果同一个类的多个分类拥有同一个方法,调用结果如何
我们新加一个Animal的分类Animal+Test2
1 | //Animal.h |
这个时候运行程序会发现输出的还是
1 | Animal(Test)--eat |
这是什么原因呢?通过二我们知道,加载分类的方法是反向遍历的,说明Animal(Test)是比Animal(Test2)后编译的,通过查看targets -> Build Phases -> Compile Sources证明了这个观点

我们调整下顺序

会发现这个时候输出
1 | Animal(Test2)--eat |
所以
如果同一个类的多个分类拥有同一个方法,调用结果如何?
答案是:*执行后编译的
五、load方法是如何加载的
我们分别在Animal、Animal+Test和Animal+Test2都增加个+load方法,运行程序却发现如下打印
1 | Animal--load |
如果按照二的理解不应该是只会打印Animal(Test2)--load的吗?还是要回到runtime源码
* _objc_init
* load_images
* prepare_load_methods
* schedule_class_load
* call_load_methods
先看prepare_load_methods
1 | void prepare_load_methods(const headerType *mhdr) |
classlist装着所有类的load方法;categorylist装着所有分类的load方法;
schedule_class_load内容如下
1 | static void schedule_class_load(Class cls) |
schedule_class_load(cls->superclass);保证了拿类的load方法前会先去拿父类的load方法
再到call_load_methods方法
1 | void call_load_methods(void) |
- 先调用
call_class_loads也就是之前存储在classlist的类的load方法; - 再调用
call_category_loads也就是之前存储在categorylist的分类的load方法
从而
关于load方法有以下特点
- 是直接通过指针调用的,
- 程序一启动就会调用,并且只会调用一次;
- 所有的load方法都会调用,
- 先调用类的load方法,类的load方法中又会先调用父类的load方法,然后调用分类的load方法
- 分类的load方法调用的顺序是编译的顺序
所以一开始的现象也就解释了
六、继承中的load方法是如何调用的
我们新增一个Animal的子类Dog,并且增加Dog+Test和Dog+Test2
编译顺序如下

其实知道五以后,都能猜到答案了,你猜对了吗
1 | Animal--load |
Dog的父类是Animal,所以Dog的load方法会最先调用,由于类的load方法优先分类的,从而接着是Dog--load,最后是分类的,按照编译的顺序执行
关于这些分类的组合有很多,关键还得理解本质,读者可自行组合分析
七、initialize方法如何加载的
把之前的load方法都注释掉,换成initialize方法,main.m代码如下
1 | #import <Foundation/Foundation.h> |
输出
1 | Animal(Test2)--initialize |
可以看到我们并没有显示调用initialize,但是这个方法却调用了,我们知道[Animal alloc]是调用了objc_msgSend()方法,说明应该是这里做了处理了,相关类如下
* class_getInstanceMethod
* lookUpImpOrNil
* lookUpImpOrForward
* _class_initialize
lookUpImpOrForward如下
1 | ... |
_class_initialize如下
1 | void _class_initialize(Class cls) |
这里做了很多省略,从这里可以看出:
- 当对象第一次收到消息的时候会调用
_class_initialize,并且会记住已经初始化过了 - 当父类的
_class_initialize没有调用时会初始化父类的调用;
所以
1 | Animal(Test2)--initialize |
解释如下
[Animal alloc]发现isInitialized是NO,从而调用initialize方法,但是分类的优先级高,原因见二;[Dog alloc]发现父类的isInitialized是YES,但是自己没有initialize方法,会去调用父类的initialize方法,回到了1,所以父类的会调用2遍
八、综合结论
常规方法
initialize方法
- 先初始化父类的;
再初始化子类的,如果子类没有实现,父类实现了会被多次调用,使用的时候需要注意
其他方法
原类和分类有共同方法,只会执行分类的方法;
- 同一个类的多个分类拥有共同的方法,执行最后编译的分类方法
load方法
- 先执行类的load方法;
- 再执行分类的load方法;
- 多个分类的load方法,先编译的先执行;
- 调用子类的load方法前,会调用父类的load方法