objc - Category中调回主类的同名原方法

###本文环境:Xcode 7.x

我们知道,Category里写了与主类同名的方法,按照Objc的消息机制调用,会调用了Category的,而主类的同名方法像是被覆盖了。

#Category方法中调用主类的同名原方法
实际上,Category并没有覆盖主类的同名方法,只是Category的方法排在方法列表前面,而主类的方法被移到了方法列表的后面。
于是,我们可以在Category方法里,利用Runtime提供的API,从方法列表里拿回原方法,从而调用。示例:

ViewController+HookViewDidLoad.h:

#import "ViewController.h"

@interface ViewController (HookViewDidLoad)

- (void)viewDidLoad ;

@end

ViewController+HookViewDidLoad.m:

#import "ViewController+HookViewDidLoad.h"
#include <objc/runtime.h>

@implementation ViewController (HookViewDidLoad)

- (void)viewDidLoad {
    NSLog(@"Invoke Category viewDidLoad");
    
    [ViewController invokeOriginalMethod:self selector:_cmd];
}

+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector {
    // Get the class method list
    uint count;
    Method *list = class_copyMethodList([target class], &count);
    
    // Find and call original method .
    for ( int i = count - 1 ; i >= 0; i--) {
        Method method = list[i];
        SEL name = method_getName(method);
        IMP imp = method_getImplementation(method);
        if (name == selector) {
            ((void (*)(id, SEL))imp)(target, name);
            break;
        }
    }
    free(list);
}

@end

遍历ViewController类的方法列表,列表里最后一个同名的方法,便是原方法,这里我是从列表尾向列表首遍历。完整示例工程代码在这里(Github iCategory-Hook)


#深一点,多个Category的情况
那么,我们来看看定义了同一个类的多个Category同名方法时,调用情况又会是怎么样呢,到底调用了哪一个Category的方法呢?请先下载示例代码,完整示例工程代码在这里Github iCategory-Sequence
首先改变一下下面两个Category文件的编译顺序,运行看看输出:

ViewController+ViewDidLoads.m文件比ViewController+ViewDidLoad.m编译后时,再改变其内部两Category的编译顺序,运行看看输出:

最后,我们可以得到结论,当同一个类的有多个Category时,调用同名方法,谁编译最后,谁就会被调用。为什么呢,因为最后编译的那个Category,其方法被放在了方法列表(无论是类里的实例方法列表还是元类里的类方法列表)的前面,当objc_msgSend查找方法时会优先找到了它。


#调试Runtime源码验证一下

打开可编译可调试的objc4-680源码工程,分别调试下面两个场景:

场景1.Category在不同文件的情况:

场景2.Category在同一个文件的情况:

先切换到场景1,下面我们一步步来打印Class Foo的方法列表baseMethodList

1.运行一次objc4-680工程,main.mmain函数里我打印了Class Foo的指针。运行后,我们拿到它的指针值是0x100001680

debug-objc[5846:363702] Pointer of Class Foo: 0x100001680

2.在objc-os.mm_objc_init方法处,打个断点:

3.再运行,当程序停在断点处,然后在lldb输入下面命令:

(lldb) expr method_list_t * $methods = (*(class_ro_t *)((objc_class *)0x100001680)->data()).baseMethodList
(lldb) p $methods->get(0)
(lldb) p $methods->get(1)
(lldb) p $methods->get(2)
			...
(lldb) p $methods->get(7)

至于((objc_class *)0x100001680)->data()这里本应该返回class_rw_t *指针的,这里却强转为class_ro_t *,大家去参考一下这篇文章深入解析 ObjC 中方法的结构
观察lldb的输出,然后再切换到场景2,对比两场景下lldb的打印方法输出的顺序,说明我们上面的结论是正确的。


#再深一点,剖析Runtime对Category方法顺序的处理

1.打开从苹果开源网站Runtime源码工程(会有很多error,编译不了),去我Github下载可编译调试的Runtime源码工程Github objc4-680,Terminal进入源码目录,查找Category结构体定义:

grep -rne "struct.*Category" .

得到

./runtime/objc-private.h:238:typedef struct category_t *Category;

继续grep

grep -rne "struct category_t" .

得到:

