objc - 编译时期的Category

来源:互联网 发布:问道手游源码服务端 编辑:程序博客网 时间:2024/06/02 08:10

本文环境:Xcode 7.x

为了加深我们对Category的理解,在Runtime处理Category之前,我们看看编译时期Category会被clang弄成长什么样呢?

1. 源码

来,我们为Foo类写几个Category来研究一下 (完整工程代码在objc4-680项目,Foo类在debug-objc/目录):

Foo.h & Foo.m

于是,在Terminal下执行:

clang -rewrite-objc Foo.m

然后得到一个Foo.cpp文件。打开一看,文件比较大,我们拉到结尾部分,找到代码:

#ifndef _REWRITER_typedef_Foo#define _REWRITER_typedef_Foo

然后把上面代码一直至到文件头第0行的所有代码删掉,留下上面代码至文件尾的代码就好。删掉代码后,我们另存为Foo_simplified.cpp (如下图):

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_tro指针(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_tmethod_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 FoobaseMethods只有三个方法,到Runtime时期的class_ro_t FoobaseMethodList里把所有Category的方法列表加了进来,并且原先三个方法移到了后面。

新方法列表其实像这样添加了:_OBJC_$_CATEGORY_Foo_$_Two的方法子列表 + _OBJC_$_CATEGORY_Foo_$_One的方法子列表 + _OBJC_CLASS_RO_$_Foo的方法子列表,而内部子列表的顺序是没有变的。

到看过上一篇文章的同学会知道,list_array_ttattachLists方法实现,就是这样添加移动方法的,注意,method_array_t是继承list_array_tt的。可是,搜遍苹果Runtime源码objc4项目,并没有发现有哪一处是调用attachLists来修改baseMethodList的。

我们再试一次,根据上一次已经拿到的Fooclass指针0x00000001000016a0,在dyld把控制权交给objc4的入口处,即objc-os.mmvoid _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_tbaseMethodList进行添加Category的方法呢?那接下来我们看看dyld的源码。


0 0