iOS底层探究-----类的加载 下 | 8月更文挑战

519 阅读11分钟

前言

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

根据前两篇文章 类的加载 上类的加载 中,我们对类的加载有了一定的了解,并且梳理两条流程:

  • 宏观流程: dyld --> images --> 内存 --> (方法、协议、属性。。。)

  • 细节流程 read_images --> read_class(名称-类) --> realizeClassWithoutSwift(ro-rwsuperClassisa...) --> methodizeClass() --> prepareMethodLists(写入方法名 + 排序)

因为当程序编译完成后, ro 已经是固定不变内存(clean memory)空间,只读,加载后不会发生改变的内存空间,包括类名称、方法、协议和实例变量的信息;当处于运行时,数据空间是会发生改变的,而 ro 是不能改变的,那么就有了 rw 的产生,把 ro 的内容赋值给 rw ,而 rw 是属于 dirty memory ,由于其动态性,可以往类中添加属性、方法、协议。在运行时会发生变更的内存。但是 rw 的内存比较昂贵,而且,并不是每一个的类都有分类,也不是每一个类都会进行动态添加方法、属性、协议,所以就对 rw 进行扩展,然后就有了 rwe,在这里面处理对应的类的信息。

资源准备

attachCategories 反推思路

根据上篇文章的分析,分类的加载有两条线路:

  • methodizeClass -> attachToClass -> attachCategories
  • load_images -> loadAllCategories -> load_categories_nolock -> attachCategoriesattachCategories最终调用到了attachList

F7B070F5-E442-4E2B-8FE7-B4D2DC0666D2.png

既然attachList重点,那么就进入函数里面,查看他的实现:

3FD0F3B2-DE60-43DA-B939-91983A0AB63D.png

  • 当类里面没有方法数组,且有一个分类时,也就是(!list  &&  addedCount == 1)判断,那么不开辟新数组,直接赋值给了list,此时,内部存储的相当于是元素;

  • 当没有方法数组,且有不确定数量的分类时,也就是else的判断,先开辟新数组,然而由源码可知,新加入的addedLists是一个* *结构,且通过for循环插入在数组的最前面,然后再判断有没有 oldList(主类方法列表) ,如果有,就直接把他作为一个整体放入到新开辟的数组中,此时oldList就是一个数组指针。那么这个新开辟的数组就是一个二维数组了,如下图注解图①

  • 当有方法数组,且有不确定数量的分类时,也就是(hasArray())判断,先开辟新数组,然后通过for循环把旧数组里面的元素倒序插入新开辟的数组中,然后再通过for循环把要加入的 addedLists的元素,插入到新数组的最前面,最后释放旧数组。那么下次过来的时候,又设置新的array,如下图注解图②

注解图①未命名文件.png

注解图②未命名文件-2.png

分类与主类加载情况

根据前面几篇文章分析,类的加载与load方法有关,那么分类的加载与load是否有关系呢?我们可以分以下4种方式进行探索:

  1. 类和分类都实现load方法。
  2. 类实现load,分类不实现load方法。
  3. 类不实现load方法,分类实现load
  4. 类和分类都不实现load方法。 同时,我们在源码里面添加 LGPerson 和 这个类的分类LGPerson+LGA,在通过断点调试.

类和分类都有load方法

主类和分类的load方法都实现,然后再断点调试,如下图:

1DF4B0F1-0233-4930-8B6A-F49AEB46100D.png

通过打印的方法信息和左侧堆栈的调用情况,我们可以梳理出一个程序执行的线路出来:

_read_images(非懒加载类) --> realizeClassWithoutSwift --> methodizeClass -> attachToClass --> load_categories_nolock --> attachCategories

类有load,分类没有load方法。

主类的load方法实现,而分类 load 方法注释掉,然后再断点调试,如下图:

28ABD978-B914-4D17-89D3-848944D7EAA2.png

通过打印的方法信息情况,我们可以梳理出一个程序执行的线路出来:

_read_images(非懒加载类) --> realizeClassWithoutSwift --> methodizeClass -> attachToClass

注意哦,此时,是没有执行 attachCategories 方法的。

类没有load,分类有load方法。

主类的load方法注释掉,而分类 load 方法打开,然后再断点调试,如下图:

28ABD978-B914-4D17-89D3-848944D7EAA2.png

通过打印的方法信息情况,我们可以梳理出一个程序执行的线路出来(和 类有load,分类没有load方法 这一种是一样的):