./runtime/objc-private.h:238:typedef struct category_t *Category;
./runtime/objc-runtime-new.h:1250:struct category_t {

那么,我们去objc-runtime-new.h第1250行,得到Category的定义:

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;
};

可看出,struct category_t包含了Category的名字,所属的主类,实例方法列表,类方法列表,协议列表,实例属性列表。(Category无法添加实例变量,可用关联对象实现)
其实,main()执行方法之前,dyld加载动态链接库及程序自己的可执行文件/mach-o文件。
dyld_dyld_start函数开始,接着调用libdispatch.dylib_os_object_init函数,接着调用libobjc_objc_init函数。此时,将控制权交给libobjc
2.我们可以按快捷⌘+⌥+\,打个_read_images符号断点来看看:

⌘+R运行,然后查看调用栈:

3.我们去看看_read_images源代码,可以看出,for (EACH_HEADER)代码非常多,苹果对Mach-o Headers做了非常多的处理。
我们直接跳到到关于Category处理的那一个for (EACH_HEADER)里面:

第一个remethodizeClass(cls)是对实例方法的方法列表重整。
第二个remethodizeClass(cls->ISA())传元类,是对类方法的方法列表的重整。

我们去跟踪进去看一下remethodizeClass函数的实现:

4.接着继续跟踪进去attachCategories(cls, cats, true /*flush caches*/)函数,发现里面对方法列表进行处理:

5.好了,我们跟踪进去看看这关键的attachLists方法的实现吧:

通过阅读attachLists方法代码码,可分析得到,memmove把原方法们移到了列表的后面,memcpy把新增的方法复制插到了列表的前面。于是,新的方法列表重建完。

##也可以lldb调试源码,一步一步观察

注意我们要观察同一个Class的方法列表,在上面第4步里,当我们发现cls值为NSObject时,我们这里可以为断点加个条件。右击断点,然后点击Edit Breakpoint

加上条件:(int)strcmp(((objc_class*)cls)->mangledName(), "NSObject") == 0

那么再运行,运行到这行时,当Class是NSObject时就会触发断点。然后,我们就跟踪进去到第5步的attachLists方法。

在执行memmove前,先打印看看状态(expr --也就是printexpr -O --也即po

(lldb) expr -- addedCount  #值为1,那么下面只观察addedLists[0]就可以了
(lldb) expr -- addedLists[0]->count
(lldb) expr -- addedLists[0]->first	 #也即expr -- addedLists[0][0]
(lldb) expr -- addedLists[0][1]
(lldb) expr -- addedLists[0][2]
...
(lldb) expr -- addedLists[0][addedLists[0]->count - 1]

然后在右侧小窗口点击Add Expression

加上Expression

方便观察,多加几个,把array()->lists[0]array()->lists[1]array()->lists[2]…的Expression也加上。当然也可用expr命令打印观察。

memcpy执行后,观察右侧小窗口Expression值变化。也可打印看看状态

(lldb) expr -- array()->count
(lldb) expr -- array()->lists[0]
(lldb) expr -- array()->lists[0]->count
(lldb) expr -- array()->lists[0][0]
(lldb) expr -- array()->lists[0][1]
...
(lldb) expr -- array()->lists[0][array()->lists[0]->count - 1]
(lldb) expr -- array()->lists[1]
(lldb) expr -- array()->lists[1]->count
(lldb) expr -- array()->lists[1][0]
(lldb) expr -- array()->lists[1][1]
...
(lldb) expr -- array()->lists[2]
(lldb) expr -- array()->lists[2]->count
...
(lldb) expr -- array()->lists[array()->count - 1]
...

会发现,array()->lists[0]就是addedLists[0]了,(如果addedLists元素是两个的话array()->lists[1]其实就是addedLists[1]),而array()->list原先的都被移到后面了。
结论是一样的:Category的方法不断的被放进了列表前面。


OK,完成了分析。建议大家去我Github下载可编译调试的Runtime源码工程Github objc4-680。多点调试,多点阅读一下源码。
在源码里,大家可以看到打印日志函数_objc_inform前面都有个判断的变量,去到这变量定义处objc-env.h(自己从源码里引入项目里就好),发现还有更多对应的环境变量。想打印这些日志,只要Edit Scheme(⌘+⇪+<),加入环境变量就好:


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值