
在前两篇文章中,我们分析了类的加载。但是在类的加载过程中,不仅仅是类本身的加载,还有分类,类的扩展等的加载。下面我们就来分析以下,分类和类的扩展是怎么加载的。
一、CPP文件分析分类
首先我们将.m文件转换成CPP文件,以此来观察以下分类在底层是什么样子的。这里我们再来回忆一下,生成CPP文件的两种终端指令:
-
clang: (这里也可以不要后面的-o xxx.cpp)
$ clang -rewrite-objc xxx.m -o xxx.cpp
xcrun
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx.m -o xxx.cpp
1. 这里我们定义一个Person类,并创建它的分类:

2. 利用终端,将Person-Jax.m文件转换成CPP文件:

3. 查看Person+Jax.cpp文件,探索分类的底层结构:
在该文件中,我们看到了_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;
};
-
通过下面的代码,我们可以推断出
Person-Jax在底层的结构:

其中我们通过_category_t可以了解到一下信息: 这里还有一条信息,那就是
_category_t中的name,是Jax。(这里大家可能会有疑问,既然是Jax,那Person-Jax里面为什么是Person呢?因为现在是静态编译,编译器不知道赋什么值。所以随机给的是Person)-
通过
_category_t可以看到,有两个_method_list_t:-
instanceMethods:实例方法列表。 -
classMethods:类方法列表。
-
_protocol_list_t:协议列表。_prop_list_t:属性列表。(在分类中,可以定义属性,但是不会自动生成getter和setter方法)-
通过文件中最下方代码,我们还可以得出一条结论:分类是存放在
__DATA段的__objc_catlist中。

二、分类的加载
我们回想一下,我们在iOS底层探索 --- 类的加载(中)的时候,遇到的methodizeClass吗?这里有一条官方注释是这样写的:Attaches any outstanding categories。也就是说,我们的分类是在这里被附加的。那我们就再次探索一下这个函数。
我们会发现,方法列表、属性列表、协议列表等等,它们的附加都与rwe有关系:

也就是说,附加的时候,必然要有rwe的存在。那我们就去找rwe:


在我们进入ext()函数之后,对于源码有点迷茫。但是下面的extAllocIfNeeded()结合ext()就有点意思了(根据字面意思:“需要的情况下,alloc ext�”)。整体来看,也就是说,如果有ext,那就必然能执行get,获得ext。(有点绕,大家好好捋一下思路)
这里可以将ext理解为一个标识符。我们都知道:
-
ro是clean memory(ro是只读的,不需要的时候可以清除,需要的时候再从磁盘中读取。复制到rw), -
rw是dirty memory(rw是动态分配的,比如我们的分类里面的数据,是昂贵的) - 这里就有一个问题了,不是所有的类都需要
rw,也就是说ro的数据已经能够满足需求了,这个时候就有了rwe的出现。(当需要动态加载的时候,就有一个标识符ext;如果没有,就普通的从ro里面去获取。Extention)
2.1 extAllocIfNeeded()
我们来搜索一下,extAllocIfNeeded()看一下其在什么地方调用(截取其中一个):

(
rwe是在动态运行时才会被创建,这一点可以根据官方的注释得到。有兴趣的可以看一下``extAllocIfNeeded()`的调用函数的注释或者分析以下。)
2.2 attachCategories
extAllocIfNeeded()在attachCategories中也被调用了,由于我们现在分析的是分类,所以我们关注的重点就是attachCategories。

这里面的auto rwe = cls->data()->extAllocIfNeeded();同时也可以证明,rwe是通过extAllocIfNeeded()来获取的。
这里大家对比一下,extAllocIfNeeded()的调用对象是不同的,上面是rw,这里是cls->data();这里大家不要误解,cls->data()的返回值是class_rw_t *类型的。

看到
bits不知道大家有没有熟悉的感觉,没错,我们在iOS底层探索 --- 类的结构探索(上)里面探索过:
-
attachCategories
这里我们全局搜索一下,看一下,分类的加载在哪里被调用。
搜索下来,有两处调用:-
attachToClass->attachCategories -
load_categories_nolock->attachCategories


-
既然有两个地方调用了attachCategories,那我们就通过断点调试,一个一个的分析。
2.2.1 attachToClass
同样的我们全局搜所attachToClass,发现其只在methodizeClass中有调用:

虽然有三处调用,但是其中两处的调用,受previously(函数中的一个判断条件) 影响。
- 这里的
previously来自于realizeClassWithoutSwift(static Class realizeClassWithoutSwift(Class cls, Class previously)); -
realizeClassWithoutSwift是我们探索过的,在_read_images中被调用,而previously传入的是nil。因此在methodizeClass中,只有一次会被调用。(这里大家会有疑问,既然传入的是nil,为什么还要多此一举;其实这是一个备用参数,方便调节用的。)
也就是说我们只需要研究:
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
三、分类加载的几种情况
在上面我们分析了分类的底层结构,我们得知分类在底层是以结构体的形式存在。那么我们接下来探索一下分类的加载。
在这之前,我们补充一个之前没有明确说明的知识点:懒加载类 和 非懒加载类。
在iOS中,为了提高对类的处理效率和性能,会对类进行识别。当类需要使用的时候,系统才会对类进行实现;如果没有使用就不会实现。
像这种需要实现才进行加载的类,被称为懒加载类;反之,无论是否需要实现都进行加载的类,被称为非懒加载类。(我们日常开发中,通过XCode创建的类,默认都是懒加载类)
一般情况下,我们可以通过+load方法,来调整我们自己实现的类。自定义类实现+load方法,就可以变为非懒加载类。因为+load方法的调用是在main之前的。
那么此时关于分类的加载我们就有四种情况:
- 1、
主类和分类都实现+load方法。 - 2、
主类实现+load方法,分类不实现。 - 3、
主类不实现+load方法,分类实现。 - 4、
主类和分类都不实现+load方法。
断点调试,在main函数里面调用Person

对于测试类和分类,我们使用下面的:

3.1、主类 和 分类 都实现+load方法
我们在attachCategories中打上断点(在我们上面添加的代码中,规避系统方法)。

-
didInitialAttachCategories
这里还是要说一下这个变量的,didInitialAttachCategories初始化为false:

在load_images里面,有这样一个判断语句:

相信看到这里,大家都会明白,为什么是第一次load_images之后才会执行(那段官方注释)。
3.2 主类实现+load方法,分类不实现
同样的我们通过断点调试,得到如下的函数调用栈:
这个情况与下面的情况类似,看下面的分析。
3.3 主类不实现+load方法,分类实现
这里有一个细节,当前我们的主类并没有实现+load,但是我们在_read_image函数里面,还是走的非懒加载,这说明,分类实现+load之后,主类被迫营业了。(这里大家好好理解一下,分类是针对主类实现的。)

- 这里就有疑问了,既然没有执行
attachCategories;那么分类里面的信息怎么加载的呢?此时分类是非懒加载类,按理说是要执行attachCategories的呀。
这里我们通过断点调试来探索一下:
-
首先我们在
realizeClassWithoutSwift函数里面添加如下代码,并添加断点:

-
然后运行工程,断点调试,控制台操作如下(这里的操作,在iOS底层探索 --- 类的结构探索(上)里面我们做过探索,这里就不再赘述):

3.4 主类 和 分类 都不实现+load方法
这种情况下,前面这些函数都没有调用。
四、load_categories_nolock
上面我们知道,在我们没有实现+load(懒加载)的情况下,分类依然能都从data里面加载,那这个时候分类的数据从哪里来的呢?这个时候我们就要去探索一下load_categories_nolock。
-
count从哪里来的呢?

大家注意看,count的初始值是0;那么count是在哪里变化的呢?(函数内部没有count的赋值操作)
其实我们将这个代码块折叠一下,就清晰了:

这就相当于一个
block的调用,先执行下面的代码,才会进入上面的代码块。
-
catlist
既然count的值跟catlist有关系,那我们就进去看一下:

可以看到,我们的catlist是从MachO文件中获取的。
也就是说分类也是从MachO中加载进来的。这也就验证了上面,我们为什么能够从data中获取分类的数据。也就是说MachO会直接的去加载整个的数据结构。
注意:不要随便的去实现load方法,这样会打乱MachO的数据加载,当我们自己去实现+load方法之后,就有了上面一大堆的流程(包括其中的一些算法),这是非常耗时的。像分类中实现+load方法,就是非常不可取的。
五、多个分类
如果有多个分类,但是分类不完全实现+load方法,主类实现+load方法。这个时候,会跟3.2的情况一样吗?
这里我们可以在load_categories_nolock中打一个断点,看一下count的数值就知道了。(这样做的理由是,因为有分类实现了+load,那么就一定会走load_categories_nolock;那么我们在这个函数里面,看一下在非懒加载类的流程中,有几个分类会走这里,就可以得到我们想知道的答案了。)
-
首先多添加几个分类,其中一个不实现
+load

-
断点调试

可以看到,count的数量是3;说明此时的情况和3.1的情况是一样的。










