前言
之前的学习已经大致了解了类的加载,但是对于分类,是有特殊的处理的,也就是所谓的attachCategories,那么这边分类的加载到底是怎么样呢?本节主要就是探讨分类的源码实现结构。
Ro Rw Rwe
在我之前的博客# 元类的底层探索中已经详细写了关于这三者的区别,Ro
是存在于沙盒文件
中,Rw
(脏内存)是从Ro(干净内存)中拷贝出来,在类第一次被使用的时候,就产生了。沙盒中的数据是只读
的,如果我们要修改,那必然是要拷贝
出来存在于Rw中。但是由于Rw在IOS中由于手机的客观因素,区别于Mac,内存是十分紧张的,所以就由此产生了Rwe
,也就是从Rw的扩展。优化这部分Rw,有些数据是不会变的,没必要全部作为可修改的数据存储。Rwe
的存储容量是在运行时
被额外分配的,比如说分类,并不是每个类都会有分类,所以苹果在这一块做了相当多的优化,我们把不会变的东西放在Ro中,把可读写的放到Rwe中。
如何赋值的Rw
系统是通过地址(从mach0中获取到的)指针的平移获取到相应的地址,从而可以从这个地址中获取值(通过它指定的类型强转获取)。
class_rw_t *data() **const** {
**return** bits.data();
}
class_rw_t* data() **const** {
**return** (class_rw_t *)(bits & FAST_DATA_MASK);
}
如何赋值的Rwe
前面已经知道rwe是从attachCategories
的extAllocIfNeeded
方法中获取的。但是里面具体做了什么还没有做了解。那么全局搜索extAllocIfNeeded,就可以知道系统是什么时候对rwe做了修改,搜索结果是addMethods_finish
,class_addProtocol
,_class_addProperty
,objc_duplicateClass
差不多就是添加方法协议属性的时候会赋值。
那么分类在何时加载呢??
反推!!
全局搜索attachCategories
,有attachToClass
和load_categories_nolock
,证明是这两个地方会调起来分类的加载,继续搜索attachToClass。
通过previously
判断分别走的方法,追踪previously,发现都是nil,这个参数也就是备用参数。attachToClass是在methodizeClass
方法中调用的。
graph TD
realizeClassWithoutSwift --> methodizeClass -->attachToClass-->attachCategories
attachCategories &
随着分支的增加,必然需要研究末尾我们需要研究的这个对象它的实现方式是什么?有没有必要深入研究它,通过它的实现方法,里面通过attachLists方法动态添加。
这里只单纯的添加一个方法
**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方法:
方法
:为主类写一个分类,分类里写好对象方法类方法,打断点跟踪打印方法中是否有分类方法。
流程图
问题补充
分类的加载是否需要排序?
不需要。 参考之前的分析,分类的加载一共分为四种情况,后三种情况都是通过ro读取,在ro读取时,也就是系统判定,在二分查找时,会进行循环遍历,也就是不停地”--“
找到最前面的分类的方法,此时的方法列表里存的是所有的分类的方法,也没有次序,并且是一维数组,所以可以”--“
,但是第一种情况也就是主类分类都有load,这时他会走attachCategories,这时候方法里已经实现了对分类方法的插入(二维数组)此时取得时候只要取第一个即可。
补充控制编译分类的顺序
在给一个主类写了好几个分类,并且都实现了分类的同名方法,既然分类是无序的,那么取哪个分类的方法就取决于编译顺序,编译顺序如何而来?在第一次你创建的时候就决定了,如果想改,可以通过buildPhases里的compile Sources调整顺序,越往下的越靠前。
为什么可以通过地址类型强转读取到ro的数据?
因为在llvm编译器编译阶段,编译器已经帮我们把值根据地址赋值好了。
method_list_t数据结构?
存储的是指针类型的数组,通过get获取到对应的指针地址指向method_t,method_t里面存储了方法以及方法相关的数据。
主类没有load,分类有load方法的特别情况
如果只有一个分类,那么按照之前的分析,他是不会走attachCategories
,但是如果有多个分类,并且至少有两个分类实现了load,也会走attachCategories
方法,很奇怪了,如果主类没有实现load方法。
通过打断点调试,主类跟分类都实现了load方法,会走loadAllCategories
方法,如果主类没有实现load,只有一个分类实现了load方法,啥也不会走,如果有两个分类实现了,会走prepare_load_methods
,然后调起realizeClassWithoutSwift
使主类被迫营业,这种情况就跟我们第一种情况一样,会走attachCategories
方法了。。另外,如果新建了一个分类没有做任何操作,是不计入总的分类数量的,在cats_count中只计入已经有实现方法的分类(即使没有load方法也可以)。
后记
分类跟主类都是采用的懒加载方式,只要有一方写了load方法,就会走非懒加载的流程,进行方法的写入,写得多,走得多!倘若都不写,那么这些方法将在第一次消息发送的时候由系统通过macho直接放到data中,不需要
我们操作,所以非必要情况不要加load
方法!
暂且写这么多~~后面再补充吧~补课太心累了,后面终于圆回来看懂了。