1、往期文章

iOS 底层探索文章系列

上一篇中,我们分析了 类是如何加载到内存中的,那么今天我们探索的是,分类是如何加载到类里面的,以及分类和类搭配使用的情况。

2、探索分类的本质

我们用以下三种方式来探索分类的本质是什么

2.1 通过 Xcode Documentation

Documentation 搜索 Category 关键字

image.png

image.png

2.2 通过 objc 源码搜索 category_t

image.png

2.3 通过 clang

在终端输入 clang -rewrite-objc main.m -o main.cpp ,然后查看生成的c++文件

在源码中,我们已经得知分类的类型为 catagory_t

image.png
image.png

然后我们发现分类的倒数第二个的值为0,说明没有协议,所以被赋值为0。继续搜索 _CATEGORY_INSTANCE_METHODS_LGPerson_,找到其底层实现

image.png

其中有一个方法,格式为:sel+签名+函数地址。这个就是我们前面分析过的 method_t 的结构,我们在 objc 源码里面搜索 method_t

image.png

发现跟我们上面所说的一一对应了

  • name = sel
  • types = 签名
  • imp = 函数地址

同时,我们也注意到一个问题,在分类中,我们定义了两个属性,但是编译成c++之后,并没有看到相关属性,这是因为 分类中定义的属性没有相应的set和get方法的实现,只有方法的声明,后续我们将在关联对象的篇章中进行分析。

image.png

2.4 总结

综上所述,分类的本质就是一个 category_t 的结构体类型

image.png

3、分类的加载

我们先给 LGPerson 创建两个分类 LGA 和 LGB

image.png

在上一篇文章中,我们查看 methodizeClass 的实现时,可以发现 类的数据分类的数据 是分开处理的,主要原因是因为在 编译阶段,就已经确定好了方法的归属位置,而分类是后面才加进来的。

其中分类需要通过 attachLists 方法添加到类之后,外界才能进行试验,在此过程,我们已经知道了分类加载三个步骤中的后面两个步骤

  • 分类数据 加载时机: 根据 类和分类是否实现 load 方法 来区分不同的时机。
  • attachCategories 方法中准备分类数据
  • attachLists 中将分类数据添加到主类

4、分类的加载时机

下面我们来探索 分类数据的加载时机,以主类和两个分类实现 load 方法为例

4.1 通过第二步反推第一步的加载时机

通过上一篇文章我们了解到,当在走到 attachCategories 方法时,必然会有分类数据的加载,可以通过反推法查看 在什么时候调用 attachCategories 的,通过查找,有两个方法中调用

  • load_categories_nolock 方法中

image.png

  • attachToClass 方法中,这里经过调试发现,基本不会进到if流程中,除非加载两次,一般的类一般只会加载一次

image.png

我们直接运行 objc 代码,可以得出以下打印日志,通过输出日志可以发现 attachToClass 方法的下一步就是 load_categories_nolock 方法,进行加载分类数据

image.png

我们全局搜索 load_categories_nolock 的调用的地方,发现只有两次调用

一次是在 loadAllCategories 方法

image.png

另外一次是在 _read_images 方法,但是经过调试,是不会进入这个if判断流程的,而是走的 loadAllCategories

image.png

最后,我们全局搜索 loadAllCategories 在哪里地方进行了调用,发现只有 load_images 方法里面进行了调用

image.png

4.2 通过堆栈信息分析

attachCategories 中加自定义逻辑的断点,bt 查看堆栈信息。

image.png

可得,在上述情况下分类的数据加载时机为 attachCategories -> load_categories_nolock -> loadAllCategories -> load_images,而我们分类的正常加载流程为 realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories

我们看另外一种情况,主类和分类(LGA)实现 load,分类(LGB)不实现

image.png

可以看到,只要有一个分类是非懒加载分类,那么所有的分类都会被标记位非懒加载分类

5、类和分类的搭配使用

通过上面的例子,我们可以将类和分类是否实现 load 方法分为四种情况

类和分类 分类实现 load 分类未实现 load
类实现 load 非懒加载类 + 非懒加载分类 非懒加载类 + 懒加载分类
类未实现 load 懒加载类 + 非懒加载分类 懒加载类 + 懒加载分类

5.1 非懒加载类 + 非懒加载分类

  • 类的数据加载是通过 _getObjc2NonlazyClassList 加载,即对 rorw 的操作和对 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 是二维数组。

image.png

5.2 非懒加载类 + 懒加载分类

image.png

  • 类和分类的加载在 read_images 就加载好数据了
  • 其中 data 数据在编译期就已经完成了

5.3 懒加载类 + 非懒加载分类

image.png

  • 此情况下会迫使主类提前加载,即主类强行转换为 非懒加载类样式

5.4 懒加载类 + 懒加载分类

image.png

  • 此情况下懒加载类与懒加载分类的数据加载是在 消息第一次调用时加载

5.5 总结

  • 1. 非懒加载类 + 非懒加载分类,其数据的加载在 load_images 方法中,首先对类进行加载,然后把分类的信息贴到类中。
  • 2. 非懒加载类 + 懒加载分类,其数据加载在 read_image 就加载数据,数据来自data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起
  • 3. 懒加载类 + 懒加载分类,其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成
  • 4. 懒加载类 + 非懒加载分类,只要分类实现了load,会迫使主类提前加载,即在 _read_images 中不会对类做实现操作,需要在 load_images 方法中触发类的数据加载,即rwe初始化,同时加载分类数据