在上一篇文章中探索过iOS类的加载,也探索了部分分类的加载;那么本篇文章将继续接着继续探索分类的加载。
在上一篇文章中,我们在attachCategories
方法中探究到了在分类进行添加之后,才会对rwe
进行赋值和初始化的,那么我们就从rwe
开始赋值的操作继续探索。
在探索过程中,我们需要带着问题去探索,第一个问题就是分类时如何加载到类中去的?
首先看下图,是attachCategories
的一部分代码,在rwe
初始化之后,rwe就有值了,那么在attachLists
是怎么把方法添加到里面去的呢?
下图是attachLists
方法,它分三种情况:
第一种情况是第二方框,当addedLists
来了之后,将addedLists
的第一个元素赋值给list,也就是说,当list
不存在时,就会从0-1;
第二种情况是第三个方框的代码,当有list之后,加入很多个list,总的意思就是将新加入的放在前面,旧的数据放在后面。它这样的实现是一种算法思维,LRU
算法,也就是说当分类和主类中的方法存在一样时,就加载主类的方法。
第三种情况是加载多个分类方法,它的功能是先扩容,然后将后加入的分类放在前面。
下面我们去通过代码去验证一下:
首先我们根据rwe
的初始化代码去研究,在代码中添加我们对自己的类的判断:
在attachCategories
方法中的auto rwe = cls->data()->extAllocIfNeeded();
行代码去查看extAllocIfNeeded()
方法中的extAlloc
:
当程序进入到这个判断中,就证明是我们自己添加的类;下面我们来看一下list
的变化:
在刚开始的时候,list
是空的,没有任何值;
(lldb) p rwe
(class_rw_ext_t *) $0 = 0x0000000101130a10
(lldb) p *$0
(class_rw_ext_t) $1 = {
ro = 0x0000000000000000
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
demangledName = 0x0000000000000000
version = 0
}
而当我们执行完method_list_t *list = ro->baseMethods();
行代码后,再去获取list
的值:
(lldb) p list
(method_list_t *) $2 = 0x00000001000081c8
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 8
first = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003aa0 (KCObjc`-[Person kc_instanceMethod1])
}
}
}
(lldb)
可以看到它现在加载的是本类的rw
;
下面继续进入到attachLists
中去看看是如何进行加载的:
当程序进入attachLists
方法,它首先执行第一步的方法,将list
赋值为addedLists的第1个元素;
下面我们来看看他的值是什么:
(lldb) p addedLists
(method_list_t *const *)【指针地址】 $4 = 0x00007ffeefbf63f8
(lldb) p addedLists[1]
(method_list_t *const) $5 = 0x00000001003419dd
(lldb) p 0x00000001003419dd
(long) $6 = 4298381789
(lldb) p *$5
(method_list_t) $7 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 1936876880
count = 620785263
first = {
name = <no value available>
types = 0xa0e781a6e89188e6 ""
imp = 0x20849ae7b6a9e794 (0x20849ae7b6a9e794)
}
}
}
(lldb) p addedLists[0]
(method_list_t *const) $8 = 0x00000001000081c8
(lldb) p *$8
(method_list_t) $9 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 8
first = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003aa0 (KCObjc`-[Person kc_instanceMethod1])
}
}
}
将addedLists
的第0个,第1个都打印了一下,都有值,因为地址是连续的,第一个可能是别人的内容。那么本类的方法就加载完毕,现在去看看分类的加载:
继续执行方法,回道attachCategories
方法,在程序断点执行到下图时:
我们去看一下mlist
的值(这个如果执行的方法不是这个,可以清除缓存重新试一下),可以看到是分类的方法:
(lldb) p mlist
(method_list_t *) $0 = 0x0000000100008038
(lldb) p *$0
(method_list_t) $1 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 24
count = 4
first = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003970 (KCObjc`-[Person(LGA) kc_instanceMethod1])
}
}
}
(lldb)
继续执行程序,看下图,首先经过排序,然后指针内存平移,获得最后一个元素:
(lldb) p mlists
(method_list_t *[64]) $2 = {
[0] = 0xffffffffffffffff
[1] = 0x0000000000000000
[2] = 0x00007ffeefbf6a20
[3] = 0x00007fff68c52ad3
[4] = 0x00007fff86a22be0
[5] = 0x00007ffeefbf6b10
[6] = 0x00007ffeefbf69e0
[7] = 0x000000010030f66e
[8] = 0x000000010034ce08
[9] = 0x000000010034ce08
[10] = 0x00007fff86a22be0
[11] = 0x00007ffeefbf6a8f
[12] = 0x00007ffeefbf69b0
[13] = 0x0000000100310a70
[14] = 0x00007ffeefbf6af8
[15] = 0x00007ffeefbf6a8f
[16] = 0x00007ffeefbf6a90
[17] = 0x00007ffeefbf6af8
[18] = 0x00007ffeefbf69e0
[19] = 0x0000000100310a25
[20] = 0x01007ffeefbf6a00
[21] = 0x00007ffeefbf6a8f
[22] = 0x00007ffeefbf6a90
[23] = 0x00007ffeefbf6af8
[24] = 0x0000000000000000
[25] = 0x00007fff8f2bce28
[26] = 0x0000000000000000
[27] = 0x00000001003432a2
[28] = 0x00007ffeefbf6a20
[29] = 0x00007fff68c22ad7
[30] = 0x00007fff8f2bce28
[31] = 0x0000000000000044
[32] = 0x00007ffeefbf6a50
[33] = 0x00007fff68c2b2bd
[34] = 0x00000001000b0e68
[35] = 0x00019ca683cec01a
[36] = 0x00000001000b0d28
[37] = 0x00000001002d77b0
[38] = 0x00007ffeefbf6b30
[39] = 0x00007fff68c2941e
[40] = 0x00000001000dedc0
[41] = 0x00000001003432db
[42] = 0x00000001003419dd
[43] = 0x0000000000000000
[44] = 0x0000000000000000
[45] = 0x0000000000000002
[46] = 0x00007ffeefbf6ab0
[47] = 0x00000001005b12d5
[48] = 0x00000001005af8e0
[49] = 0x00000001005af8e0
[50] = 0x00007ffeefbf6be0
[51] = 0x00000001000038f0
[52] = 0x00007ffeefbf6ae0
[53] = 0x0000000100008410
[54] = 0x00007ffeefbf6b00
[55] = 0x0000000100008430
[56] = 0x00007ffeefbf6b00
[57] = 0x00000001002e42ae
[58] = 0x00000001000083e8
[59] = 0x0000000100008410
[60] = 0x00007ffeefbf6b30
[61] = 0x00000001002e4412
[62] = 0x0000003000000018
[63] = 0x0000000100008038
}
(lldb) p mlists + ATTACH_BUFSIZ - mcount
(method_list_t **) $3 = 0x00007ffeefbf6b18
(lldb) p *$3
(method_list_t *) $4 = 0x0000000100008038
(lldb)
当继续执行,又开始跑回attachLists
方法,然后执行第二部操作,其中的hasArray
原本是私有属性,经过更改,变为public
:
而当我们继续执行,还是会重复第二次的操作,接下来就进入了第三种方式的加载,开始是LGA
分类的加载,然后是LGB
分类的加载:
list_array_tt
的结构
执行第三部分代码:
(lldb) p array()
(list_array_tt<method_t, method_list_t>::array_t *) $14 = 0x0000000100640730
(lldb) p $14.lists[0]
(method_list_t *) $15 = 0x0000000100008300
Fix-it applied, fixed expression was:
$14->lists[0]
(lldb) p *$15
(method_list_t) $16 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003be0 (KCObjc`-[Person(LGB) kc_instanceMethod1])
}
}
}
(lldb)
打印得到的是LGB
的分类方法,最后完整执行程序,最终打印的方法也是我们在上面验证的方法:
那么关于如何加载分类,已经得到了答案,那么是在什么时机加载的呢?
下面带着第二个问题去探索:
看看完整的执行整个过程(中间加了很多自己写的判断类的代码,有很多打印输出),其中主类和两个分类都添加了load
方法:
readClass: 这个是我要研究的 Person
_getObjc2NonlazyClassList: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
methodizeClass: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
attachToClass: 这个是我要研究的 Person
load_categories_nolock:operator(): 这个是我要研究的 Person
extAlloc:这是我要研究的 Person
attachCategories: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
extAlloc:这是我要研究的 Person
load_categories_nolock:operator(): 这个是我要研究的 Person
attachCategories: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
prepare_load_methods: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
prepare_load_methods: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
2020-10-17 15:13:22.830386+0800 KCObjc[40272:4663341] -[Person(LGB) kc_instanceMethod1]
2020-10-17 15:13:22.831749+0800 KCObjc[40272:4663341] 0x10115be80
从上面的所有方法的打印过程中,有很多方法都是探索过的,通过上帝视角,得到第二个问题的探索入口在load_categories_nolock
方法当中;
而经过搜索load_categories_nolock
后,只有两处调用了此方法,一个在_read_images
中,另一个在loadAllCategories
方法中,而经过验证,_read_images
方法中的load_categories_nolock
并不会执行,那么真正执行load_categories_nolock
的方法在loadAllCategories
中。
那么在之前的探索,可以得知非懒加载类在Load
的情况下才存在,那么如果主类中加入Load
方法,分类只弄一个有Load
方法,去验证一下分类是否会加载;
在经过测试后,得到一个结论:只要有一个分类是非懒加载类,那么所有分类方法都会是非懒加载类,运行时会将非懒加载类添加进去,它的数据全部由load_image
加载到数据。
那么就有一个很有意思的问题,当主类没Load
方法,分类有或分类没Load
,主类有,那又会有哪些情况呢?
下面去探索一下:
首先在分类中将Load
方法注释,主类保留:
先完整执行程序,看打印输出的结果
readClass: 这个是我要研究的 Person
_getObjc2NonlazyClassList: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
methodizeClass: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
attachToClass: 这个是我要研究的 Person
2020-10-17 16:03:15.569478+0800 KCObjc[40649:4689679] -[Person(LGB) kc_instanceMethod1]
2020-10-17 16:03:15.570557+0800 KCObjc[40649:4689679] 0x10072a1f0
Program ended with exit code: 0
可以看到程序会执行realizeClassWithoutSwift
方法,我们以此方法为研究对象
在realizeClassWithoutSwift
方法中打上断点,去看看ro
的值:
(lldb) p kc_ro
(const class_ro_t *) $0 = 0x0000000100008240
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 388
instanceStart = 8
instanceSize = 24
reserved = 0
ivarLayout = 0x0000000100003f6d "\x11"
name = 0x0000000100003f66 "Person"
baseMethodList = 0x0000000100008038
baseProtocols = 0x0000000000000000
ivars = 0x0000000100008288
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000082d0
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1->baseMethodList
(method_list_t *const) $2 = 0x0000000100008038
Fix-it applied, fixed expression was:
$1.baseMethodList
(lldb) p $2
(method_list_t *const) $2 = 0x0000000100008038
(lldb) p $2.get(0)
(method_t) $3 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003be0 (KCObjc`-[Person(LGB) kc_instanceMethod1])
}
Fix-it applied, fixed expression was:
$2->get(0)
(lldb) p $2.get(1)
(method_t) $4 = {
name = "cateB_2"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003c10 (KCObjc`-[Person(LGB) cateB_2])
}
Fix-it applied, fixed expression was:
$2->get(1)
(lldb) p $2.get(2)
(method_t) $5 = {
name = "cateB_1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003c40 (KCObjc`-[Person(LGB) cateB_1])
}
Fix-it applied, fixed expression was:
$2->get(2)
(lldb) p $2.get(3)
(method_t) $6 = {
name = "cateB_3"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003c70 (KCObjc`-[Person(LGB) cateB_3])
}
Fix-it applied, fixed expression was:
$2->get(3)
(lldb) p $2.get(4)
(method_t) $7 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003980 (KCObjc`-[Person(LGA) kc_instanceMethod1])
}
Fix-it applied, fixed expression was:
$2->get(4)
(lldb) p $2.get(5)
(method_t) $8 = {
name = "cateA_2"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x00000001000039b0 (KCObjc`-[Person(LGA) cateA_2])
}
Fix-it applied, fixed expression was:
$2->get(5)
(lldb) p $2.get(6)
(method_t) $9 = {
name = "cateA_1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x00000001000039e0 (KCObjc`-[Person(LGA) cateA_1])
}
Fix-it applied, fixed expression was:
$2->get(6)
(lldb) p $2.get(7)
(method_t) $10 = {
name = "cateA_3"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003a10 (KCObjc`-[Person(LGA) cateA_3])
}
Fix-it applied, fixed expression was:
$2->get(7)
(lldb) p $2.get(8)
(method_t) $11 = {
name = "kc_instanceMethod3"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003a80 (KCObjc`-[Person kc_instanceMethod3])
}
Fix-it applied, fixed expression was:
$2->get(8)
(lldb)
可以看到ro
中的方法列表有Person
,LGA
,LGB
的所有方法,而且加载没有顺序,那么意味着当前的数据在这个过程中,只要没有进行非懒加载,在cls
读取Mach-o
数据的时候,他就已经编译进来了,不需要运行时再添加分类方法进去了。
然后我们去看看排序之后的方法列表是怎样的:
(lldb) p $2.get(0)
(method_t) $12 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003be0 (KCObjc`-[Person(LGB) kc_instanceMethod1])
}
Fix-it applied, fixed expression was:
$2->get(0)
(lldb) p $2.get(1)
(method_t) $13 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003980 (KCObjc`-[Person(LGA) kc_instanceMethod1])
}
Fix-it applied, fixed expression was:
$2->get(1)
(lldb) p $2.get(2)
(method_t) $14 = {
name = "kc_instanceMethod1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003ab0 (KCObjc`-[Person kc_instanceMethod1])
}
Fix-it applied, fixed expression was:
$2->get(2)
(lldb) p $2.get(3)
(method_t) $15 = {
name = "cateA_2"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x00000001000039b0 (KCObjc`-[Person(LGA) cateA_2])
}
Fix-it applied, fixed expression was:
$2->get(3)
(lldb) p $2.get(4)
(method_t) $16 = {
name = "cateA_1"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x00000001000039e0 (KCObjc`-[Person(LGA) cateA_1])
}
Fix-it applied, fixed expression was:
$2->get(4)
(lldb) p $2.get(5)
(method_t) $17 = {
name = "cateA_3"
types = 0x0000000100003f73 "v16@0:8"
imp = 0x0000000100003a10 (KCObjc`-[Person(LGA) cateA_3])
}
Fix-it applied, fixed expression was:
$2->get(5)
(lldb)
这加载的顺序并没有排序,其实他是进行排序了的;至于它是如何实现排序的,就不写过程了,系统首先会根据主类的方法进行排序,在分类的方法中,则会根据方法地址从小到大进行排序。
那么就得到了第二个结论:当主类有Load
,分类没有,那么数据来自于data()
,只需要对同名方法进行处理和修复一下。
那么主类没有Load
,分类也没有Load
,先打印一下整个输出:
readClass: 这个是我要研究的 Person
realizeClassMaybeSwiftMaybeRelock: 这个是我要研究的 Person
realizeClassMaybeSwiftMaybeRelock: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
methodizeClass: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
attachToClass: 这个是我要研究的 Person
2020-10-17 16:29:33.924814+0800 KCObjc[40771:4699799] -[Person(LGB) kc_instanceMethod1]
2020-10-17 16:29:33.925950+0800 KCObjc[40771:4699799] 0x10114c8d0
Program ended with exit code: 0
从打印的结果得知,整个数据的加载,都推迟到了第一次调用消息的时候,那么它是从哪加载数据的呢?那么就需要以readClass
方法入手,经过探索,它同样是从data()
中加载的数据。
最后一种情况,主类没有Load
,分类有Load
:
同样的先打印流程:
readClass: 这个是我要研究的 Person
operator(): 这个是我要研究的 Person
operator(): 这个是我要研究的 Person
prepare_load_methods: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
methodizeClass: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
attachToClass: 这个是我要研究的 Person
extAlloc:这是我要研究的 Person
attachCategories: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
methodizeClass: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
attachToClass: 这个是我要研究的 Person
extAlloc:这是我要研究的 Person
attachCategories: 这个是我要研究的 Person
prepareMethodLists: 这个是我要研究的 Person
prepare_load_methods: 这个是我要研究的 Person
realizeClassWithoutSwift: 这个是我要研究的 Person
2020-10-17 16:37:55.247587+0800 KCObjc[40892:4706600] -[Person(LGB) kc_instanceMethod1]
2020-10-17 16:37:55.249469+0800 KCObjc[40892:4706600] 0x101a38cb0
Program ended with exit code: 0
在所有打印方法中,没有load_categories_nolock
方法的打印,那么先在这个方法处打上断点,然后同样的我们先以readClass
入手:
看下图的count
只有8个,只有主类的方法,没有分类的方法。
继续执行,他就会进入load_categories_nolock
方法,通过bt
打印堆栈信息:
可以看到,它就经过loadAllCategories
方法,而调用此方法的函数是load_images
方法。这跟第一种方试差不太多;按原理来说,主类没有Load
方法,那么它会在第一次消息的时候加载,而实际上是主类没有Load
,分类有,会迫使主类提前加载。