objc - 编译时期的Category

###本文环境: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的源码。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值