OC底层原理18:类的加载(下)分类
在上一篇 OC底层原理17:类的加载(上) 中,理解了 类是如何从Mach-O加载到内存
中,这次我们来解释下 分类
是如何 加载
到 类
中的,以及 分类和类搭配使用
的情况
分类的本质
前提:在main中定义 LGperson
的分类LG
探索分类的本质,有以下三种方式
- 【方式一】通过
clang
- 【方式二】通过
Xcode
文档搜索Category
- 【方式三】通过
objc
源码搜索category_t
方式一:通过clang
【方式一】
clang -rewrite-objc main.m -o main.cpp
查看底层编译,即main.cpp
- 其中
分类
的类型是_category_t
- 分类的倒数第二个0,表示的是
没有协议
,所以赋值为0
- 其中
- 搜索
struct _category_t
,如下所示
其中有两个 method_list_t
,分别表示 实例方法
和 类方法
;protocol_list_t
表示协议列表;
- 搜索
_CATEGORY_INSTANCE_METHODS_LGPerson_
,找到其底层实现
其中有3个方法,格式为:sel+签名+地址
,是 method_t
结构体的属性即key
搜索method_t,其中对应关系如下
name
对应sel
type
对应方法签名
imp
对应函数地址
同时,我们发现了一个问题:查看 _prop_list_t
,明明 分类
中定义了 属性
,但是在底层编译中并没有看到属性,如下图所示,这是因为 分类中定义的属性没有相应的set、get方法
,我们可以通过 关联对象
来设置(关于如何设置 关联对象
,我们将在后续的扩展中进行说明)
方式二:通过Xcode文档搜索 Category
如果不会 clang
,可以通过 Xcode
文档搜索 Category
方式三:通过objc源码搜索 category_t
还可以通过objc源码搜索 category_t
类型
总结 面试重点
综上所述,分类的 本质
是一个 _category_t
类型
有两个属性:
name(类的名称)
和cls(类对象)
有两个
method_list_t
类型的方法列表,表示分类中实现的实例方法+类方法
一个
protocol_list_t
类型的协议列表
,表示分类中实现的协议
一个
prop_list_t
类型的属性列表
,表示分类中定义的属性
,一般在分类中添加的属性都是通过关联对象来实现需要注意的是,分类中的
属性
是没有set、get
方法
分类的加载
前提:创建LGPerson的两个分类:LGA、LGB
在上一篇文章中的 realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories
中提及了 rwe
的加载,其中分析了分类的 data数据
时如何加载到 类
中的,且分类的加载顺序是:LGA -> LGB
的顺序加载到类中,即 越晚加进来,越在前面
其中查看 methodizeClass
的源码实现,可以发现 类的数据
和 分类的数据
是分开处理的,主要是因为在 编译阶段
,就已经确定好了 方法的归属位置
(即 实例方法
存储在 类
中,类方法
存储在 元类
中),而 分类
是后面才加进来的
其中分类需要通过 attatchToClass
添加到类,然后才能在外界进行使用,在此过程,我们已经知道了分类加载三步骤的后面两个步骤,分类的加载主要分为3步:
分类数据
加载时机
:根据类和分类是否实现load方法
来区分不同的时机attachCategories
准备分类数据
attachLists
将分类数据添加到主类中
分类的加载时机
下面我们来探索 分类数据的加载时机
,以 主类LGPerson + 分类LGA、LGB 均实现+load方法
为例
通过第二步数据准备反推第一步的加载时机
通过上一篇文章我们了解到,在走到
attachCategories
方法时,必然会有分类数据的加载
,可以通过反推法
查看在什么时候调用attachCategories
的,通过查找,有两个方法中调用- load_categories_nolock方法中
addToClass
方法中,这里经过调试发现,从来不会进到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
其中正向和反向的流程如下图所示:
我们再来看一种情况:主类+分类LGA实现+load,分类LGB不实现+load方法
断点定在
attachCategories
中加自定义逻辑部分,一步步往下执行p entry.cat
p *$0
继续往下执行,会再次来到
attachCategories
方法中断住p entry.cat
p *$0
总结:
只要有一个分类是非懒加载分类,那么所有的分类都会被标记位非懒加载分类
,意思就是加载一次已经开辟了rwe
,就不会再次懒加载,重新去处理LGPerson
分类和类的搭配使用
通过上面的两个例子,我们可以大致将 类
和 分类
是否实现 +load
的情况分为4种:
【情况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
- 查看
p kc_ro->baseMethodList
p 1.get(0) ~ p1.get(4)
p 1.get(5)、 p1.get(10)
从上面的打印输出可以看出,方法的顺序是 LGB—LGA-LGPerson类
,此时分类已经加载进来了,但是还没有排序,说明在没有进行非懒加载时,通过 cls->data
读取 Mach-O
数据时,数据就已经编译进来了,不需要运行时添加进去
来到
methodizeClass
方法中断点部分p list
p $0->get(0)- p $0->get(5)
来到
prepareMethodLists
的for
循环部分p addedLists[0]
p addedLists[1]
p *$1
p *$2
来到
fixupMethodList
方法中的if (sort) {
部分- 其中
SortBySELAddress
的源码实现如下:根据名字的地址进行排序
- 其中
走到
mlist->setFixedUp();
,在读取listp mlist
p $7->get(0) ~ p $7->get(3)
p $7->get(4) ~ p $7->get(6)
通过打印发现,仅 对同名方法进行了排序
,而分类中的其他方法是不需要排序的,其你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方法 ,即 现在只有主类的数据
- 查看kc_ro结构
p kc_ro->baseMethodList
p 0->get(0) ~ p0->get(3)、p $0->get(7)
- 为了调试分类的数据加载, 继续往下执行,
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
方法
- Post title:OC底层原理18:类的加载(下)分类
- Post author:张建
- Create time:2020-11-19 15:21:46
- Post link:https://redefine.ohevan.com/2020/11/19/OC底层原理/OC底层原理18:类的加载(下)分类/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.