类的加载 3 - 分类的加载

本文继续衔接 [OC 底层探索 14、类的加载 2]探索分类的加载。调试源码

一、分类的本质

分类的结构查看方法

1).cpp 文件

在 main.m 中任意添加一个分类信息的 .h .m 文件信息,编译生成 cpp 文件 clang -rewrite-objc main.m -o main.cpp

如下 tu:

category_t 结构:

1
2
3
4
5
6
7
8
1 struct _category_t {
2 const char *name;
3 struct _class_t *cls;
4 const struct _method_list_t *instance_methods;
5 const struct _method_list_t *class_methods;
6 const struct _protocol_list_t *protocols;
7 const struct _prop_list_t *properties;
8 };

有 2 个 _method_list_t 分类的方法是要 attach 到本类 cls 上由本类进行调用的,分类是不存在元类的说法的。

2)objc 源码搜索 category_t

 

2、分类的方法 list

分类添加的属性系统不会给其生成 set/get 方法,我们可通过 runtime 的 associate 进行 set/get 方法的动态关联。

二、分类的加载

1、源码分析

继续 OC 底层探索 14、类的加载 2 对 methodizeClass() 源码分析,首先给 MyPerson 添加分类,并添加如下方法:

 

1、运行工程,方法 list 中只有本类的 2 个方法,并没有分类中的方法,见下图:

继续执行代码到 源码 2 的 47 行起,category 分类的 attach,此时 list 仍是有 2 个方法:

2、objc::unattachedCategories.attachToClass() 代码如下,源码 1:

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
1     void attachToClass(Class cls, Class previously, int flags)
2 {
3 runtimeLock.assertLocked();
4 ASSERT((flags & ATTACH_CLASS) ||
5 (flags & ATTACH_METACLASS) ||
6 (flags & ATTACH_CLASS_AND_METACLASS));
9 // 我加的调试代码
10 const char *mangledName = cls->mangledName();
11 const char *myPersonName = "MyPerson";
12 if (strcmp(mangledName, myPersonName) == 0) {
13 printf("%s 函数 func: %s\n",__func__,mangledName);
14 }
16 auto &map = get();
17 auto it = map.find(previously);
19 if (it != map.end()) {
20 category_list &list = it->second;
21 if (flags & ATTACH_CLASS_AND_METACLASS) {
22 int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
23 attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
24 attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
25 } else {
26 attachCategories(cls, list.array(), list.count(), flags);
27 }
28 map.erase(it);
29 }
30 }

但是运行并未走进 attachCategories 源码 1 的 20 行代码,我们跳进 attachCategories() 方法并对其进行断点 (下面源码 2 的第 41 行):

