OC底层原理18:类的加载(下)分类

张建 lol

在上一篇 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)

  • 来到 prepareMethodListsfor 循环部分

    • p addedLists[0]
    • p addedLists[1]
    • p *$1
    • p *$2

  • 来到 fixupMethodList 方法中的 if (sort) { 部分

    • 其中 SortBySELAddress 的源码实现如下:根据名字的地址进行排序

  • 走到 mlist->setFixedUp(); ,在读取list

    • p 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

此时的 baseMethodListcount 还是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】懒加载类 + 懒加载分类 ,其数据加载 推迟到第一次消息时,数据同样来自 datadata编译时期 就已经完成

  • 【情况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.