_read_images(非懒加载类) --> realizeClassWithoutSwift --> methodizeClass -> attachToClass

也是没有执行 attachCategories 方法的。

类和分类都不实现load方法。

主类和分类的load方法都注释掉,然后再断点调试,如下图:

FC8F8EC7-98EE-4B60-854D-782B06237B14.png

发现直接就到了 main 函数里面,上面说到的这些方法都没实现。

那么他们这几种情况,具体发什么了什么,我们就需要进行详细的数据跟踪了。

分类加载流程跟踪(单个分类)

分别在主类 LGPerson 和分类 LGPerson (LGA) 里面添加多个方法:

  • 主类: FF40D868-4149-4CE4-87FF-27FE437C7134.png

  • 分类: BDC13802-D66D-4D02-ACE7-809CB58205CB.png

  • 条件是:类和分类都有load方法 首先,我们得确定在实现类的时候,分类是否已经道到 ro 里面了。如果已经有了数据,那么就说明分类加载是在类的实现之前就已经完成了。那么我们可以在 realizeClassWithoutSwift 函数里面,打印 ro 的信息,如下图:

69ADE374-9276-41C1-B545-8780AFC0338A.png

我们可以查看到类里面的方法数组里面,目前只有 10 个方法,我们再把这个 10 方法打印出来,看里面有没有分类的方法存在:

C420AE11-8579-41B1-BFFB-592EDD9E55CF.png DE76D3CA-A5B4-440F-8828-8828D20C78AC.png

可以看到 10 个方法全部是主类的方法,那么说明此时分类的方法还没有加载过来。

接着再根据上文探寻到的路径执行,到 attachCategories 函数,通过 lldb 调试,获取里面的内存信息,如下图:

25DBD375-3B82-4D37-88EB-40D757112552.png

mlist 是一个地址,而 mlists 里面存的是地址数组,共 64 个元素。

  • ATTACH_BUFSIZ的值是64mcount初始值为0mlists共有64个位置。

  • 这里将mlist赋值到mlists64 - 1的位置,并对mcount进行+1操作。

接着往下走:

C35DB163-CE70-4EDF-86D9-421B4ECBEE7B.png

到了这里,就印证了文章开始所讲的 attachLists 函数。其传入进去的参数是 ** 类型。

  • 通过prepareMethodListsmlists中的方法进行排序,这里mcount1,取的是mlists的第64 - 1的位置的值。

  • 接下来进入attachLists函数,传入mlists + ATTACH_BUFSIZ - mcountmethod_list_t **类型,传入mcount1

进入到attachLists 函数里面:

F136DA57-53C9-4695-A906-35EB23D2DA65.png

这个listcount3,存的是我们主类的3个方法。再接着往下分析:

71B2A65B-9BAD-4915-87BA-8EEF1555B85F.png

到了这里,就验证了我们对attachLists 函数的源码分析,开辟出的新数组,把旧数组作为指针存入到新数组里面,而这个指针是指向一个数组。传入进来的分类(load_categories_nolock 里面的cat)是以元素循环存入新数组。

多个分类加载

未命名文件-3.png

load_categories_nolock函数的跟踪

for循环中:

A5C88DCD-3928-497B-ABC1-B4C939EBDB07.png

循环添加分类:

A9DD3622-4F27-4954-AD48-E5F143660072.png

此时,i = 1count = 4,分类是LGA_Two,接下来就进入attachCategories函数。

attachCategories函数的跟踪

FD663B90-310D-40DD-9E10-6D3D86B71908.png

  • 根据 lldb 调试,传入的mlists + ATTACH_BUFSIZ - mcount依然是method_list_t **类型,也就是method_list_t指针地址类型。

attachLists函数的跟踪

CE051821-2B18-43A1-9FFC-3C37C3FC3438.png

  • oldCount = 2,根据之前的分析我们可以知道,它们是主类的method_list_t *和第一个分类的method_list_t *

  • newCount = 3,用这个大小开辟了一个新的newArray

  • for循环,取array()->lists的值,也就是old的值,从后向前存储到newArray

刚刚把主类和分类都有 load 方法的情况给分析完了,那么现在就还剩下他的几种情况,接下来,继续往下探究。

五种情况 ro-rw 数据的加载

目前程序里面,只有 Person (LGA) 一个分类。

