分类&类扩展的实现原理

341 阅读6分钟

前言

之前的学习已经大致了解了类的加载,但是对于分类,是有特殊的处理的,也就是所谓的attachCategories,那么这边分类的加载到底是怎么样呢?本节主要就是探讨分类的源码实现结构。

Ro Rw Rwe

在我之前的博客# 元类的底层探索中已经详细写了关于这三者的区别,Ro是存在于沙盒文件中,Rw(脏内存)是从Ro(干净内存)中拷贝出来,在类第一次被使用的时候,就产生了。沙盒中的数据是只读的,如果我们要修改,那必然是要拷贝出来存在于Rw中。但是由于Rw在IOS中由于手机的客观因素,区别于Mac,内存是十分紧张的,所以就由此产生了Rwe,也就是从Rw的扩展。优化这部分Rw,有些数据是不会变的,没必要全部作为可修改的数据存储。Rwe的存储容量是在运行时被额外分配的,比如说分类,并不是每个类都会有分类,所以苹果在这一块做了相当多的优化,我们把不会变的东西放在Ro中,把可读写的放到Rwe中。

如何赋值的Rw

系统是通过地址(从mach0中获取到的)指针的平移获取到相应的地址,从而可以从这个地址中获取值(通过它指定的类型强转获取)。

A285F58A-D598-4D3A-8E55-4CFBB8B1D862.png

        class_rw_t *data() **const** {

        **return** bits.data();

    }
        class_rw_t* data() **const** {

        **return** (class_rw_t *)(bits & FAST_DATA_MASK);

    }

如何赋值的Rwe

前面已经知道rwe是从attachCategoriesextAllocIfNeeded方法中获取的。但是里面具体做了什么还没有做了解。那么全局搜索extAllocIfNeeded,就可以知道系统是什么时候对rwe做了修改,搜索结果是addMethods_finishclass_addProtocol_class_addPropertyobjc_duplicateClass 差不多就是添加方法协议属性的时候会赋值。

那么分类在何时加载呢??

反推!!

全局搜索attachCategories,有attachToClassload_categories_nolock,证明是这两个地方会调起来分类的加载,继续搜索attachToClass。

0B7CE5BF-DA58-4034-B662-ACE672569987.png

通过previously判断分别走的方法,追踪previously,发现都是nil,这个参数也就是备用参数。attachToClass是在methodizeClass方法中调用的。

graph TD
realizeClassWithoutSwift --> methodizeClass -->attachToClass-->attachCategories

attachCategories &

随着分支的增加,必然需要研究末尾我们需要研究的这个对象它的实现方式是什么?有没有必要深入研究它,通过它的实现方法,里面通过attachLists方法动态添加。

57FB0DCA-AE16-4E52-BE9D-75C09E95C61D.png

截屏2021-07-18 下午9.44.18.png

这里只单纯的添加一个方法

**else** **if** (!list  &&  addedCount == 1) {

            // 0 lists -> 1 list

            list = addedLists[0];

            validate();

        }

先开辟老+新的容量大小,然后把老的往末尾插,新的往前插入。

// 1 list -> many lists

            Ptr<List> oldList = list;

            uint32_t oldCount = oldList ? 1 : 0;

            uint32_t newCount = oldCount + addedCount;

            setArray((array_t *)malloc(array_t::byteSize(newCount)));

            array()->count = newCount;

            **if** (oldList) array()->lists[addedCount] = oldList;

            **for** (**unsigned** i = 0; i < addedCount; i++)

                array()->lists[i] = addedLists[i];

            validate();

如果已经存在数组了,其他数组进来,新开辟一个空间数组,把旧的数组倒序插入,最后把新的插到前面。

**if** (hasArray()) {

            // many lists -> many lists

            uint32_t oldCount = array()->count;

            uint32_t newCount = oldCount + addedCount;

            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));

            newArray->count = newCount;

            array()->count = newCount;

