欢迎阅读iOS底层系列(建议按顺序)
1.本文概述
本文旨在通过分析分类的加载流程,类和分类分别在懒加载和非懒加载时的表现,完善所有类的加载流程。
2.分类相关探索
2.1 分类初探
上一篇文章类的加载分析分析了map_images的主要流程,此流程中最后为分类的加载部分,现在回头来解析下。
其先从macho中__objc_catlist段下读取分类,然后遍历读取的分类。这两个步骤接收的类型都是category_t。
2.2 分类的数据结构
自然的点击category_t,可以看到它的结构体:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
struct property_list_t *_classProperties;
};
name:经过验证是分类的名称,即括号里面的字段(部分文章说是类名)cls:对应的类地址,remapClass时通过shiftclass得到类名instanceMethods:实例方法classMethods:类方法instanceProperties:实例属性classProperties:类属性
为什么需要instanceMethods和classMethods?
分类可以添加实例方法,也可以添加类方法。在类结构被加载时,对应的元类也会被加载。我们都知道,实例方法存在类中,类方法存在元类中。所以在类加载时取instanceMethods的数据,元类加载时取classMethods的数据。实例方法和类方法的处理不一样,自然需要分开存储。
classProperties 是什么?
从Xcode 8开始,LLVM已经支持Objective-C显式声明类属性了,主要为了与Swift中的类属性互操作。
在定义属性时,增加class修饰符,即可定义为类属性,类属性需要手动实现setter和getter。在需要解藕时,类属性或许能帮的上你。
2.3 验证类和分类的懒加载和非懒加载的存在
上篇文章分析了启动时类的加载,但是说明了只限于非懒加载类,这么说还存在懒加载类嘛?类如果区分懒加载和非懒加载,分类是否也区分?
2.3.1 验证类的懒加载和非懒加载存在
在类的加载源码中,可以看到这么一条注释:
// Realize non-lazy classes (for +load methods and static instances)
译:实现非懒加载类(通过+load方法和静态构造函数)
那么,就按它所说,创建CJFStudent,CJFTeacher,CJFPerson类,任意两个类实现下+load方法,一个不实现:
+ (void)load{
NSLog(@"%s",__func__);
}
在源码中加入验证方法代码:
运行后可以看到控制台输出:
实现了+load的类被加载了,未实现的没有被加载。
得以验证:
存在懒加载类和非懒加载类,并且实现了+load方法为非懒加载类。
2.3.2 验证分类的懒加载和非懒加载存在
既然类的懒加载和非懒加载区别是+load方法,那分类也如法炮制验证下,
创建CJFPerson+text分类,在实现分类的源码中加入验证代码:
然后依次实现和不实现此分类的+load,得到控制台输出:
可以看到,启动时分类的加载和是否实现+load方法有关。也顺便验证了,分类的数据结构中,name为分类的名称。
得以验证:
无论分类还是类,是否实现了+load方法,确实会影响到启动时的加载流程。
那类和分类的是否只是根据+load方法或者静态构造函数来判断是懒加载还是非懒加载的?关键的+load方法何时被执行?类和分类互相之间是否也有关联影响到加载流程?这些问题一个一个来分析下。
2.4 load_images() 分析
既然+load方法如此关键,自然是有研究它的必要性。
上一篇文章类的加载分析中,只分析了libObjc从dyld接收的三件事中的map_images,那现在分析下load_images。
直接看其源码:
较为简单,只有两个步骤:
-
prepare_load_methods:准备load方法 -
call_load_methods:调用load方法
2.4.1 prepare_load_methods()
schedule_class_load:调度类的load方法。
static void schedule_class_load(Class cls){
...
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
...
}
总的来说,通过递归的方法,先父类后子类的把+load方法添加到对应的loadable表中,这解释了为什么+load方法会先调用父类,在调用子类。
其保存的是个loadable_class结构体:
struct loadable_class {
Class cls; // may be nil
IMP method;
};
结构体包含imp,方便后续调用。
- 添加分类的
+load方法到对应的loadable列表中(和类的loadable表不是同一张),添加之前调用realizeClassWithoutSwift,防止类未实现(另有妙用,做个标记,后面还要回来)。 其保存的是个loadable_category结构体:
struct loadable_category {
Category cat; // may be nil
IMP method;
};
结构体同样包含imp,方便后续调用。
2.4.2 call_load_methods()
call_class_loads:调用类的表中的+load方法
按照表中顺序,依次取出调用+load方法的imp,通过函数指针的方式实现快速调用。
call_category_loads:调用分类的表中的+load方法 同样是,按照表中顺序,依次取出调用+load方法的imp,通过函数指针的方式实现快速调用。
通过这个调用流程,也解释了为什么+load方法会先调用类,在调用分类。
而无继承关系的类+load方法调用取决于被加入表的顺序,也就是Compile Sources的顺序;同一个类的分类的+load方法调用也是取决于被加入表的顺序,也是Compile Sources的顺序。
至于直接使用函数指针调用的方式也很好理解,目前还是处于启动阶段,如果采用发送消息是比较耗时的。
2.5 类和分类的懒加载和非懒加载的表现
已知两种研究对象,两种加载类型,那就有4种排列组合。
2.5.1 非懒加载类和非懒加载分类
这种情况就是上一篇文章类的加载分析分析的,
同时实现+load方法,那类必然会在启动时被加载。
所以
read_images - realizeClassWithoutSwift - methodizeClass,这个顺序是固定的,可是来到methodlizeClass准备附加分类时,对应分类还未被加入哈希表中,
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
这一步unattachedCategoriesForClass获取的分类就是空的,分类并不是在methodlizeClass被附加,而是在read_images中处理分类的部分进行加载。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
...
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
...
}
这里先把未附加但是已读取到的分类addUnattachedCategoryForClass加入对应的哈希表中,判断类是否实现cls->isRealized(),因为是非懒加载类,类必然已经实现,就调用remethodizeClass重新附加分类。
2.5.2 非懒加载类和懒加载分类
先在创建的分类中添加方法名为categoryInstanceMethod和categoryClassMethod的两个方法。然后再分析:
既然是非懒记载类,那read_images - realizeClassWithoutSwift - methodizeClass顺序是固定的,
static void methodizeClass(Class cls){
...
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
...
}
来到methodlizeClass准备附加分类时,对应分类是懒加载的,不会被存入哈希表中,unattachedCategoriesForClass获取的就是空的(这里和第一种情况同样是获取到的哈希表是空的,但是第一种是还未添加到表中,这种是不会被添加到表中,需要加以区分)。
这时候懒加载分类被安置到哪里了?
在methodlizeClass查看类的ro可以看到
categoryInstanceMethod和categoryClassMethod分别被添加到CJFPerson的类和元类的ro中(这是类和元类两次调用的图,看它们的count不一样就可以证明),也验证分类的数据结构中,需要有instanceMethods和classMethods字段的原因(分开存储)。
所以当是非懒加载分类时,分类的数据从早被编译好的ro中读取出来,接下来被复制到类的rw中。
2.5.3 懒加载类和懒加载分类
因为是懒加载分类,数据依然是先被加载到ro中;
因为是懒加载类,在启动时类不会被加载。
那么类何时被加载?
可以想像,懒加载的原理就是第一次被使用的时候被加载。那么尝试调用下方法使用它,调用方法自然会来到消息发送流程,这部分在方法查找流程分析过lookUpImpOrForward方法,但是那时候忽略了一个步骤:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver{ ...
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
...
}
在发送消息时,会判断发送消息的这个类是否被实现过,如果没有,就会层层调用到realizeClassWithoutSwift去实现类。
那realizeClassWithoutSwift - methodizeClass的顺序依然是固定的。
在methodizeClass中,从ro读取分类信息到类的rw中。
2.5.4 懒加载类和非懒加载分类
因为是懒加载类,那read_images中的非懒加载类处理不会被执行;
因为是非懒加载分类,read_images中的分类处理会被执行。
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
...
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
...
}
此时,分类数据被addUnattachedCategoryForClass添加到哈希表中,可是类还没有被实现,cls->isRealized()为false,不会进来执行remethodizeClass附加分类信息到类。
这也好理解,类都没准备好,分类怎么能附加。
那么这时候,类怎么加载呢,也是等第一次发送消息嘛,可是分类的+load方法已经在启动时急不可耐的需要被执行,此时分类的主体--类却还没被加载,有些说不过去。
还记得上面分类的+load方法调用流程嘛(特意标记过,可以回头结合这里理解下)。
在分类的+load方法被调用前,系统会执行realizeClassWithoutSwift做个容错来保证类被加载过。可是除了容错,主要目的是同时兼顾处理了这么一种情况(懒加载类和非懒加载分类)。
所以,当非懒加载分类的+load方法被执行前,会调用realizeClassWithoutSwift - methodizeClass来加载类,此时就不是通过ro了,
static void methodizeClass(Class cls){
...
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
...
}
而是通过unattachedCategoriesForClass取出哈希表中的分类,调用attachCategories把分类附加到类中。
2.5.5 总结
这四种情况比较绕,需要静下心来分析理解原理,这里也直接给出结论:
- 是否是懒加载,取决于
+load方法。 - 如果分类是懒加载的,编译时就会被加入
ro。 - 如果分类是非懒加载的,根据类是否是懒加载来决定加载流程。如果是懒加载类,会在分类的
+load方法调用前被加载;如果是非懒加载类,类更早一步被加载。 - 如果类是懒加载的,根据分类是否是懒加载来决定加载流程。如果是懒加载分类,在第一次使用时被加载;如果是非懒加载分类,在分类的
+load方法调用前被加载。
2.6 分类和类拓展
分析完分类,顺便分析下类拓展。
分类和类拓展的区别是什么?这可能是面试过程中经常被问到的问题之一。
类拓展可以添加属性;分类不能直接添加属性。答案是很简单,那为什么分类不能直接添加属性?
创建CJFPerson的类拓展CJFPerson+Extension,并导入头文件,添加属性extensionName,在之前CJFPerson的分类中添加属性categoryName。
然后
clang -rewrite-objc CJFPerson.m -o CJFPerson.cpp
查看编译后的源文件,搜索两个属性名
很明显,extensionName在编译时就已经被确定,categoryName却没有。
所以类拓展在编译时就被当作类的一部分被加载,而分类却是运行时把数据附加到类的。
而编译后ro已经确定,属性会生成的ivar要加入ro是不被允许的,rw中又没有ivar_list字段。而且系统只声明类setter和getter方法,并没有实现。
所以分类不能直接添加属性,但是可以使用关联对象动态添加。
需要注意的是,如果没有导入类拓展的头文件,类拓展不会被系统编译。
2.7 +initialize 分析
+initialize和+load方法因为都是系统较早执行的方法,经常被拿来做比较。
简单来分析下+initialize的实现源码:
总的来说,系统调用某个类的+initialize前,会先递归实现父类的+initialize,然后调用callInitialize来真正实现
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
callInitialize内部是通过发送消息的方式调用SEL_initialize的。
那么+initialize何时被调用?
创建CJFPerson类,父类和分类,都实现+initialize,初始化CJFPerson,然后运行
然后在做三件事件事:
- 取消
CJFPerson的初始化,在运行,发现没有任何打印。 - 初始化多个
CJFPerson,在运行,发现没有多次打印。 - 注释类和分类的
+initialize,在运行,发现父类的被打印两次。
测试后,基本可以有这样的结论
+initialize在类第一次发送消息的时候被执行
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
...
if (initialize && !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
...
}
来到lookUpImpOrForward,发现还有一处细节,发送消息前会检查这个类是否被初始化,未初始化会调用initializeNonMetaClass,结论得以验证。
- 分类的
+initialize把主类的+initialize"覆盖"了。
这是因为是发送消息,方法会被添加到方法列表,分类的方法会添加在主类之前,导致这一现象。上篇文章详细的分析过这个现象。
- 如果子类没有实现
+initialize,父类有实现,在初始化子类时,父类的+initialize会被多次调用。
这是因为initializeNonMetaClass中是递归查找父类的,所有优先执行父类的+initialize,又因为是发送消息的,在调用子类的+initialize时,子类方法列表找不到,就会去父类的方法列表查找,造成多次调用的现象。
3.写在最后
分类是日常开发中常用的技术,也是面试中的常客,是必须要掌握的。分类和类是不可分割的,所以这一章的内容是需要和上一章类的加载分析结合来理解的。
近期在研究逆向相关,导致底层系列的文章更新较慢。后续会陆续更新多线程,锁,block等底层原理,及组件化实战过程。敬请关注。