###本文环境:Xcode 7.x。
为了加深我们对Category的理解,在Runtime处理Category之前,我们看看编译时期Category会被clang
弄成长什么样呢?
##1. 源码
来,我们为Foo
类写几个Category来研究一下 (完整工程代码在objc4-680项目,Foo
类在debug-objc/
目录):
于是,在Terminal
下执行:
clang -rewrite-objc Foo.m
然后得到一个Foo.cpp
文件。打开一看,文件比较大,我们拉到结尾部分,找到代码:
#ifndef _REWRITER_typedef_Foo
#define _REWRITER_typedef_Foo
然后把上面代码一直至到文件头第0行的所有代码删掉,留下上面代码至文件尾的代码就好。删掉代码后,我们另存为Foo_simplified.cpp
(如下图):
##2. 分析
通过对上面Foo_simplified.cpp
的仔细分析,我们可得到下面一些结论:
1.方法部分,实例方法以_I_
作前缀,类方法以_C_
作前缀,都以static
修饰。方法命名的规则是:前缀+类名+[Category名]+原方法名。方法的前两个参数是self
和_cmd
。
另外,因为@synthesize identity
,_I_Foo_identity
及_I_Foo_setIdentity_
被生成。而Foo主类的Category Two里没有@synthesize name
,所以,_I_Foo_Two_name
及_I_Foo_Two_setName
没有被生成。
再另外,Foo主类与Category One里同名方法- (void)cat;
并不存在覆盖一说,编译后,它们分别变成了:
static void _I_Foo_cat(Foo * self, SEL _cmd)
static void _I_Foo_One_cat(Foo * self, SEL _cmd)
2.结构体部分,可以看到Class,Category,Protocol,Method等的结构体,对比Runtime源码里的,其实是一致的。i.e. 下面拿Category结构体对比一下。
a.Foo_simplified.cpp
里的struct _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;
};
b.objc-runtime-new.h
里的struct 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;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};
4.struct _class_t
与Runtime源码里的struct objc_class
一致,仔细看下面对比:
Foo_simplified.cpp
里的struct _class_t
:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
Runtime源码里objc-private.h
里的struct objc_object
:
struct objc_object {
isa_t isa;
}
Runtime源码里objc-runtime-new.h
里的struct objc_class
:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
5.struct _class_t
的ro
指针(read only)指向的struct _class_ro_t
结构体是存放着编译时期已经确定的方法列表、协议列表、实例变量列表、属性列表。它与Runtime源码里的struct class_ro_t
是一致的,大家可以自行对比Runtime源码里objc-private.h
里的struct class_ro_t
:
##3. 编译时期:方法列表里方法的顺序
看着Foo_simplified.cpp
上面的箭头图,跟着两个SETUP[]
代码(如下)的箭头一直往上找:
OBJC_CLASS_SETUP[]
:
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CLASS_SETUP[] = {
(void *)&OBJC_CLASS_SETUP_$_Foo,
};
OBJC_CATEGORY_SETUP[]
:
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
(void *)&OBJC_CATEGORY_SETUP_$_Foo_$_One,
(void *)&OBJC_CATEGORY_SETUP_$_Foo_$_Two,
};
那么,我们可以得到这样的实例方法顺序:
_class_ro_t _OBJC_CLASS_RO_$_Foo:
{{(struct objc_selector *)"cat", "v16@0:8", (void *)_I_Foo_cat},
{(struct objc_selector *)"identity", "i16@0:8", (void *)_I_Foo_identity},
{(struct objc_selector *)"setIdentity:", "v20@0:8i16", (void *)_I_Foo_setIdentity_}}
_category_t _OBJC_$_CATEGORY_Foo_$_One:
{{(struct objc_selector *)"cat", "v16@0:8", (void *)_I_Foo_One_cat}}
_category_t _OBJC_$_CATEGORY_Foo_$_Two:
{{(struct objc_selector *)"cat", "v16@0:8", (void *)_I_Foo_Two_cat},
{(struct objc_selector *)"more", "v16@0:8", (void *)_I_Foo_Two_more},
{(struct objc_selector *)"touch", "v16@0:8", (void *)_I_Foo_Two_touch}}
可以看到,编译时期,Foo
的_class_ro_t
里的方法列表_method_list_t *baseMethods
只有"cat"
&"identity"
&"setIdentity"
三个。
##4. Rutime时期:方法列表里方法的顺序
我们尝试在objc4-680项目调试打印Foo类class_ro_t
的method_list_t * baseMethodList
。
1.首先,选择Target debug-objc:
2.我们在objc-runtime-new.mm
的_read_images
方法里添加两个断点,注意是for(EACH_HEADER)
里_getObjc2ClassList
方法后面(如下图):
断点一,是为了打印所有类名,添加Action:expr ((objc_class*)cls)->mangledName()
,打印后自动继续执行的Options记得勾上,如下图:
断点二,是为了发现类是Foo
时,程序停住,Condition:(int)strcmp(((objc_class*)cls)->mangledName(), "Foo") == 0
,如下图:
3.两断点设好后,运行。过了好一会,程序停在了Line 2511
行第二个断点处。然后,我们在lldb
打印:
(lldb) p (objc_class *)cls
(objc_class *) $3088 = 0x00000001000016a0 ## 得到了Foo class的指针
(lldb) expr method_list_t * $methods = (*(class_ro_t *)((objc_class *)cls)->data()).baseMethodList
(lldb) p $methods->get(1)
(lldb) p $methods->get(2)
(lldb) p $methods->get(3)
...
(lldb) p $methods->get(7)
观察输出,得到实例方法的顺序却变成这样了:
Foo(Two):
name = "cat" imp = 0x0000000100000bf0 (debug-objc`-[Foo(Two) cat] at Foo.m:27)
name = "more" imp = 0x0000000100000c20 (debug-objc`-[Foo(Two) more] at Foo.m:31)
name = "touch" imp = 0x0000000100000c40 (debug-objc`-[Foo(Two) touch] at Foo.m:41)
Foo(One):
name = "cat" imp = 0x0000000100000bc0 (debug-objc`-[Foo(One) cat] at Foo.m:17)
Foo:
name = "cat" imp = 0x0000000100000b50 (debug-objc`-[Foo cat] at Foo.m:7)
name = "identity" imp = 0x0000000100000b80 (debug-objc`-[Foo identity] at Foo.h:5)
name = "setIdentity:" imp = 0x0000000100000ba0 (debug-objc`-[Foo setIdentity:] at Foo.h:5)
因此,我们在main.m
文件里main
函数里的发cat
消息给Foo
的实例时,实际上调用了Foo(Two)
里的cat
方法,因为它排在方法列表前面。
Foo *foo = [[Foo alloc] init];
[foo cat];
##4. 疑问
编译时期的_class_ro_t Foo
的baseMethods
只有三个方法,到Runtime时期的class_ro_t Foo
的baseMethodList
里把所有Category的方法列表加了进来,并且原先三个方法移到了后面。
新方法列表其实像这样添加了:_OBJC_$_CATEGORY_Foo_$_Two的方法子列表
+ _OBJC_$_CATEGORY_Foo_$_One的方法子列表
+ _OBJC_CLASS_RO_$_Foo的方法子列表
,而内部子列表的顺序是没有变的。
到看过上一篇文章的同学会知道,list_array_tt
的attachLists
方法实现,就是这样添加移动方法的,注意,method_array_t
是继承list_array_tt
的。可是,搜遍苹果Runtime源码objc4项目,并没有发现有哪一处是调用attachLists
来修改baseMethodList
的。
我们再试一次,根据上一次已经拿到的Foo
的class
指针0x00000001000016a0
,在dyld
把控制权交给objc4
的入口处,即objc-os.mm
的void _objc_init(void)
方法处,打个断点:
然后,lldb
里调试:
(lldb) expr method_list_t * $methods = (*(class_ro_t *)((objc_class *)0x00000001000016a0)->data()).baseMethodList
(lldb) p $methods->get(0)
(lldb) p $methods->get(1)
...
还是没什么新发现。
在Runtime拿到控制权前,即控制权在dyld
的时候,是什么时候对class_ro_t
的baseMethodList
进行添加Category的方法呢?那接下来我们看看dyld的源码。