\


            **for** (**int** i = oldCount - 1; i >= 0; i--)

                newArray->lists[i + addedCount] = array()->lists[i];

            **for** (**unsigned** i = 0; i < addedCount; i++)

                newArray->lists[i] = addedLists[i];

            free(array());

            setArray(newArray);

            validate();

        }

类 & 分类 的加载

之前已经知道,类是懒加载的模式,如果不写load方法,那么就不会走read_images方法,那么分类是否也存在这种情况?类和分类的加载有什么不同呢?

加载方法次序

总体走向

1.分类和类都存在load方法:

_read_images - > realizeClassWithoutSwift - > mothodizeClass - >load_categories_lock - > attachCategories

2.分类有load,主类没有load:打印方法,都存在(来自data()

_read_images - > realizeClassWithoutSwift - >mothodizeClass - > attachToClass - > 没走attachCategories

3.主类有load,分类没有load:打印方法,都存在来自data()

_read_images - > realizeClassWithoutSwift - >mothodizeClass - > attachToClass - > 没走attachCategories

4.都没有load方法:

->main (推迟到第一次消息发送,初始化->data())

5.有很多分类,但分类不一全都有load方法:

方法:为主类写一个分类,分类里写好对象方法类方法,打断点跟踪打印方法中是否有分类方法。

流程图

未命名流程图.png

问题补充

分类的加载是否需要排序?

不需要。 参考之前的分析,分类的加载一共分为四种情况,后三种情况都是通过ro读取,在ro读取时,也就是系统判定,在二分查找时,会进行循环遍历,也就是不停地”--“找到最前面的分类的方法,此时的方法列表里存的是所有的分类的方法,也没有次序,并且是一维数组,所以可以”--“,但是第一种情况也就是主类分类都有load,这时他会走attachCategories,这时候方法里已经实现了对分类方法的插入(二维数组)此时取得时候只要取第一个即可。

补充控制编译分类的顺序

在给一个主类写了好几个分类,并且都实现了分类的同名方法,既然分类是无序的,那么取哪个分类的方法就取决于编译顺序,编译顺序如何而来?在第一次你创建的时候就决定了,如果想改,可以通过buildPhases里的compile Sources调整顺序,越往下的越靠前。

为什么可以通过地址类型强转读取到ro的数据?

因为在llvm编译器编译阶段,编译器已经帮我们把值根据地址赋值好了。

截屏2021-07-21 下午4.53.44.png

method_list_t数据结构?

存储的是指针类型的数组,通过get获取到对应的指针地址指向method_t,method_t里面存储了方法以及方法相关的数据。

截屏2021-07-21 下午4.56.24.png

截屏2021-07-21 下午4.57.28.png

主类没有load,分类有load方法的特别情况

如果只有一个分类,那么按照之前的分析,他是不会走attachCategories,但是如果有多个分类,并且至少有两个分类实现了load,也会走attachCategories方法,很奇怪了,如果主类没有实现load方法。 通过打断点调试,主类跟分类都实现了load方法,会走loadAllCategories方法,如果主类没有实现load,只有一个分类实现了load方法,啥也不会走,如果有两个分类实现了,会走prepare_load_methods,然后调起realizeClassWithoutSwift使主类被迫营业,这种情况就跟我们第一种情况一样,会走attachCategories方法了。。另外,如果新建了一个分类没有做任何操作,是不计入总的分类数量的,在cats_count中只计入已经有实现方法的分类(即使没有load方法也可以)。

截屏2021-07-21 下午5.26.32.png

后记

分类跟主类都是采用的懒加载方式,只要有一方写了load方法,就会走非懒加载的流程,进行方法的写入,写得多,走得多!倘若都不写,那么这些方法将在第一次消息发送的时候由系统通过macho直接放到data中,不需要我们操作,所以非必要情况不要加load方法!

暂且写这么多~~后面再补充吧~补课太心累了,后面终于圆回来看懂了。