本系列是学习iOS底层原理过程中的记录笔记第三篇,往期目录:
探索iOS底层原理开篇——对象本质
探索iOS底层原理第二篇——KVO
探索iOS底层原理第三篇——KVC
国际惯例抛出面试题:
- Category的实现原理
- Category和Class Extension的区别是什么?
- Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
- load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
Category分析
我们先通过objc源码查看category内部结构:
结构体里定义了实例方法列表、类方法列表、协议列表、成员属性列表等信息,这也能进一步说明分类确实可以添加成员的。
接下啦我们再objc-runtime-new.mm
文件中找到分类的有效实现方法:
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
//定义方法列表的二维数组
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
//定义成员列表的二维数组
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
//定义协议列表的二维数组
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
//取出分类方法存放在mlists列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
//取出分类成员属性存放在proplists列表
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
//取出分类协议存放在protolists列表
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
//关键代码:讲分类方法列表附加到类对象
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
然后我们发现了关键代码attachLists
, 这个方法就是添加分类后,改变分类所在的类对象方法列表结构,我们进去看看具体实现:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
/*分类方法添加到类对象方法列表后,memmove会讲原来的方法列表往后挪动addedCount单位,
addedCount就是分类方法的个数,所以分类的方法列表放在了最前面*/
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
从memmove
和memcpy
可知:
分类方法添加到类对象方法列表后,会将原来的方法列表往后挪动addedCount
单位,addedCount
就是分类方法的个数,所以分类的方法列表放在了最前面.因此
分类的方法总在类对象方法列表的最前面!!!!!
分类的方法总在类对象方法列表的最前面!!!!!
分类的方法总在类对象方法列表的最前面!!!!!
+load方法分析
首先,+load方法会在runtime加载类、分类时调用
每个类、分类的+load,在程序运行过程中只调用一次
调用顺序
先调用类的+load
按照编译先后顺序调用(先编译,先调用)
调用子类的+load之前会先调用父类的+load再调用分类的+load,按照编译先后顺序调用(先编译,先调用)
我们用代码验证上面的结论:新建Person、Dog、Cat,和继承Person的Student子类,每个类都实现+load
方法,在main.m
里只实现Person
的test
方法:
我们对比一下编译文件的前后顺序:选中项目->Build Phases->Compile Source
试着更换位置,打印的结果也是同步更改:
上述的结果都验证了:
类的load方法调用顺序是按照编译顺序,先编译先调用
我们再试着把Person
和Student
类互换位置,按道理应该是Student
先调用,应为Student
在Person
前面,但因为Person
是Student
的父类,前面说了调用子类的Load
方法会先调用父类的load方法,我们验证一下:[图片上传中...(image.png-ff1a18-1557630608086-0)]
可以看出Person
的load
方法确实在Student
前调用,所以:
调用子类的+load之前会先调用父类的+load
我们新建Person的两个分类,Person+test1
和Person+test2
,子类S度、Student也新建两个分类,看一下他们的load方法调用顺序:
可以看出
分类的load方法调用顺序也是跟编译顺序有关,先编译先调用
把其中两个分类放在前面,也是
先调用本类的load方法,然后再调用分类的load方法:
结果是 Person +Test2分类调用了,这其实也是有编译顺序有关,当
本类和分类都实现非load方法时,实际会调用分类方法。当多个分类实现非load方法时,由编译顺序决定,后编译的那个分类调用
+initialize方法分析
+initialize方法会在类第一次接收到消息时调用
-
调用顺序
先调用父类的+initialize,再调用子类的+initialize
(先初始化父类,再初始化子类,每个类只会初始化1次)
直接代码验证,为每个类都实现initalize方法Person和Student调用两个test方法,结果如下:
可以看出,
+initialize方法会在类第一次接收到消息时调用,并且只调用一次
接下来修改一下代码,只调用子类Student方法两次,运行结果如下:
可以看出,Person虽然没有被调用过任何方法,但是Student作为子类被调用了方法,会调用Student的initialize方法,调用之前会先调用父类Person的initialize方法。所以
先调用父类的+initialize,再调用子类的+initialize
结果load方法的打印结果,可以分析出:
load方法是当类加载编译后执行,不管有没有用到;
initialize只在调用过方法(使用过)才调用,两个方法都只调用一次;
当多个分类都实现initialize方法时,最后编译的分类调用initialize方法
总结:
-
Category的实现原理
- Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
-
Category和Class Extension的区别是什么?
1.Class Extension在编译的时候,它的数据就已经包含在类信息中
2.Category是在运行时,才会将数据合并到类信息中 -
Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
1.有load方法
2.load方法在runtime加载类、分类的时候调用
3.load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用 -
load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
1.load方法是当类加载编译后执行,不管有没有用到;
2.initialize只在调用过方法(使用过)才调用,两个方法都只调用一次;
3.当多个分类都实现initialize方法时,最后编译的分类调用initialize方法