0%

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

本文我们研究分类的加载流程。

分类的本质

在研究对象的本质的时候,我们都用clang命令将main.m转换成main.cpp文件查看其本质,分类也不例外,我们使用相同的方法分析。

首先我们定义一个JSPerson的分类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface JSPerson (TestLoad)

- (void)sayCategory;

@end
@implementation JSPerson (TestLoad)

- (void)sayCategory{
NSLog(@"JSPersonCategpry say : %s",__func__);
}

+ (void)load{
NSLog(@"JSPersonCategpry load");
}
@end

然后在main.m文件中使用:

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *p = [JSPerson alloc];
[p sayCategory];
NSLog(@"Hello, World!");
}
}

我们执行:

1
clang -rewrite-objc main.m -o main.cpp

我们打开main.cpp文件,找到分类相关的代码。

1
2
3
4
5
6
7
8
9
10
11
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_JSPerson_$_TestLoad,
};
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;
};

我们从源码里也能搜索到它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);

protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};

可以看到分类的本质是结构体category_t

我们继续回到_read_images函数探索。

rwe的赋值

1
auto rwe = cls->data()->extAllocIfNeeded();

extAllocIfNeeded方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class_rw_ext_t *extAllocIfNeeded() {
auto v = get_ro_or_rwe();
if (fastpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
} else {
return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
}
}
class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
runtimeLock.assertLocked();

auto rwe = objc::zalloc<class_rw_ext_t>();

rwe->version = (ro->flags & RO_META) ? 7 : 0;

method_list_t *list = ro->baseMethods();
if (list) {
if (deepCopy) list = list->duplicate();
rwe->methods.attachLists(&list, 1);
}

// See comments in objc_duplicateClass
// property lists and protocol lists historically
// have not been deep-copied
//
// This is probably wrong and ought to be fixed some day
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rwe->properties.attachLists(&proplist, 1);
}

protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rwe->protocols.attachLists(&protolist, 1);
}

set_ro_or_rwe(rwe, ro);
return rwe;
}

auto rwe = cls->data()->extAllocIfNeeded();是进行rwe的创建,那么为什么要在这里进行rwe的初始化??因为我们现在要做一件事:往本类添加属性、方法、协议等,即对原来的 clean memory要进行处理了

  • 进入extAllocIfNeeded方法的源码实现,判断rwe是否存在,如果存在则直接获取,如果不存在则开辟
  • 进入extAlloc源码实现,即对rwe 0-1的过程,在此过程中,就将本类的data数据加载进去了
  • 其中关键代码是rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);即存入mlists的末尾,mlists的数据来源前面的for循环
  • 在调试运行时,发现category_t中的name编译时是JSPerson(参考clang编译时的那么),运行时是TestLoad即分类的名字
  • 代码mlists[ATTACH_BUFSIZ - ++mcount] = mlist;,经过调试发现此时的mcount等于1,即可以理解为 倒序插入,64的原因是允许容纳64个(最多64个分类)

小结

本类 中 需要添加属性、方法等,所以需要初始化rwe,rwe的初始化主要涉及:分类、addMethod、addProperty、addprotocol , 即对原始类进行修改或者处理时(运行时),才会进行rwe的初始化

attachCategories

通过attachCategories反推分类的加载,我们libobjc源码全局搜索attachCategories的调用,发现有两个地方调用:attachToClassload_categories_nolock

####attachToClass

调用attachToClass的方法只有一个methodizeClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void methodizeClass(Class cls, Class previously)
{
///省略代码
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
// When a class relocates, categories with class methods
// may be registered on the class itself rather than on
// the metaclass. Tell attachToClass to look for those.
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
///省略代码
}

这里有一个previously执行条件,我们依次网上查找调用链,发现previously==nil,previously参数只是方便动态化调试,所以实际调用的只有下面这一处代码:

1
2
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

load_categories_nolock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();

