在上一篇文章OC底层原理11:类的加载(上)中,理解了类是如何从Mach-O加载到内存中,这次我们来解释下分类是如何加载到类中的,以及分类和类搭配使用的情况。
分类的本质
前提:在main中定义LGPerson的分类LG
探索分类的本质,有以下三种方式:
- 【方式一】通过
clang - 【方式二】通过
Xcode文档搜索Category - 【方式三】通过
objc源码搜索category_t
【方式一】通过clang
clang -rewrite-objc main.m -o main.cpp 查看底层编译,即 main.cpp
- 其中
分类的 类型是_category_t其中有两个method_list_t,分别表示
实例方法和类方法 LG分类分类的倒数第二个0,表示的是
没有协议,所以赋值为0- 搜索
_CATEGORY_INSTANCE_METHODS_LGPerson_,找到其底层实现:其中有3个方法,格式为:
sel+签名+地址,是method_t结构体的属性即key
【方式二】通过Xcode文档搜索 Category
通过Xcode文档搜索 Category
【方式三】通过objc源码搜索 category_t
总结
综上所述,分类的本质 是一个category_t类型
- 有两个属性:
name(类的名称)和cls(类对象) - 有两个
method_list_t类型的方法列表,表示分类中实现的实例方法+类方法 - 一个
protocol_list_t类型的协议列表,表示分类中实现的协议 - 一个
prop_list_t类型的属性列表,表示分类中定义的属性,一般在分类中添加的属性都是通过关联对象来实现 - 需要注意的是,分类中的
属性是没有set、get方法
分类的加载
前提:创建LGPerson的两个分类:LGA、LGB
在上一篇iOS-底层原理 17:类的加载(上)文章中的
realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加载,其中分析了分类的data数据 是如何 加载到类中的,且分类的加载顺序是:LGA -> LGB的顺序加载到类中,即越晚加进来,越在前面。
其中查看methodizeClass的源码实现,可以发现类的数据和 分类的数据是分开处理的,主要是因为在编译阶段,就已经确定好了方法的归属位置(即实例方法存储在类中,类方法存储在元类中),而分类是后面才加进来的.
分类的加载主要分为3步:
- 1⃣️分类数据
加载时机:根据类和分类是否实现load方法来区分不同的时机 - 2⃣️
attachCategories准备分类数据 - 3⃣️
attachLists将分类数据添加到主类中
从上一篇iOS-底层原理:类的加载(上)我们已经知道了分类加载三步骤的后面两个步骤,下面探讨下分类的加载时机。
分类的加载时机
情况1: 以主类LGPerson + 分类LGA、LGB 均实现+load方法为例。
通过第二步2⃣️数据准备(attachCategories准备分类数据)反推第一步的加载时机
通过上一篇文章我们了解到,在走到attachCategories方法时,必然会有分类数据的加载,可以通过反推法查看 在什么时候调用attachCategories的,通过查找,有两个方法中调用:
-
load_categories_nolock方法中 -
attachToClass方法中,这里经过调试发现,从来不会进到if流程中,除非加载两次,一般的类一般只会加载一次 -
不加任何断点,运行objc代码,可以得出以下打印日志,通过日志可以发现
attachToClass方法的下一步就是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:主类+分类LGA实现+load,分类LGB不实现+load方法
- 断点定在
attachCategories中加自定义逻辑部分,一步步往下执行 - 继续往下执行,会再次来到
attachCategories方法中断住
总结:只要有一个分类是非懒加载分类,那么所有的分类都会被标记位非懒加载分类,意思就是加载一次 已经开辟了rwe,就不会再次懒加载,重新去处理 LGPerson.
分类和类的搭配使用
通过上面的两个情形,我们可以大致将类 和 分类 是否实现+load的情况分为4种:
| 类+分类 | ||
|---|---|---|
| 分类实现+load | 分类未实现+load | |
| 类实现+load | 非懒加载类+非懒加载分类 | 非懒加载类+懒加载分类 |
| 类未实现+load | 懒加载类+非懒加载分类 | 懒加载类+懒加载分类 |
- 【情况1】
非懒加载类 + 非懒加载分类 - 【情况2】
非懒加载类 + 懒加载分类 - 【情况3】
懒加载类 + 懒加载分类 - 【情况4】
懒加载类 + 非懒加载分类
非懒加载类 + 非懒加载分类
即主类实现了+load方法,分类同样实现了+load方法
.在前文分类的加载时机时,我们已经分析过这种情况,所以可以直接得出结论,这种情况下:
- 类的数据加载是通过
_getObjc2NonlazyClassList加载,即ro、rw的操作,对rwe赋值初始化,是在extAlloc方法中 分类的数据加载是通过load_images加载到类中的 其调用路径为:map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass,此时的mlists是一维数组,然后走到load_images部分load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此时的mlists是二维数组下面为源码中调试的打印日志:
非懒加载类 与 懒加载分类
即主类实现了+load方法,分类未实现+load方法.
- 打开
realizeClassWithoutSwift中的自定义断点,看一下ro- 查看
kc_ro从上面的打印输出可以看出,方法的顺序是
LGB—LGA-LGPerson类,此时分类已经 加载进来了,但是还没有排序,说明在没有进行非懒加载时,通过cls->data读取Mach-O数据时,数据就已经编译进来了,不需要运行时添加进去
- 查看
- 来到
methodizeClass方法中断点部分 - 来到
prepareMethodLists的for循环部分 - 来到
fixupMethodList方法中的if (sort) {部分其中
SortBySELAddress的源码实现如下:根据名字的地址进行排序 - 走到
mlist->setFixedUp();,在读取list
通过打印发现,仅对同名方法进行了排序,而分类中的其他方法是不需要排序的,其你imp地址是有序的(从小到大) -- fixupMethodList中的排序只针对 name 地址进行排序
不加任何断点,运行程序,获取打印日志:
总结:非懒加载类 与 懒加载分类的数据加载,有如下结论:
类 和 分类的加载是在read_images就加载数据了- 其中
data数据在编译时期就已经完成了
懒加载类 与 懒加载分类
即主类和分类均未实现+load方法
- 不加任何断点,运行程序,获取打印日志
其中
realizeClassMaybeSwiftMaybeRelock是消息流程中慢速查找中有的函数,即在第一次调用消息时才有的函数. - 在
readClass断住,然后读取kc_ro,即读取整个data此时的
baseMethodList的count还是16,说明也是从data中读取出来的,所以不需要经过一层缓慢的load_images加载进来
总结:懒加载类 与 懒加载分类的数据加载是在消息第一次调用时加载.
懒加载类 与 非懒加载分类
即主类未实现+load方法, 分类实现了+load方法
- 不加任何断点,运行程序,获取打印日志
- 在打印的日志中没有看到
load_categories_nolock方法,查看attachCategories -- extAlloc -- attachToClass -- attachCategories,在attachToClass中加断点 - 在
readClass方法中断住,查看kc_ro其中
baseMethodList的count是8个,打印看看:对象方法3个+属性的setget方法共4个+1个cxx方法 ,即 现在只有主类的数据 - 为了调试分类的数据加载, 继续往下执行,
bt查看堆栈:load_images -> loadAllCategories -> load_categories_nolock
总结:懒加载类 + 非懒加载分类的数据加载,只要分类实现了load,会迫使主类提前加载,即 主类 强行转换为 非懒加载类样式
总结
类和分类搭配使用,其数据的加载时机总结如下:
- 【情况1】
非懒加载类 + 非懒加载分类,其数据的加载在load_images方法中,首先对类进行加载,然后把分类的信息贴到类中 - 【情况2】
非懒加载类 + 懒加载分类,其数据加载在read_image就加载数据,数据来自data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起 - 【情况3】
懒加载类 + 懒加载分类,其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成 - 【情况4】
懒加载类 + 非懒加载分类,只要分类实现了load,会迫使主类提前加载,即在_read_images中不会对类做实现操作,需要在load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据 如下图所示:
补充:load_images原理分析
load_images方法的主要作用是加载镜像文件,其中最重要的有两个方法:prepare_load_methods(发现) 和 call_load_methods(调用)
- 进入
load_images源码实现
进入prepare_load_methods源码
- 进入
_getObjc2NonlazyClassList -> schedule_class_load源码,这里主要是根据类的继承链递归调用获取load,直到cls不存在才结束递归,目的是为了确保父类的load优先加载
- 进入
add_class_to_loadable_list,主要是将load方法和cls类名一起加到loadable_classes表中
- 进入
getLoadMethod,主要是获取方法的sel为load的方法
_getObjc2NonlazyCategoryList -> realizeClassWithoutSwift -> add_category_to_loadable_list,主要是将非懒加载分类的load方法加入表中- 进入
add_category_to_loadable_list实现,获取所有的非懒加载分类中的load方法,将分类名+load加入表loadable_categories
- 进入
进入call_load_methods源码,主要有3部分操作
-
反复调用
类的+load,直到不再有 -
调用一次分类的+load -
如果有类或更多未尝试的分类,则运行更多的
+load -
进入
call_class_loads,主要是加载类的load方法其中
load方法中有两个隐藏参数,第一个为id 即self,第二个为sel,即cmd -
call_category_loads,主要是加载一次分类的load方法
综上所述,load_images方法整体调用过程及原理图示如下:
-
调用过程图示
-
原理图示
主要分为两步
-
从所有的
非懒加载类和分类中的+load分别添加到表中 -
调用
类和分类的+load方法
-
引用
本文学习引用iOS-底层原理 18:类的加载(下),在此致谢