在之前,理解了类是如何从
Mach-O
加载到内存的,这次来了解下分类
是如何加载到类
中的,以及分类和类搭配使用的情况。
1. 分类的本质
- 在 main 中定义 LGPerson 的分类 LG
@interface LGPerson (LG)
@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cate_instanceMethod;
- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
+ (void)cate_classMethod;
@end
@implementation LGPerson (LG)
- (void)cate_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2{
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod3{
NSLog(@"%s",__func__);
}
+ (void)cate_classMethod{
NSLog(@"%s",__func__);
}
@end
- 使用
clang
查看编译的main.cpp
文件:
clang -rewrite-objc main.m -o main.cpp
从上面图中可以看出,分类的类型是_category_t
(倒数第二个是 0,表示没有协议,所以赋值为 0)
-
cpp
文件中,搜索struct _category_t
,如下所示:
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods; 实例方法
const struct _method_list_t *class_methods; 类方法
const struct _protocol_list_t *protocols; 协议
const struct _prop_list_t *properties; 属性
};
- 搜索
_CATEGORY_INSTANCE_METHODS_LGPerson_
,找到其底层实现
其中有 3 个方法,格式为sel+签名+地址
,是_objc_method
结构体的属性:
- 继续搜索
_objc_method
,其中的对应关系如下:
struct _objc_method {
struct objc_selector * _cmd; 对应 sel
const char *method_type; 对应 方法签名
void *_imp; 对应函数地址
};
2. 分类的加载
2.1 创建分类
创建 LGPerson 的两个分类:LGA 和 LGB:
2.2 加载过程
在之前介绍类的加载时提到了rwe
的加载,其中分析了分类的data 数据
是如何加载到类的,而且分类的加载顺序是:LGA
->LGB
的顺序加载到类中,即越晚加进来,越在前面
。
其中查看了methodizeClass
的源码实现,可以发现类的数据和分类的数据是分开处理的,主要是因为在编译阶段,就已经确定好了方法的归属位置(即实例方法存储在类中,类方法存储在元类中),而分类是后面才加进来的。
其中分类需要通过attatchToClass
添加到类,然后才能在外界进行使用,在此过程,我们已经知道了分类加载步骤的后面两个步骤,分类的加载主要分为 3 步:
- 分类数据加载时机:根据类和分类是否实现 load 方法来区分不同的时机
-
attachCategories
准备分类数据 -
attachLists
将分类数据添加到主类中
2.3 分类的加载时机
2.3.1 主类 LGPerson + 分类 LGA\LGB 均实现+load 方法
下面我们来探索一下分类数据的加载时机,以主类 LGPerson + 分类 LGA\LGB 均实现+load 方法为例。
通过第二步反推第一步的加载时机
在之前我们了解到,在走到 attachCategories
方法时,必然会有分类数据的加载,可以通过反推法查看在什么时候调用 attachCategories
的,通过查找,有两个方法中调用:
-
load_categories_nolock
方法中
-
addToClass
方法中,这里经过调试,在主类和分类都实现load
方法时,是不会走到 if 流程中的:
-
不加任何断点,运行
objc
代码,可以得出以下打印日志,通过日志可以发现addToClass
方法的下一步就是load_categories_nolock
方法就是加载分类数据:
-
全局搜索
load_categories_nolock
的调用,有两次调用-
一次在
loadAllCategories
方法中
-
一次在
_read_images
方法中
-
-
但是经过调试发现,是不会走
_read_images
方法中的 if 流程,而是走的loadAllCategories
方法中的
-
全局搜索查看
loadAllCategories
的调用,发现是在load_images
时调用的
-
我们给
attachCategories
加自定义逻辑的断点,bt
查看堆栈信息:
所以综上所述:该情况下的分类的数据加载时机的反推路径为:
attachCategories -> load_categories_nolock -> loadAllCategories -> load_images
而我们的分类加载正常的流程的路径为:
realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories
其中正向和反向的流程如下图所示:
2.3.2 主类 LGPerson + 分类 LGA 实现 +load 方法,LGB 不实现 +load 方法
- 断点定在
attachCategories
中加自定义逻辑部分,一步步往下执行
- 继续往下执行,会再次来到
attachCategories
方法中断住
总结:只要有一个分类是非懒加载分类,那么所有的分类都会被标记为非懒加载分类
,意思就是加载一次 已经开辟了rwe
,就不会再次懒加载,重新去处理LGPerson
。
2.4 分类和类的搭配使用
通过上面的两个例子,我们可以大致将类 和 分类 是否实现+load
的情况分为4种
类+分类 | . | . |
---|---|---|
. | 分类实现+load | 分类未实现+load |
类实现+load | 非懒加载类+非懒加载分类 | 非懒加载类+懒加载分类 |
类未实现+load | 懒加载类+非懒加载分类 | 懒加载类+懒加载分类 |
2.4.1 非懒加载类 + 非懒加载分类
即主类实现了+load
方法,分类同样实现了+load
方法,在前文分类的加载时机时,我们已经分析过这种情况,所以可以直接得出结论,这种情况下:
- 类的数据加载是通过
_getObjc2NonlazyClassList
加载,即ro、rw
的操作,对rwe
赋值初始化,是在extAlloc
方法中 - 分类的数据加载是通过
load_images
加载到类中的
2.4.2 非懒加载类 + 懒加载分类
类和分类的加载是在read_image
就加载数据了,数据来自data
,data
在编译时期就已经完成,即data
中除了类的数据,还有分类的数据,与类绑定在一起
2.4.3 懒加载类 + 懒加载分类
其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成
2.4.4 懒加载类 + 非懒加载分类
只要分类实现了load
,会迫使主类提前加载,即主类强行转换为非懒加载类样式,即在_read_images
中不会对类做实现操作,需要在load_images
方法中触发类的数据加载,即rwe初始化
,同时加载分类数据
如下图所示:
3.load_images
load_images
方法的主要作用是加载镜像文件,其中最重要的有两个方法:perpare_load_methods
(加载) 和 call_load_methods
(调用)
3.1 perpare_load_methods
进入perpare_load_methods
源码:
-schedule_class_load
方法,这里是根据类的继承链递归调用获取 load
,直到cls
不存在才结束递归
,目的是为了确保父类的 load
优先加载:
-
进入
add_class_to_loadable_list
,主要是获取load
方法,然后将load方法
和cls类名
一起加到loadable_classes
表中:
-
进入
getLoadMethod
,主要是获取方法的sel 为 load
的方法:
-add_category_to_loadable_list
获取所有的非懒加载分类
中的load
方法,将分类名``+load
方法加入表loadable_categories
3.2call_load_methods
主要有 3 部分操作:
反复调用类的
+load
,直到不再有调用一次分类的
+load
-
如果有类或更多未尝试的分类,则运行更多的
+load
-
进入
call_class_loads
,主要是加载类的load
方法
注意:方法中有两个隐藏参数,第一个为id 即self,第二个为sel,即cmd
-
call_category_loads
,主要是加载一次分类的 load 方法
3.3 总结
综上所述,load_images
方法整体调用过程及原理图示如下:
-
调用过程图示:
并且:
父类的 load 方法优先于子类调用
主类的 load 方法优先于分类调用
分类的 load 方法则取决于编译顺序。
-
原理图示
主要分为两步:
- 从所有的
非懒加载类
和分类
中的+load
分别添加到表中 - 调用类和分类的
+load
方法