欢迎阅读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
等底层原理,及组件化实战
过程。敬请关注。