objc - 编译时期的Category

    xiaoxiao2023-03-25  7

    本文环境: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 (如下图):

    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的源码。


    转载请注明原文地址: https://ju.6miu.com/read-1203866.html
    最新回复(0)