分类有 load 方法,主类没有

运行程序,断点在 _read_images 函数里面断住了,在主类里面,没有设置 load 方法,但是一样在这里断住了:

DE97DC21-AF84-40BA-A3B1-181FE6033124.png

说明了,虽然在主类里面没有实现load方法,但是同样是非懒加载。因为分类是主类的,既然分类都执行了load方法,那么主类,也会 被迫 执行。

然后再进入到 realizeClassWithoutSwift 函数里面,打印 ro 的信息:

E6E46200-13E5-461A-B4E8-355B52F968EC.png

我们可以看到数组里面是有 13 个方法的,根据前面打印主类的方法数组,可以知道在主类里面有 10 个方法,但是现在有 13 个方法,那就说是把分类的方法,也加进去了。而我们所有的数据,都是从 dyld 过程里面获取的,这就说明,在整个加载过程中,已经把分类加载进去了,和主类杂合在一起。那么分类的数据,就从 data() 里面获取。

主类有 load 方法 ,分类没有 

和上面步骤一样,经过 lldb 调试,在realizeClassWithoutSwift中打印的结果,和上面的这个情况是一样的。方法已经存在了,说明是通过data()加载的,已经被编译器处理了。

分类和主类都没有 load 方法

此时主类和分类都没有实现 load 方法。运行工程:

784CD6F8-56C5-4462-A0DD-CD432CF8B8B4.png

根据运行的结果,就会发现,直接就到了类的初始化上面来了,也就是说,根本就没有走我们之前几次情况时,所调用的路线。我们再跟着断点往下走:

EABF1F68-1EB4-43FE-9647-4B8FAC582DE4.png

通过这个工程左边的堆栈信息,就很明确了,此时是懒加载执行的,进行了消息发送。再看此时的 lldb 打印 ro 的信息,和非懒加载的数据是一样的。

F97FB006-D715-41B8-B657-1970F0358E4A.png

这就说明了,当主类和分类都没有 load 方法时,就会推迟到第一次消息的时候,进行初始化,这个时候,无论是主类,还是分类,他们加载的数据都是从 data() 里面进行获取的。

多个分类不全有 load 方法  +  主类有 load 方法

再增加 3 个分类,分别是 LGPerson+LGA_OneLGPerson+LGA_TwoLGPerson+LGA_Three,加上原先的分类,就 4 个分类。那么再运行代码,来到 load_categories_nolock 方法,就可以打印出分类加载的数量 :

5C681C5E-F36C-4C45-A33B-1168DA01D2D1.png

得到的分类数,就是 4 。也就是说,经过非懒加载进行实现的,也是从 machO 里面加载的数据。

  • 就和我们前面分析的几种情况一样,数据是从 data() 里面获取,但是 data() 的数据,却是从 machO 里面来的。这也就是我们为什么说 load 方法的调用,是一个耗时的操作。结合我们上面的分析,调用 load 方法,就会执行非懒加载,在 load_categories_nolockattachCategories 两个函数里面,执行加多的算法,这样就会出现大的耗时操作。所以,load 方法的使用,要慎重。

methodList -> 数据结构

attachCategories函数里面,由于分类是由 rwe 存储的, rwe->methods.attachLists(mlists, mcount)这种加载分类的方式,attachLists函数中是方法列表加载的核心代码。而mlistsmethod_list_t类型,通过 lldb 打印 :

A749CC1F-6506-42C3-9EB8-1232BBC764F2.png

  • 通过源码和打印分析,由源码,从分类中获取的方法列表是method_list_t,通过打印,由get方法获取,取出来的是method_t类型。我们来分别看下method_list_tmethod_t的源码:

  • method_list_t源码:

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {

    bool isUniqued() const;
    bool isFixedUp() const;
    void setFixedUp();
    
    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        ASSERT(i < count);
        return i;
    }
    
    bool isSmallList() const {
        return flags() & method_t::smallMethodListFlag;
    }
    
    bool isExpectedSize() const {
        if (isSmallList())
            return entsize() == method_t::smallSize;
        else
            return entsize() == method_t::bigSize;
    }
    
    method_list_t *duplicate() const {
        method_list_t *dup;
        if (isSmallList()) {
            dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
            dup->entsizeAndFlags = method_t::bigSize;
        } else {
            dup = (method_list_t *)calloc(this->byteSize(), 1);
            dup->entsizeAndFlags = this->entsizeAndFlags;
        }
        dup->count = this->count;
        std::copy(begin(), end(), dup->begin());
        return dup;
    }
};
  • method_t源码:
