前言
上回分析了类的加载的大致流程,我们知道了懒加载类和非懒加载类的系统调用栈信息,通过断点调试追踪,我们知道了系统在调用realizeClassWithoutSwift去实现类的信息,通过methodizeClass方法,在进行方法列表绑定时,又通过prepareMethodLists对方法列表中的方法进行排序的,然后将类里的方法列表,属性列表、协议列表以及分类信息做一个绑定的过程。
然后
分类的信息绑定,如下图:
这个
attachToClass就是分类绑定的过程。
attachToClass的大致流程
下面看看attachToClass的源码:
void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
(flags & ATTACH_METACLASS) ||
(flags & ATTACH_CLASS_AND_METACLASS));
auto &map = get();
auto it = map.find(previously);
if (it != map.end()) {
category_list &list = it->second;
if (flags & ATTACH_CLASS_AND_METACLASS) {
int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
} else {
attachCategories(cls, list.array(), list.count(), flags);
}
map.erase(it);
}
}
大致理解:
-
auto &map = get();先获取一个map,类似集合。
Type &get() {
return *reinterpret_cast<Type *>(_storage);
}
-
auto it = map.find(previously);寻找previously类是否在其中。
iterator find(const_arg_type_t<KeyT> Val) {
BucketT *TheBucket;
if (LookupBucketFor(Val, TheBucket))
return makeIterator(TheBucket, getBucketsEnd(), true);
return end();
}
从find函数可知,map中的元素是BucketT这种类型,我们暂且理解为桶结构模板。如果找到了previously类,则返回其对应的桶结构的索引下标iterator,否则返回end()集合的末尾。
using iterator = DenseMapIterator<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>; iterator是DenseMapIterator结构体类型👇
-
if (it != map.end())不等于map.end就表示找到了。 -
category_list &list = it->second;iterator的second成员变量的类型是category_list👇
- 接着调用
attachCategories,这是核心代码,后面再仔细分析其流程。 -
map.erase(it);完成绑定后移除
至此,我们知道了分类绑定的调用链顺序:
realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories
绑定的大致过程分析完了,我们知道了how-->是如何加载的,那when-->何时加载到内存的呢?
既然类的加载是在_read_images中完成的,那么分类是否也在其中呢?
回到_read_image
根据注释和log日志,我们在3629到3633行找到了关于分类相关的代码-->
load_categories_nolock
load_categories_nolock
- 实例方法、协议和实例属性绑定在类cls中,而类方法、协议和类属性绑定在类cls的ISA即元类中。
- 都是通过
attachCategories中去绑定的。
以上只是我们根据类的加载,去分析分类何时加载到内存的,并没有去证明这个过程,接下来我们用示例证明分类的加载的时机,是否在_read_image中?
示例断点追踪
- 创建分类
#import "LGPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson (LG)
@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cate_instanceMethod1;
- (void)cate_instanceMethod2;
- (void)cate_instanceMethod3;
+ (void)cate_classMethod1;
@end
NS_ASSUME_NONNULL_END
#import "LGPerson+LG.h"
@implementation LGPerson (LG)
- (void)cate_instanceMethod1 {
NSLog(@"%s", __func__);
}
- (void)cate_instanceMethod2 {
NSLog(@"%s", __func__);
}
- (void)cate_instanceMethod3 {
NSLog(@"%s", __func__);
}
+ (void)cate_classMethod1 {
NSLog(@"%s", __func__);
}
@end
-
打断点
-
run运行
断点根本没有进来,看来不是在_read_images里去触发分类的加载时机,那时机到底在哪里?我们直接在load_categories_nolock里打个断点,再看看调用栈:
调用栈是:
load_images-->loadAllCategories-->load_categories_nolock-->attachCategories
小结
以上我们分析了分类加载how 与 when,请看下图👇
attachCategories流程分析
接下来我们具体看看attachCategories是如何将分类的信息绑定到类里面的。
-
注入代码,强制断点跳到我们要研究的类
LGPerson中:
-
LGPerson 和分类LG, 添加+load方法-->非懒加载化,可以简化加载的流程
- run运行
断点进入
cat中确实是LG分类,cat的信息是从header_info中而来,而header_info是从machO中读取的。
查看cat中的分类方法:
接着看加载:
在
attachCategories如法炮制设置断点:
其中mlists是二维数组
插入分类LG的方法列表
分类LG没有声明属性
也没有声明协议
只有一个分类
先
prepareMethodLists-->fixupMethodList
再attachList 共3部分条件判断
- 第一种情况是第二方框,当addedLists来了之后,将addedLists的第一个元素赋值给list,也就是说,当list不存在时,就会从0-1;
- 第二种情况是第三个方框的代码,当有list之后,加入很多个list,总的意思就是将新加入的放在前面,旧的数据放在后面。它这样的实现是一种算法思维,LRU算法,也就是说当分类和主类中的方法存在一样时,就加载主类的方法。
-
第三种情况是加载多个分类方法,它的功能是先扩容,然后将后加入的分类放在前面。
断点进入的是第二种情况:
查看oldList的方法列表,其实就是LGPerson本类的方法列表
copy分类方法到内存
因为放在了索引0处,所以分类方法在本类的方法的前面。
下面是lldb验证分类的方法在array()前面。
(lldb) p array()
(list_array_tt<method_t, method_list_t>::array_t *) $0 = 0x00000001006121a0
(lldb) p $0.lists[0]
(method_list_t *) $2 = 0x0000000100008048
Fix-it applied, fixed expression was:
$0->lists[0]
(lldb) p $2.get(0)
(method_t) $3 = {
name = "cate_instanceMethod1"
types = 0x0000000100003e10 "v16@0:8"
imp = 0x0000000100003a40 (KCObjc`-[LGPerson(LG) cate_instanceMethod1])
}
Fix-it applied, fixed expression was:
$2->get(0)
(lldb) p $2->get(1)
(method_t) $4 = {
name = "cate_instanceMethod2"
types = 0x0000000100003e10 "v16@0:8"
imp = 0x0000000100003a70 (KCObjc`-[LGPerson(LG) cate_instanceMethod2])
}
(lldb) p $2->get(2)
(method_t) $5 = {
name = "cate_instanceMethod3"
types = 0x0000000100003e10 "v16@0:8"
imp = 0x0000000100003aa0 (KCObjc`-[LGPerson(LG) cate_instanceMethod3])
}
(lldb) p $0.lists[0].get(1)
(method_t) $6 = {
name = "cate_instanceMethod2"
types = 0x0000000100003e10 "v16@0:8"
imp = 0x0000000100003a70 (KCObjc`-[LGPerson(LG) cate_instanceMethod2])
}
Fix-it applied, fixed expression was:
$0->lists[0]->get(1)
(lldb) p $0.lists[1]
(method_list_t *) $7 = 0x00000001000081d8
Fix-it applied, fixed expression was:
$0->lists[1]
(lldb) p $7.get(0)
(method_t) $8 = {
name = "kc_instanceMethod1"
types = 0x0000000100003e10 "v16@0:8"
imp = 0x0000000100003b40 (KCObjc`-[LGPerson kc_instanceMethod1])
}
Fix-it applied, fixed expression was:
$7->get(0)
(lldb) p $7->get(1)
(method_t) $9 = {
name = "kc_instanceMethod3"
types = 0x0000000100003e10 "v16@0:8"
imp = 0x0000000100003b10 (KCObjc`-[LGPerson kc_instanceMethod3])
}
(lldb) p $7->count
(uint32_t) $10 = 8
(lldb)
至此,分类的方法就与本类的方法加载到了一起。
以上通过设置判断条件,断点追踪,得到以下结论:
本类和分类均是非懒加载类时,分类方法的加载在load_images中,本类的方法在_read_images中,然后再把分类的方法贴到本类方法列表中,其中分类的方法靠前。
其它情况
根据排列组合,还有以下三种情况:
- 主类非懒加载 分类懒加载
- 主类懒加载 分类非懒加载
- 主类 分类均懒加载
我们稍微改变下示例,声明两个分类LGA和LGB
@interface LGPerson (LGA)
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end
@interface LGPerson (LGB)
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
1.主类非懒加载 分类懒加载
在realizeClassWithoutSwift中观察类cls里的data()数据,发现是class_ro_t结构体类型👇
查看baseMethodLists里面的方法
上图可知,在realizeClassWithoutSwift中,cls的两个分类的方法已经绑定在方法列表中了,再看左侧调用栈信息,可知是在_read_images时,cls的分类就已经加载完毕了。
2.主类懒加载 分类非懒加载
注意:main方法中没有调用LGPerson的任何方法:
直接运行,看看日志记录👇
从日志中查看,没有触发
load_categories_nolock方法,最终走的是attachToClass,那么我们在attachToClass中断点看看,只有主类的方法,如下图👇
但是我并没有在main方法中调用关于LGPerson的任何方法,LGPerson本是懒加载类,按道理只有消息发送时才能触发LGPerson类的加载,但是现在确已经加载了。
接着我们的断点继续走,来到
attachCategories中:
接着断点走到
attachLists
接着来到
else第三种情况,上面分析过:先扩容,然后将后加入的分类放在方法列表的前面。
综上,主类懒加载,分类非懒加载时,
- 主类会以非懒加载的方式提前加载数据,
- 分类方法添加的调用链是:
realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories --> attachLists
3.主类 分类均懒加载
依旧main方法中没有调用LGPerson的任何方法,此时类信息和分类信息都不应该加载
运行观察日志:果然没有加载👇
再触发方法,让它们加载
既然是消息发送,那我们去到
lookUpImpOrForward中断点查看,如何触发的懒加载
断点锁定了LGPerson,继续走,如下图👇
因为懒加载,类LGPerson肯定未实现,那么会触发
realizeClassMaybeSwiftAndLeaveLocked
接着走,来到
realizeClassWithoutSwift
后面流程就不用走大家都应该知道。至此,我们锁定了一个新的方法
realizeClassMaybeSwiftMaybeRelock,同理,我们在此方法中增设条件
放开所有断点,run运行项目,查看日志:
上图证明了,不论是懒加载的类,还是懒加载的分类,都是在消息发送触发的前提条件下,才会去触发类信息与分类信息的加载。
总结
分类数据的加载时机如下:
- 非懒加载类 + 非懒加载分类:其数据的加载在load_images方法中,首先对类进行加载,然后把分类的信息贴到类中。
- 非懒加载类 + 懒加载分类:其数据加载在read_image就加载数据,数据来自MachO文件的data,是在编译时期就已经完成,data中除了类的数据,还有分类的数据,与类绑定在一起。
- 懒加载类 + 懒加载分类:其数据加载推迟到第一次消息发送时,数据同样来自MachO文件的data,data在编译时期就已经完成。
- 懒加载类 + 非懒加载分类:只要分类实现了load,会迫使主类提前加载,即在_read_images中不会对类做实现操作,需要在load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据。












网友评论