准备工作
- 新建一个命令行工程;
- 新建一个
YYPerson
类,定义一个walk
方法; - 新建一个
YYPerson+Test
分类,定义一个test
方法; - 新建一个
YYPerson_Eat
分类,定义一个eat
方法; - 然后cd 到文件路径下,执行
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YYPerson+Test.m
,经过编译
,会生成一个YYPerson+Test.cpp
文件,在此文件中我们看到_category_t
的结构体定义如下:
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;
};
-
YYPerson+Test
分类 在编译期会生成一个_category_t
结构体,如下所示:
static struct _category_t _OBJC_$_CATEGORY_YYPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"YYPerson",
0, // &OBJC_CLASS_$_YYPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_YYPerson_$_Test,
0,
0,
0,
};
- 主类
YYPerson
会赋值给_category
结构体的第一个成员; -
_CATEGORY_INSTANCE_METHODS_YYPerson_
的定义如下:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_YYPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_YYPerson_Test_test}}
};
- 看到了
test
方法,最终会赋值到_category
结构体的第四个成员_method_list_t *instance_methods
实例方法列表中; - 表明
YYPerson+Test
文件,在编译期时是生成一个_category
结构体,所有数据都存放在这个结构体中,并没有合并到主类YYPerson
中,合并的操作是在运行时,不在编译期
分类的底层结构体
在objc源码工程中全局搜索category_t
,可以看到分类的结构体定义如下所示:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *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在底层是
category_t
结构体; - 存在
name
与cls
两个属性; - 有两个
method_list_t
类型的结构体属性分别表示分类中实现的实例方法+类方法
; - 一个protocol_list_t类型的协议列表,表示分类中实现的协议;
- 一个property_list_t类型的属性列表,表示分类中定义的属性,一般在分类中添加的属性都是通过关联对象来实现;
- 注意在分类中的属性是没有set、get方法。
分类的加载机制
- 准备工作一:主类
YYCat
,新建两个分类YYCat (Add_One)
与YYCat (Add_Two)
- 准备工作二:为了方便调试源码工程,我们在
readClass
,realizeClassWithoutSwift
,methodizeClass
,attachToClass
,attachCategories
中都加入了以下的测试代码;
//测试代码
const char *mangledName = cls->mangledName();
const char *YYCatName = "YYCat";
if (strcmp(mangledName, YYCatName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
if (!kc_isMeta) {
printf("%s: 定位到 %s \n",__func__,YYCatName);
}
}
- 在 iOS底层系列16 -- 类的加载 文章中探索了类的加载机制,其中
methodizeClass
函数关于类的数据加载
与分类的数据加载
是分开进行的。
- 可以看到分类category是通过调用
attatchToClass
添加到类class的,然后才能在外界进行使用,主要分为以下几个步骤:- 分类数据加载时机是
根据类和分类是否实现load方法
来区分不同的时机; - attachCategories准备分类数据;
- attachLists将分类数据添加到主类中;
- 分类数据加载时机是
探索分类的加载时机
【当主类与分类均实现load类方法时】
-
attachCategories
源码实现如下:
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
//测试代码
const char *mangledName = cls->mangledName();
const char *YYCatName = "YYCat";
if (strcmp(mangledName, YYCatName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
if (!kc_isMeta) {
printf("%s: 定位到 %s \n",__func__,YYCatName);
}
}
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
//创建class_rwe_t结构体
auto rwe = cls->data()->extAllocIfNeeded();
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[I];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
- 直接在
attachCategories
函数内部加入测试代码并打下断点,当断点停住时,在LLDB控制台输入bt,打印函数调用堆栈如下所示:
- 由于
map_images
在load_images
之前调用,那么map_images
函数中关于分类信息加载的调用链为:map_images --> map_images_nolock --> _read_images --> readClass --> _getObjc2NonlazyClassList(非懒加载类) --> realizeClassWithoutSwift(实现类即加载类信息) --> methodizeClass(方法类化) --> attachToClass->attachCategories
- 在
map_images
的调用链中,在attachToClass
函数内部,去获取分类数据auto it = map.find(previously)
,分类数据是空的,所以不会调用attachCategories
函数,加载分类数据延迟到load_images
函数中了,在methodizeClass
会将主类的data数据赋值给class_rw_ext_t
;
在
load_images
函数中分类加载的函数调用链为:load_images --> loadAllCategories --> load_categories_nolock --> attachCategories
在
attachCategories
函数中将分类的data数据赋值给class_rw_ext_t
;当断点停在
attachCategories
函数中的如下代码行时:
- 前后两次打印的结果如下:
- 可以看到加载的两个分类的详细信息;
- 当第一次加载主类的一个分类(YYCat (Add_One))时,需要开辟rwe内存,第二次再加载另一个分类(YYCat (Add_Two)),就不需要再开辟rwe内存了,先前已经创建了,直接将第二次加载的分类信息写入rwe中即可;
类与分类的搭配使用
- 主要是看是否实现了load类方法,两两组合总共有四种情况如下所示:
【第一种:非懒加载类 与 非懒加载分类】
- 即
主类与分类都实现了load方法,且外界向主类发送消息
,逻辑流程上面已经探讨过了,现做如下总结: -
map_images
中的调用链:map_images --> map_images_nolock --> _read_images --> readClass --> _getObjc2NonlazyClassList(非懒加载类) --> realizeClassWithoutSwift(实现类即加载类信息) --> methodizeClass(方法类化) --> attachToClass --> attachCategories
; - 在
map_images
的调用链中,在attachToClass
函数内部,去获取分类数据auto it = map.find(previously)
,分类数据是空的,所以不会调用attachCategories
函数,加载分类数据延迟到load_images
函数中了,在methodizeClass
会将主类的data数据赋值给class_rw_ext_t
; - 在
load_images
函数中分类加载的函数调用链为:load_images --> loadAllCategories --> load_categories_nolock --> attachCategories
- 在
attachCategories
函数中将分类的data数据赋值给class_rw_ext_t
;
【第二种:非懒加载类 与 懒加载分类】
即
主类实现load方法,分类没有实现load方法,且外界没有调用主类发送消息
当断点停在
realizeClassWithoutSwift
函数内部的测试代码行时,函数调用堆栈为map_images->map_images_nolock->read_images->realizeClassWithoutSwift
,LLDB调试图下所示:
可以看出
class_ro_t
中方法的存储顺序为:YYCat (Add_Two) --> YYCat (Add_One) --> YYCat
;class_ro_t
中已经包含了分类的方法信息,即在编译期时类与分类的data数据就已经合并了
;放开当前断点,进入
methodizeClass
函数,打下断点如下:
- 当断点停止1436行时,LLDB调试如下:
- 跟上面的打印结果一致;
- 过掉当前断点来到
prepareMethodLists
函数 打下断点如下:
- 当断点停在所在行,LLDB调试如下:
- 过掉当前断点来到
fixupMethodList
函数,打下断点如下:
- 当断点停在1218行时,LLDB调试如下:
与排序之前的方法顺序进行比较,发现排序之后的方法顺序确实发生了变化;
-
总结:
- 在编译期时,类的
class_ro_t
中就已经存在类与分类的data数据; - 在运行时,在
methodizeClass
函数中,将类与分类的data数据赋值给class_rw_ext_t
;
- 在编译期时,类的
【第三种:懒加载类 与 懒加载分类】
- 即主类与分类都没有实现load类方法;
- 当外界没有向主类发送消息时,在
map_images
流程中只执行了readClass
只加载了class的地址和名称,并没有实现类即没有调用realizeClassWithoutSwift
函数,当程序执行到readClass
内部时,断点断住,LLDB调试结果如下:
- 在编译期时,类的
class_ro_t
中就已经存在类与分类的data数据; - 接下来在外界向主类发送消息,
YYPerson *person = [[YYPerson alloc]init]
,LLDB调试结果如下:
发送alloc消息,接着进入了
消息的慢速查找流程
,然后进入实现类的流程
,最后进入methodizeClass
函数,实现将类与分类的data数据赋值给class_rw_ext_t
;-
总结:
- 在编译期时,类的
class_ro_t
中就已经存在类与分类的data数据; - 第一次给主类发送消息时,首先会进入
消息的慢速查找流程
,然后进入实现类的流程
,最后进去methodizeClass
函数,实现将类与分类的data数据赋值给class_rw_ext_t
;
- 在编译期时,类的
【第四种:懒加载类 与 非懒加载分类】
- 即主类没有实现load类方法,分类实现了load类方法;
- 当程序执行到
readClass
内部时断点断住,LLDB调试如下:
- 在
class_ro_t
中只有主类的data数据,并没有分类的data数据,说明在编译期时,分类的数据并没有合并到主类中; - 过掉断点,在
attachCategories
函数内部断住,LLDB调试如下:
函数调用堆栈为
load_images->prepare_load_methods
在prepare_load_methods
中会调用_getObjc2NonlazyCategoryList
函数去获取所有非懒加载分类
,通过分类category
获取对应的主类Class cls = remapClass(cat->cls)
,然后进入实现主类的流程realizeClassWithoutSwift
,最后进去methodizeClass
函数,实现将类与分类的data数据赋值给class_rw_ext_t;最终总结如下图所示:
load类方法的调用顺序
- 首先调用所有类的load方法;
- 按照编译顺序调用,先编译的类,先调用;
- 在调用当前类的load方法之前,会先调用父类的load方法;
- 然后调用所有分类的load方法;
- 按照编译顺序调用,先编译的分类,先调用;
- 分类不存在继承的情况;
initlization方法
- 在类第一次接收到消息时调用;
- 先调用父类的initlization方法,再调用子类的initlization方法;
- 若子类没有实现initlization方法,会调用父类的initlization方法,所以父类的initlization方法可能会被调用多次;
- 如果分类实现了initlization方法,会优先调用分类的initlization方法;