struct method_t {

    static const uint32_t smallMethodListFlag = 0x80000000;
    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.

    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
。。。。。 代码省略。。。。。

根据源码:

  • method_list_t 是继承于 entsize_list_tt,在entsize_list_tt的实现里面,就可以知道通过 getOrEnd 进行返回数据,对应着就是在打印里面的 get
  • getOrEnd 里面,
Element& getOrEnd(uint32_t i) const { 
        ASSERT(i <= count);
        return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
    }

    Element& get(uint32_t i) const { 
        ASSERT(i < count);
        return getOrEnd(i);
    }
  1. 先是从结构体头部,平移到结构体尾部。
  2. 通过sizeof(*this)i * entsize()计算出方法位置的大小,再平移,就获取到相应的方法了。
  3. 最后通过 (Element *) 强转出来。

在进行验证下:

50E88548-D267-43E6-AB02-355CD22EB378.png

通过 lldb 打印,当一开始就进行读取时,method_list_t的地址是0x00000001000045c0,再看 method_list_t 的大小 8,然后再通过 get 拿到第 0 个位置的地址,是0x00000001000045c8,对比刚开始的位置,是平移了 8 ,那就完全对上了。

  • 也间接验证了,在 method_list_t 里面,存的都是指针地址。当然,如果分类里面,什么都没实现的话,也不会被加载进去。

多个分类加载,需不需要排序

通过methods.attachLists进行加载的类,在调用时是否进行排序了呢,在 iOS底层探究-----慢速查找流程 中,方法的查找会通过----二分查找,我们要讲的多个分类加载,需不需要排序问题,就得从这里开始。

此时,我们在源码中设置两个分类,分别是feLGPerson+LGALGPerson+LGA_One。其实现 saySomething方法。然后在 main 里面调用,如下:

022D254F-062B-40CF-BD11-1F3B8CEC502F.png

当进行非懒加载之后,调用 saySomething方法,进入方法查找流程,就会执行到 getMethodNoSuper_nolock 方法。如下图:

6EF7FF1A-4016-4653-8F08-57C4CBBA4DD9.png

根据上面的 attachLists 数据追踪,我们当时只有一个分类,所以得到的 count = 3,现在我们有两个分类,所以得到的 count = 4。所以情形是一样的。也就说明了,方法列表并没有进行排序。

然而执行 getMethodNoSuper_nolock --> search_method_list_inline --> findMethodInSortedMethodList,在findMethodInSortedMethodList

9AEFC4E4-3A78-4293-83A2-9FC3E1424D47.png

看源码,当keyValue == probeValue时,会有一个while的死循环向前查找,这明明又是说明对方法列表进行了组合排序,这是为什么呢。

结合前面的分析,因为分类的加载,除了attachCategories的方式,还有一种data()加载的方式,data()加载的方法列表是一个把本类和分类方法杂合一起的列表,并且是排好序的。

class_ro_t 数据加载

根据前面的分析,ro的数据加载,是编译阶段就完成了,所以我们用llvm源码来进行分析。

79A4D90C-7465-4610-ABB2-A1183CA7D6B6.png

对比objc中的class_ro_t的实现,可以看出是和llvm中的class_ro_t是一样的,只是llvm中多了m_的前缀。

编译器的读取,是在 dyld 链接的时候,read_imagesload_images的时候,此时的整个 main 函数是还没运行起来的,ro 已经获得了数据,所以是在编译时完成的。

我们注意在结构体的结尾,是函数Read,这个函数中应该就是从MachO中读取了数据,接下来继续分析,进入到Read里面去。

我们可以索引class_ro_t::Read来定位,如下图:

f231241c144b44dea3b9221cb477a0ff~tplv-k3u1fbpfcp-watermark.image.png

看这个源码,和我们在开发中,进行模型赋值是差不多的,相当于序列化。

我们对Read函数的实现基本了解了,再看看调用的情况,全局搜索ro->Read(

3010360E-E987-498E-8ED4-C18F544F0F70.png

总结

  • 懒加载类+懒加载分类:消息第⼀次调⽤(加载数据)
  • ⾮懒加载类+懒加载分类:read_image 就加载数据

这两种情况,在编译时期,就完成了 data()