3、attachCategories() 源码 2:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
1 // Attach method lists and properties and protocols from categories to a class.
2 // Assumes the categories in cats are all loaded and sorted by load order,
3 // oldest categories first.
4 static void
5 attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
6 int flags)
7 {
8 if (slowpath(PrintReplacedMethods)) {
9 printReplacements(cls, cats_list, cats_count);
10 }
11 if (slowpath(PrintConnecting)) {
12 _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
13 cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
14 cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
15 }
17 /*
18 * Only a few classes have more than 64 categories during launch.
19 * This uses a little stack, and avoids malloc.
20 *
21 * Categories must be added in the proper order, which is back
22 * to front. To do that with the chunking, we iterate cats_list
23 * from front to back, build up the local buffers backwards,
24 * and call attachLists on the chunks. attachLists prepends the
25 * lists, so the final result is in the expected order.
26 */
27 constexpr uint32_t ATTACH_BUFSIZ = 64;
28 method_list_t *mlists[ATTACH_BUFSIZ];
29 property_list_t *proplists[ATTACH_BUFSIZ];
30 protocol_list_t *protolists[ATTACH_BUFSIZ];
32 uint32_t mcount = 0;
33 uint32_t propcount = 0;
34 uint32_t protocount = 0;
35 bool fromBundle = NO;
36 bool isMeta = (flags & ATTACH_METACLASS);
37 auto rwe = cls->data()->extAllocIfNeeded();// rwe 初始化 --> 要对copy出的干净内存进行方法插入操作
40 // 我加的调试代码
41 const char *mangledName = cls->mangledName();
42 const char *myPersonName = "MyPerson";
43 if (strcmp(mangledName, myPersonName) == 0) {
44 printf("%s 函数 func: %s\n",__func__,mangledName);
45 }
47 // 遍历 分类数据的准备(method property protocol)
48 for (uint32_t i = 0; i < cats_count; i++) {
49 auto& entry = cats_list[i];
51 method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
52 if (mlist) {
53 if (mcount == ATTACH_BUFSIZ) {
54 prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
55 rwe->methods.attachLists(mlists, mcount);
56 mcount = 0;
57 }
58 mlists[ATTACH_BUFSIZ - ++mcount] = mlist;// mlists 添加数据 mlist --> 从后向前 倒序插入 64-1 64-2... (二维数组)
59 fromBundle |= entry.hi->isBundle();
60 }
62 property_list_t *proplist =
63 entry.cat->propertiesForMeta(isMeta, entry.hi);
64 if (proplist) {
65 if (propcount == ATTACH_BUFSIZ) {
66 rwe->properties.attachLists(proplists, propcount);
67 propcount = 0;
68 }
69 proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
70 }
72 protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
73 if (protolist) {
74 if (protocount == ATTACH_BUFSIZ) {
75 rwe->protocols.attachLists(protolists, protocount);
76 protocount = 0;
77 }
78 protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
79 }
80 }
81 // 分类方法的 排序、附着关联
82 if (mcount > 0) {
83 prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);// 排序
84 rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);// attach
85 if (flags & ATTACH_EXISTING) flushCaches(cls);
86 }
88 rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
90 rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
91 }

tip:rwe 的初始化 开辟:extAllocIfNeeded(),在以下几种场景 (我们可操作的):

  1. static IMP addMethod()
  2. static SEL * addMethods()
  3. BOOL class_addProtocol()
  4. static bool _class_addProperty()
  5. 分类

4、继续源码分析

attachLists() 源码 3:

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
1 void attachLists(List* const * addedLists, uint32_t addedCount) {
2 if (addedCount == 0) return;
4 if (hasArray()) {
5 // many lists -> many lists 多+多
6 uint32_t oldCount = array()->count;
7 uint32_t newCount = oldCount + addedCount;
8 setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));// 扩容
9 array()->count = newCount;
10 memmove(array()->lists + addedCount, array()->lists,
11 oldCount * sizeof(array()->lists[0]));// 移动旧的去后面
12 memcpy(array()->lists, addedLists,
13 addedCount * sizeof(array()->lists[0]));// cpy 新的在前面
14 }
15 else if (!list && addedCount == 1) {
16 // 0 lists -> 1 list
17 // 0到1 - list 没有数据第一次 - 第0个元素给list,此时 list 是一维的
18 list = addedLists[0];
19 }
20 else {
21 // 1 list -> many lists
22 // 1+many - 举例 many lists 是3个
23 List* oldList = list;
24 uint32_t oldCount = oldList ? 1 : 0;
25 uint32_t newCount = oldCount + addedCount;// 容量计算,旧+新的和 1+3=4
26 setArray((array_t *)malloc(array_t::byteSize(newCount)));// 开辟总大小的空间 newCount
27 array()->count = newCount;// array 的数量:是新的添加 manylists 后的数量
28 if (oldList) array()->lists[addedCount] = oldList;// 旧的list 放最后面 第3个位置
29 // memcpy(位置, 放谁, 大小)
30 // 把新的 lists 从起始位置0开始放
31 memcpy(array()->lists, addedLists,
32 addedCount * sizeof(array()->lists[0]));
33 }
34 }

分类的 attachLists 过程:

