selector

iOS底层探索 --- 类的加载(下)

2021-07-21  本文已影响0人  Jax_YD
image

在前两篇文章中,我们分析了类的加载。但是在类的加载过程中,不仅仅是类本身的加载,还有分类,类的扩展等的加载。下面我们就来分析以下,分类和类的扩展是怎么加载的。


一、CPP文件分析分类

首先我们将.m文件转换成CPP文件,以此来观察以下分类在底层是什么样子的。这里我们再来回忆一下,生成CPP文件的两种终端指令:

$ clang -rewrite-objc xxx.m -o xxx.cpp
$ 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;
};

我们通过CPP文件知道了分类在底层是_category_t结构,这个时候我们也可以在源码中搜索一下,来对比一下:

image

二、分类的加载

我们回想一下,我们在iOS底层探索 --- 类的加载(中)的时候,遇到的methodizeClass吗?这里有一条官方注释是这样写的:Attaches any outstanding categories。也就是说,我们的分类是在这里被附加的。那我们就再次探索一下这个函数。

我们会发现,方法列表属性列表协议列表等等,它们的附加都与rwe有关系:

image

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

image
image

在我们进入ext()函数之后,对于源码有点迷茫。但是下面的extAllocIfNeeded()结合ext()就有点意思了(根据字面意思:“需要的情况下,alloc ext�”)。整体来看,也就是说,如果有ext,那就必然能执行get,获得ext。(有点绕,大家好好捋一下思路)

这里可以将ext理解为一个标识符。我们都知道:

2.1 extAllocIfNeeded()

我们来搜索一下,extAllocIfNeeded()看一下其在什么地方调用(截取其中一个):

image
(rwe是在动态运行时才会被创建,这一点可以根据官方的注释得到。有兴趣的可以看一下``extAllocIfNeeded()`的调用函数的注释或者分析以下。)

2.2 attachCategories

extAllocIfNeeded()attachCategories中也被调用了,由于我们现在分析的是分类,所以我们关注的重点就是attachCategories

image

这里面的auto rwe = cls->data()->extAllocIfNeeded();同时也可以证明,rwe是通过extAllocIfNeeded()来获取的。

这里大家对比一下,extAllocIfNeeded()的调用对象是不同的,上面是rw,这里是cls->data();这里大家不要误解,cls->data()的返回值是class_rw_t *类型的。

image
看到bits不知道大家有没有熟悉的感觉,没错,我们在iOS底层探索 --- 类的结构探索(上)里面探索过:
image

既然有两个地方调用了attachCategories,那我们就通过断点调试,一个一个的分析。

2.2.1 attachToClass

同样的我们全局搜所attachToClass,发现其只在methodizeClass中有调用:

image

虽然有三处调用,但是其中两处的调用,受previously(函数中的一个判断条件) 影响。

也就是说我们只需要研究:

objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

三、分类加载的几种情况

在上面我们分析了分类的底层结构,我们得知分类在底层是以结构体的形式存在。那么我们接下来探索一下分类的加载

在这之前,我们补充一个之前没有明确说明的知识点:懒加载类非懒加载类

在iOS中,为了提高对类的处理效率和性能,会对类进行识别。当类需要使用的时候,系统才会对类进行实现;如果没有使用就不会实现。

像这种需要实现才进行加载的类,被称为懒加载类;反之,无论是否需要实现都进行加载的类,被称为非懒加载类。(我们日常开发中,通过XCode创建的类,默认都是懒加载类

一般情况下,我们可以通过+load方法,来调整我们自己实现的类。自定义类实现+load方法,就可以变为非懒加载类。因为+load方法的调用是在main之前的。

那么此时关于分类的加载我们就有四种情况:

在下面的探索中,我们会在源码中添加下面这样的代码,来辅助我们做探索。添加到我们需要探索的函数中,部分内容根据各自需求可做改动:

bool isMeta = cls->isMetaClass();
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "Person") == 0) {
       if (!isMeta) {
           printf("%s -Person....\n",__func__);
       }
}

断点调试,在main函数里面调用Person

image

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


image

3.1、主类分类 都实现+load方法

我们在attachCategories中打上断点(在我们上面添加的代码中,规避系统方法)。

image

这样我们通过追踪断点,得到了如下的函数调用栈:
load_images -> loadAllCategories -> load_categories_nolock -> attachCategories

在这个之前还有 _read_image -> realizeClassWithoutSwift这样的一个流程。
因为在_read_image中有这样一段注释:

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之后,主类被迫营业了。(这里大家好好理解一下,分类是针对主类实现的。)

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

其实我们将这个代码块折叠一下,就清晰了:

image
这就相当于一个block的调用,先执行下面的代码,才会进入上面的代码块。

可以看到,我们的catlist是从MachO文件中获取的。

也就是说分类也是从MachO中加载进来的。这也就验证了上面,我们为什么能够从data中获取分类的数据。也就是说MachO会直接的去加载整个的数据结构。

注意:不要随便的去实现load方法,这样会打乱MachO的数据加载,当我们自己去实现+load方法之后,就有了上面一大堆的流程(包括其中的一些算法),这是非常耗时的。像分类中实现+load方法,就是非常不可取的。


五、多个分类

如果有多个分类,但是分类不完全实现+load方法,主类实现+load方法。这个时候,会跟3.2的情况一样吗?

这里我们可以在load_categories_nolock中打一个断点,看一下count的数值就知道了。(这样做的理由是,因为有分类实现了+load,那么就一定会走load_categories_nolock;那么我们在这个函数里面,看一下在非懒加载类的流程中,有几个分类会走这里,就可以得到我们想知道的答案了。)

上一篇下一篇

猜你喜欢

热点阅读