size_t count;
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
locstamped_category_t lc{cat, hi};

if (!cls) {
// Category's target class is missing (probably weak-linked).
// Ignore the category.
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}

// Process this category.
if (cls->isStubClass()) {
// Stub classes are never realized. Stub classes
// don't know their metaclass until they're
// initialized, so we have to add categories with
// class methods or properties to the stub itself.
// methodizeClass() will find them and add them to
// the metaclass as appropriate.
if (cat->instanceMethods ||
cat->protocols ||
cat->instanceProperties ||
cat->classMethods ||
cat->protocols ||
(hasClassProperties && cat->_classProperties))
{
objc::unattachedCategories.addForClass(lc, cls);
}
} else {
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) {
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else {
objc::unattachedCategories.addForClass(lc, cls);
}
}

if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) {
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else {
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};

processCatlist(hi->catlist(&count));
processCatlist(hi->catlist2(&count));
}

全局搜素load_categories_nolock,发现调用load_categories_nolock的地方有两处

  • loadAllCategories
  • _read_images

但是经过调试发现,是不会走_read_images方法中的if流程的,而是走的loadAllCategories方法中的。

attachLists

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

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();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 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();
}

这个函数一共三部分我们分别看:

1
2
3
4
5
else if (!list  &&  addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}

addedLists[0]赋值给list,list是一维数组。

1
2
3
4
5
6
7
8
9
10
11
12
else {
// 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();
}

这个情况是list不为空,新建一个扩容的数组,将之前的数据放在lists[addedCount]位置,新元素放到之前list的前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()方法分为四种情况。

我们先定义JSPerson及它的分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface JSPerson : NSObject

- (void)sayHello;

@end
#import "JSPerson.h"

@implementation JSPerson

- (void)sayHello{

NSLog(@"JSPerson say : Hello!!!");
}

+ (void)load{}

@end
@interface JSPerson (Test)

- (void)saySomething;

@end
#import "JSPerson+Test.h"

@implementation JSPerson (Test)

- (void)saySomething{
NSLog(@"%s",__func__);
}

+ (void)load{}

@end

非懒加载分类和非懒加载类

我们在JSPerson类和分类中都实现load,我们在realizeClassWithoutSwift添加断点

类和分类都是非懒加载分类加载

我们用lldb打印当前ro的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
readClass JSPerson....
_read_images JSPerson....
(lldb) p ro
(const class_ro_t *) $0 = 0x0000000100004280
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 0
instanceStart = 8
instanceSize = 8
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "JSPerson" {
Value = 0x0000000100003bbc "JSPerson"
}
}
baseMethodList = 0x00000001000042c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x00000001000042c8
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
name = "sayHello"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000036b0 (KCObjcBuild`-[JSPerson sayHello])
}

发现此时只有的方法,并没有分类的方法,说明分类目前还没有加载。

我们在attachCategories添加断点,继续执行代码:

非懒加载分类

继续用lldb调试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) p mlist
(method_list_t *) $5 = 0x0000000100004420
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 1)
}
(lldb) p $6.get(0).big()
(method_t::big) $7 = {
name = "saySomething"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb)

说明现在分类被加载了,被加载到了rwe中:

1
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);

注意:ATTACH_BUFSIZ=64也就是说分类的方法个数不能大于64。

非懒加载分类和懒加载类

我们删除JSPerson类的load方法,重新运行程序。

类和分类都是非懒加载分类加载

继续在我们第一个断点位置使用lldb调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
_read_images JSPerson....
(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000042a8
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 0
instanceStart = 8
instanceSize = 8
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "JSPerson" {
Value = 0x0000000100003bbc "JSPerson"
}
}
baseMethodList = 0x0000000100004178
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x0000000100004178
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 2)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
name = "saySomething"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) p $3.get(1).big()
(method_t::big) $5 = {
name = "sayHello"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000036b0 (KCObjcBuild`-[JSPerson sayHello])
}
(lldb)

发现分类已经加载了,也加载了,说明非懒加载类会使懒加载的类在启动时提前加载(如果没有分类是第一次调用时加载),说明加载的时机是编译期

懒加载分类和非懒加载类

我们删除JSPerson分类的load方法,重新运行程序。

类和分类都是非懒加载分类加载

使用lldb调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000042a8
(lldb) p *$0
(const class_ro_t) $1 = {
flags = 0
instanceStart = 8
instanceSize = 8
reserved = 0
= {
ivarLayout = 0x0000000000000000
nonMetaclass = nil
}
name = {
std::__1::atomic<const char *> = "JSPerson" {
Value = 0x0000000100003bbc "JSPerson"
}
}
baseMethodList = 0x0000000100004158
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000000000000
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethods()
(method_list_t *) $2 = 0x0000000100004158
(lldb) p *$2
(method_list_t) $3 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 2)
}
(lldb) p $3.get(0).big()
(method_t::big) $4 = {
name = "saySomething"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000037f0 (KCObjcBuild`-[JSPerson(Test) saySomething])
}
(lldb) p $3.get(1).big()
(method_t::big) $5 = {
name = "sayHello"
types = 0x0000000100003cfd "v16@0:8"
imp = 0x00000001000036c0 (KCObjcBuild`-[JSPerson sayHello])
}
(lldb)

发现分类都已经加载了,说明加载的时机也是编译期

懒加载分类和懒加载类

我们把分类load方法都删除,重新运行程序

懒加载分类第一次调用

依然走到断点,注意观察左边的调用栈,发现是从lookUpImpOrForward,说明是在第一次调用方法的时候加载的。

One More Condition

前面四种情况基本能包括了分类,但是还有种情况就是:有多个分类,部分分类实现了load方法主类也实现了load。我们就探索一下这个情况,新建一个JSPerson的分类

1
2
3
4
5
6
7
8
9
10
11
12
@interface JSPerson (Test2)

- (void)saySomething2;

@end
@implementation JSPerson (Test2)

- (void)saySomething2{
NSLog(@"%s",__func__);
}

@end

根据前面我们其实应清楚,就是实现load的分类肯定会在运行时加载,我们关注的点就在于没有实现load方法的分类是什么时候加载的呢也就是attachCategories是否会加载未实现load方法的分类,运行程序

多个分类加载

使用lldb调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lldb) p cat
(category_t *) $1 = 0x0000000100004448
(lldb) p *$1
(category_t) $2 = {
name = 0x0000000100003b8f "Test2"
cls = 0x00000001000049a0
instanceMethods = {
ptr = 0x0000000100004428
}
classMethods = {
ptr = 0x0000000000000000
}
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}
operator() -JSPerson....
attachCategories -JSPerson....
(lldb) p cat
(category_t *) $3 = 0x00000001000044c8
(lldb) p *$3
(category_t) $4 = {
name = 0x0000000100003b95 "Test"
cls = 0x00000001000049a0
instanceMethods = {
ptr = 0x0000000100004488
}
classMethods = {
ptr = 0x00000001000044a8
}
protocols = 0x0000000000000000
instanceProperties = 0x0000000000000000
_classProperties = 0x0000000000000000
}

调试我们发现,两个分类都加载了,也就是只要有一个分类实现了load,其他分类都会在启动时加载。

总结

本篇主要是探索了分类的加载,主要分为5种情况

  • 非懒加载类和非懒加载分类:此时分类是在运行时,也就是程序启动的时候加载的。
  • 懒加载类和非懒加载分类:此时分类是在编译时加载
  • 非懒加载类和懒加载分类:此时分类也是在编译时加载
  • 懒加载类和懒加载分类:此时分类在第一次调用时加载。
  • 非懒加载类,多个分类,部分是非懒加载分类:此时所有分类都是在程序启动时加载。