美文网首页selector
iOS底层探索 --- 类的加载(下)

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

作者: Jax_YD | 来源:发表于2021-07-21 09:34 被阅读0次
    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的情况是一样的。

    相关文章

      网友评论

        本文标题:iOS底层探索 --- 类的加载(下)

        本文链接:https://www.haomeiwen.com/subject/vvadmltx.html