这里其实也可验证,为何我们天津爱的分类方法会优于本类方法调用 –> 本类方法并非被覆盖而是在后面了。 

2、源码验证

运行工程验证,如何走进 attachCategories() 呢?

给分类也添加 +load 方法,运行可以走了进去:

1、何时调用 attachCategories() 呢?

1.1. 断点 - 看堆栈信息

可以看到 load_categories_nolock() 之后走进了 attachCategories(),搜索 load_categories_nolock:

_read_images() 和 loadAllCategories() 两处 调了 load_categories_nolock(hi); 断点,重新运行工程,断点只走了 loadAllCategories():

–> loadAllCategories() 是在 load_images() 时调用,map_images 已经走完了。

 

1.2. 反推

已知必然会调 attachCategories(),全局搜索 attachCategories,找到下面两处进行了调用:

  1. attachToClass()
  2. load_categories_nolock()

通过断点调试发现走到了 load_categories_nolock().

attachCategories 调用流程图

问题:我们创建多个分类,只在 本类和其中一个分类中添加 load 方法 其他分类不加,那么不加 load 方法的分类是懒加载还是非懒加载处理呢?–> 下面的 ‘3’ 进行分析。

2、attachList 源码执行

执行到上面源码 2 的 84 行 (同时见下图源码):

ATTACH_BUFSIZ = 64  mcount = 1 –> 内存平移 –> 平移到最后位置。

分类的 attachLists(),走进了条件 1 + 多 : 

3、分类加载的几种情况

1)本类、分类均实现 load

上面的流程即使如此,全部走了 load_images() 加载到数据,文章上面的过程已可验证。

2)本类实现 load 方法非懒加载,分类都不实现 load

运行见下图,methodizeClass() 方法执行,2 个分类的方法数据都在 data() 里面了。(rwe 为 NULL 未开辟脏内存,data 在 macho 中)

上图 lldb 出的信息,排序:对 3 者相同的方法名的排序 - 后加的分类 方法在前,本类在最后。

3)本类和分类都懒加载 不实现 load 方法

在第一次方法调用时加载到数据:

执行过程: _read_images() –> readClass(). readClass 中 baseMethodList 的 count 也是 13 个,数据也是从 data() 里面拿到的:

方法排序和 ‘1)’ 中相同。

4)主类不实现 load 分类实现 load

通过堆栈信息查看加载流程:

methodList 一直到 执行到 attachToClass() 仍是只有 9 个方法:

attachToClass() –> 走进了 attachCategories() –> attachLists() .

可得结论:**分类实现了 load 方法会迫使主类进行提前加载数据 (但这里本类仍并非是非懒加载,只是被迫使提前加载了而已)**。 

5)主类和分类 cateMore 非懒加载 - cateTwo 懒加载不实现 load 方法:

1、运行工程,MyPerson 类第一次进入 attachCategories() –>

cateTwo 的方法加载在 data 中 (ro->baseMethods()) 取出,rwe 为 null 未开辟脏内存.

2、继续执行 MyPerson 类第二次进入 load_categories_nolock() –> attachCategories() –>

attachLists(),rew 非 NULL,分类 cateMore 的方法会 attachLists 进来:

所有分类都会走非懒加载!!!对于同一个本类,只要有一个分类是非懒加载,其他所有分类都会非懒加载的

总结:

  • 分类懒加载: 分类的数据信息是从 data 中读取的;
  • 有一 or 多个分类非懒加载:主类会提前加载 (早于 main 函数),并 attach 到所有非懒加载分类的信息;
  • 都为懒加载:在第一次方法调用时从 data() 中取出数据

问题回顾:通过 OC 底层探索 14 对方法排序源码的分析已知,方法排序:

1、首先根据方法名的地址进行排序 - name 的 address

2、若方法重名则根据 sel 排序:

1. sel 混乱则进行 fixedUp  2. sel 没有混乱则根据 imp 排序。 

验证

文章上面的流程也可得验证。

以上。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!