###本文环境: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.m
的main
函数里我打印了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 --
也就是print
,expr -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(⌘+⇪+<
),加入环境变量就好: