iOS底层探索 --- 类的加载(下)
在前两篇文章中,我们分析了类的加载。但是在类的加载过程中,不仅仅是类本身的加载,还有分类,类的扩展等的加载。下面我们就来分析以下,分类和类的扩展是怎么加载的。
一、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
类,并创建它的分类:
image
2. 利用终端,将Person-Jax.m
文件转换成CPP
文件:
image
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;
};
-
通过下面的代码,我们可以推断出
imagePerson-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
方法) -
通过文件中最下方代码,我们还可以得出一条结论:分类是存放在
image__DATA
段的__objc_catlist
中。
我们通过CPP文件知道了分类在底层是
image_category_t
结构,这个时候我们也可以在源码中搜索一下,来对比一下:
二、分类的加载
我们回想一下,我们在iOS底层探索 --- 类的加载(中)的时候,遇到的methodizeClass
吗?这里有一条官方注释是这样写的:Attaches any outstanding categories
。也就是说,我们的分类是在这里被附加的。那我们就再次探索一下这个函数。
我们会发现,方法列表
、属性列表
、协议列表
等等,它们的附加都与rwe
有关系:
也就是说,附加的时候,必然要有rwe
的存在。那我们就去找rwe
:
image
在我们进入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底层探索 --- 类的结构探索(上)里面探索过:image
-
attachCategories
这里我们全局搜索一下,看一下,分类的加载在哪里被调用。
搜索下来,有两处调用:-
attachToClass
->attachCategories
-
load_categories_nolock
->attachCategories
image -
既然有两个地方调用了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
方法。
在下面的探索中,我们会在源码中添加下面这样的代码,来辅助我们做探索。添加到我们需要探索的函数中,部分内容根据各自需求可做改动:
bool isMeta = cls->isMetaClass(); const char *mangledName = cls->nonlazyMangledName(); if (strcmp(mangledName, "Person") == 0) { if (!isMeta) { printf("%s -Person....\n",__func__); } }
断点调试,在main
函数里面调用Person
对于测试类和分类,我们使用下面的:
image
3.1、主类
和 分类
都实现+load
方法
我们在attachCategories
中打上断点(在我们上面添加的代码中,规避系统方法)。
这样我们通过追踪断点,得到了如下的函数调用栈:
load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
在这个之前还有
image_read_image
->realizeClassWithoutSwift
这样的一个流程。
因为在_read_image
中有这样一段注释:
也就是说,这里的调用会被推迟到第一次load_images
调用之后。
-
didInitialAttachCategories
这里还是要说一下这个变量的,didInitialAttachCategories
初始化为false
:
image
在load_images
里面,有这样一个判断语句:
image
相信看到这里,大家都会明白,为什么是第一次load_images
之后才会执行(那段官方注释)。
3.2 主类
实现+load
方法,分类
不实现
同样的我们通过断点调试,得到如下的函数调用栈:
_read_image
->realizeClassWithoutSwift
->methodizeClass
->attachToClass
并没有走
attachCategories
这个情况与下面的情况类似,看下面的分析。
3.3 主类
不实现+load
方法,分类
实现
_read_image
->realizeClassWithoutSwift
->methodizeClass
->attachToClass
并没有走
attachCategories
这里有一个细节,当前我们的主类
并没有实现+load
,但是我们在_read_image
函数里面,还是走的非懒加载
,这说明,分类实现+load
之后,主类被迫营业了。(这里大家好好理解一下,分类
是针对主类
实现的。)
- 这里就有疑问了,既然没有执行
attachCategories
;那么分类
里面的信息怎么加载的呢?此时分类
是非懒加载类
,按理说是要执行attachCategories
的呀。
这里我们通过断点调试来探索一下:
-
首先我们在
imagerealizeClassWithoutSwift
函数里面添加如下代码,并添加断点:
-
然后运行工程,断点调试,控制台操作如下(这里的操作,在iOS底层探索 --- 类的结构探索(上)里面我们做过探索,这里就不再赘述):
image
注意!!!
通过控制台的打印,我们可以看到,此时
ro
里面已经有了分类
信息(注意看methodList
中,count = 13
)。我们再回一下,
ro
是怎么来的:auto ro = (const class_ro_t *)cls->data();
。
上面讲过,cls->data()
返回的是bits.data()
这也就是说,
ro
中分类
的数据,来自于data
。
上面3.2
的情况也是一样的。
3.4 主类
和 分类
都不实现+load
方法
这种情况下,前面这些函数都没有调用。
推迟到第一次消息发送的时候,初始化。
四、load_categories_nolock
上面我们知道,在我们没有实现+load
(懒加载)的情况下,分类依然能都从data
里面加载,那这个时候分类的数据从哪里来的呢?这个时候我们就要去探索一下load_categories_nolock
。
-
count
从哪里来的呢?
image
大家注意看,count
的初始值是0
;那么count
是在哪里变化的呢?(函数内部没有count
的赋值操作)
其实我们将这个代码块折叠一下,就清晰了:
这就相当于一个
block
的调用,先执行下面的代码,才会进入上面的代码块。
-
catlist
既然count
的值跟catlist
有关系,那我们就进去看一下:
image
可以看到,我们的catlist
是从MachO
文件中获取的。
也就是说分类
也是从MachO
中加载进来的。这也就验证了上面,我们为什么能够从data
中获取分类
的数据。也就是说MachO
会直接的去加载整个的数据结构。
注意:不要随便的去实现load
方法,这样会打乱MachO
的数据加载,当我们自己去实现+load
方法之后,就有了上面一大堆的流程(包括其中的一些算法),这是非常耗时的。像分类
中实现+load
方法,就是非常不可取的。
五、多个分类
如果有多个分类
,但是分类
不完全实现+load
方法,主类
实现+load
方法。这个时候,会跟3.2的情况一样吗?
这里我们可以在load_categories_nolock
中打一个断点,看一下count
的数值就知道了。(这样做的理由是,因为有分类
实现了+load
,那么就一定会走load_categories_nolock
;那么我们在这个函数里面,看一下在非懒加载类
的流程中,有几个分类
会走这里,就可以得到我们想知道的答案了。)
-
首先多添加几个分类,其中一个不实现
image+load
-
断点调试
image
可以看到,count
的数量是3
;说明此时的情况和3.1
的情况是一样的。