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

image

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


一、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;
};
  • 通过下面的代码,我们可以推断出Person-Jax在底层的结构:

    image

    其中我们通过_category_t可以了解到一下信息:

  • 这里还有一条信息,那就是_category_t中的name,是Jax。(这里大家可能会有疑问,既然是Jax,那Person-Jax里面为什么是Person呢?因为现在是静态编译,编译器不知道赋什么值。所以随机给的是Person

  • 通过_category_t可以看到,有两个_method_list_t

    • instanceMethods:实例方法列表。
    • classMethods:类方法列表。
  • _protocol_list_t:协议列表。

  • _prop_list_t:属性列表。(在分类中,可以定义属性,但是不会自动生成gettersetter方法)

  • 通过文件中最下方代码,我们还可以得出一条结论:分类是存放在__DATA段的__objc_catlist中。

    image

我们通过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理解为一个标识符。我们都知道:

  • roclean memory(ro 是只读的,不需要的时候可以清除,需要的时候再从磁盘中读取。复制到rw),
  • rwdirty memory(rw是动态分配的,比如我们的分类里面的数据,是昂贵的)
  • 这里就有一个问题了,不是所有的类都需要rw,也就是说ro的数据已经能够满足需求了,这个时候就有了rwe的出现。(当需要动态加载的时候,就有一个标识符ext;如果没有,就普通的从ro里面去获取。Extention

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
    这里我们全局搜索一下,看一下,分类的加载在哪里被调用。
    搜索下来,有两处调用:

    • attachToClass -> attachCategories
    • load_categories_nolock -> attachCategories
    image

    image

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

2.2.1 attachToClass

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

image

虽然有三处调用,但是其中两处的调用,受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

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


image

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

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

image

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

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

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

image

  • 这里就有疑问了,既然没有执行attachCategories;那么分类里面的信息怎么加载的呢?此时分类非懒加载类,按理说是要执行attachCategories的呀。

这里我们通过断点调试来探索一下:

  • 首先我们在realizeClassWithoutSwift函数里面添加如下代码,并添加断点:

    image

  • 然后运行工程,断点调试,控制台操作如下(这里的操作,在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的赋值操作)

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

image

这就相当于一个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;那么我们在这个函数里面,看一下在非懒加载类的流程中,有几个分类会走这里,就可以得到我们想知道的答案了。)

  • 首先多添加几个分类,其中一个不实现+load

    image

  • 断点调试

    image

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容