OC的消息转发流程底层探究

转发流程定位

上一篇文章的最后,我们找了方法未实现时的默认函数,下面我们调一个未实现的方法,看看具体的调用栈.

1
2
2021-01-04 20:05:16.074234+0800 LLYObjc[11678:7221659] -[LLYModel fun0]: unrecognized selector sent to instance 0x1006f2150
2021-01-04 20:05:16.075692+0800 LLYObjc[11678:7221659] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[LLYModel fun0]: unrecognized selector sent to instance 0x1006f2150'

bt下看看堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
frame #0: 0x00007fff717f633a libsystem_kernel.dylib`__pthread_kill + 10
frame #1: 0x00000001004bc9bc libsystem_pthread.dylib`pthread_kill + 430
frame #2: 0x00007fff7177d808 libsystem_c.dylib`abort + 120
frame #3: 0x00007fff6e9e4458 libc++abi.dylib`abort_message + 231
frame #4: 0x00007fff6e9d58bf libc++abi.dylib`demangling_terminate_handler() + 262
* frame #5: 0x00000001002e99b3 libobjc.A.dylib`_objc_terminate() at objc-exception.mm:701:13
frame #6: 0x00007fff6e9e3887 libc++abi.dylib`std::__terminate(void (*)()) + 8
frame #7: 0x00007fff6e9e61a2 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
frame #8: 0x00007fff6e9e6169 libc++abi.dylib`__cxa_throw + 113
frame #9: 0x00000001002e9158 libobjc.A.dylib`objc_exception_throw(obj="-[LLYModel fun0]: unrecognized selector sent to instance 0x1006f2150") at objc-exception.mm:591:5
frame #10: 0x00007fff37660936 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
frame #11: 0x00007fff37545ec0 CoreFoundation`___forwarding___ + 1427
frame #12: 0x00007fff37545898 CoreFoundation`__forwarding_prep_0___ + 120
frame #13: 0x0000000100003960 LLYObjc`main(argc=1, argv=0x00007ffeefbff4f8) at main.m:55:9 [opt]
frame #14: 0x00007fff716aecc9 libdyld.dylib`start + 1
frame #15: 0x00007fff716aecc9 libdyld.dylib`start + 1

main后和doesNotRecognizeSelector方法前分别调用了 __forwarding_prep_0____forwarding__ 这两个函数,我们猜测是OC的消息转发逻辑,那么如何去验证呢,老办法,我们先去源码中看看。

一无所获。

那我们再想想其他办法,我们分别点开这两个函数的堆栈,在头部发现了一些信息

1
2
3
CoreFoundation`__forwarding_prep_0___:
CoreFoundation`___forwarding___:

看来这两个函数的定义可能在CoreFoundation库中,那么我们自然就去开源库里面找找看,CoreFoundation的源码我们是找到了,打开发现还是没有上面的函数,看来并未全部开源,那么还是其他办法去探索么?

答案是有的,还可以通过反汇编工具来查看CF的动态库文件,这里推荐Hopper Disassembler.
在Hopper中,我们找到了如下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int ____forwarding___(int arg0, int arg1) {
// 去掉了一些参数的赋值和传递逻辑和判断逻辑
loc_649bb:
var_148 = r13;
var_138 = r12;
var_158 = rsi;
rax = object_getClass(rbx);
r12 = rax;
r13 = class_getName(rax);
if (class_respondsToSelector(r12, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_64a67;
loc_64a8a:
rax = class_respondsToSelector(r12, @selector(methodSignatureForSelector:));
r14 = var_138;
var_148 = r15;
if (rax == 0x0) goto loc_64dd7;
loc_64ad5:
r12 = rax;
rax = [rax _frameDescriptor];
r13 = rax;
if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != rbx) {
rax = sel_getName(stack[-328]);
_CFLog(0x4, @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", rax);
}
rax = object_getClass(r14);
rax = class_respondsToSelector(rax, @selector(_forwardStackInvocation:));
stack[-344] = r13;
if (rax == 0x0) goto loc_64c19;
loc_64c19:
if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_64ec2;
loc_64e3c:
rax = sel_getName(var_140);
r14 = rax;
rax = sel_getUid(rax);
if (rax != var_140) {
_CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, r14, rax, r9, stack[-360]);
}
if (class_respondsToSelector(object_getClass(var_138), @selector(doesNotRecognizeSelector:)) == 0x0) {
____forwarding___.cold.2(var_138);
}
(*_objc_msgSend)(var_138, @selector(doesNotRecognizeSelector:));
asm { ud2 };
rax = loc_64ec2(rdi, rsi);
return rax;
}

通过上面伪代码,我们提炼出了几个比较重要的函数,即我们常说的消息转发的流程(除了_forwardStackInvocation这个函数,这个函数我们下面在说)原来出处在CoreFoundation中,知道了出处,我们再来验证下他们的逻辑。

转发流程验证

resolveInstanceMethod && resolveClassMethod 动态方法决议

该部分的源码就在objc中,上一篇解释消息慢查询的时候只是一笔带过了,这里我们再来分析下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
//...
//上面是imp查找过程
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
//...
//下面的缓存过程
}
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// cls是类对象
resolveInstanceMethod(inst, sel, cls);
}
else {
// cls是元类对象
resolveClassMethod(inst, sel, cls);
// 类方法列表中没有,再去实例方法中找,这里就有一个坑点,根元类的superclass指针指向根类,也就是最终会找到NSObject中去,所以理论上我们能在NSObject的分类中处理所有的类方法找不到问题。
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 未实现直接返回
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
// 如果该类resolveInstanceMethod方法已实现,帮我们调一次
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 继续走一遍查找逻辑
IMP imp = lookUpImpOrNil(inst, sel, cls);
// ...
// 非重要信息
}
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// 未实现直接返回
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
// 如果该类resolveClassMethod方法已实现,帮我们调一次
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
// 继续走一遍查找逻辑
IMP imp = lookUpImpOrNil(inst, sel, cls);
// ...
// 非重要信息
}

看了源码,我们知道了只要重写一下上面两个方法,系统就会调一次这两方法,相当于一个钩子,我们可以在钩子里面动态的给该类加上缺失的方法,正常逻辑ok,那如果我们只是重写但是不动态添加方法实现会怎么样呢?我们来验证下:

1
2
3
4
5
6
7
8
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%@ %s",NSStringFromSelector(sel),__func__);
return [super resolveInstanceMethod:sel];
}
2021-01-04 21:29:51.339930+0800 LLYObjc[12200:7268336] fun0 +[LLYModel resolveInstanceMethod:]
2021-01-04 21:29:51.340714+0800 LLYObjc[12200:7268336] fun0 +[LLYModel resolveInstanceMethod:]
2021-01-04 21:29:51.340894+0800 LLYObjc[12200:7268336] -[LLYModel fun0]: unrecognized selector sent to instance 0x101918dc0

奇怪的事情发生了,resolveInstanceMethod进来了两次,第一次进来比较好理解,但是第二次是从哪里进来的呢?是不是因为递归查找的原因?我们打个断点看看两个打印时的bt:

第一次

1
2
3
4
5
6
7
8
9
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x0000000100313413 libobjc.A.dylib`resolveInstanceMethod(inst=0x000000010113abd0, sel="fun0", cls=LLYModel) at objc-runtime-new.mm:6005:30
frame #1: 0x00000001002fee83 libobjc.A.dylib`resolveMethod_locked(inst=0x000000010113abd0, sel="fun0", cls=LLYModel, behavior=1) at objc-runtime-new.mm:6043:9
frame #2: 0x00000001002fe7ac libobjc.A.dylib`lookUpImpOrForward(inst=0x000000010113abd0, sel="fun0", cls=LLYModel, behavior=1) at objc-runtime-new.mm:6192:16
frame #3: 0x00000001002d9899 libobjc.A.dylib`_objc_msgSend_uncached at objc-msg-x86_64.s:1101
frame #4: 0x0000000100003890 LLYObjc`main(argc=1, argv=0x00007ffeefbff4f8) at main.m:55:9 [opt]
frame #5: 0x00007fff716aecc9 libdyld.dylib`start + 1
frame #6: 0x00007fff716aecc9 libdyld.dylib`start + 1

第二次

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x0000000100313413 libobjc.A.dylib`resolveInstanceMethod(inst=0x0000000000000000, sel="fun0", cls=LLYModel) at objc-runtime-new.mm:6005:30
frame #1: 0x00000001002fee83 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="fun0", cls=LLYModel, behavior=0) at objc-runtime-new.mm:6043:9
frame #2: 0x00000001002fe7ac libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="fun0", cls=LLYModel, behavior=0) at objc-runtime-new.mm:6192:16
frame #3: 0x00000001002d8379 libobjc.A.dylib`class_getInstanceMethod(cls=LLYModel, sel="fun0") at objc-runtime-new.mm:5922:5
frame #4: 0x00007fff3755d697 CoreFoundation`__methodDescriptionForSelector + 282
frame #5: 0x00007fff37579204 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #6: 0x00007fff37545ac5 CoreFoundation`___forwarding___ + 408
frame #7: 0x00007fff37545898 CoreFoundation`__forwarding_prep_0___ + 120
frame #8: 0x0000000100003890 LLYObjc`main(argc=1, argv=0x00007ffeefbff4f8) at main.m:55:9 [opt]
frame #9: 0x00007fff716aecc9 libdyld.dylib`start + 1
frame #10: 0x00007fff716aecc9 libdyld.dylib`start + 1

可以看到,第一次堆栈就是正常的lookUpImpOrForward查找逻辑触发的,但是第二次的触发确是在消息转发中获取到函数签名后,那如何验证呢?很简单,我们去实现下methodSignatureForSelector这个方法看看:

1
2
3
4
5
6
7
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
2021-01-04 22:06:15.458988+0800 LLYObjc[12668:7297440] fun0 +[LLYModel resolveInstanceMethod:]
2021-01-04 22:06:15.459889+0800 LLYObjc[12668:7297440] _forwardStackInvocation: +[LLYModel resolveInstanceMethod:]
2021-01-04 22:06:15.460152+0800 LLYObjc[12668:7297440] -[LLYModel fun0]: unrecognized selector sent to instance 0x100738090

果然,第二次打印的函数变成了这个_forwardStackInvocation,这是哪来的呢?请看上面的CoreFoundation伪代码。大胆猜测就是如果没有实现自己的函数签名的话,系统还是用之前的函数签名(fun0)去调用一次lookUpImpOrForward,实现后换成CoreFoundation内部的(_forwardStackInvocation)函数签名去调用一次lookUpImpOrForward.

forwardingTargetForSelector 快速转发

快速转发需要做的操作比较简单,返回一个实现了该方法的类。

methodSignatureForSelector && forwardInvocation 慢速转发

慢速转发有下面两个有意思的点:

趣点一:方法签名并未指定格式,你可以返回任意正确的格式,比如我返回下面这种格式也ok:

1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"];
}

项目中应该没有那个函数带了这么多参数吧。

但是下面这个格式就不行:

1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@"];
}

因为缺少最基本的sel参数。

趣点二: 只需要重写forwardInvocation函数,即使是一个空函数,系统即认为当前被转发的方法已经被人处理,不会抛出异常。这里我们就可以做一些其他的事情了。

小结

通过以上探索,我们不仅掌握了OC消息转发的实现方式,还了解了转发逻辑内部的实现原理,也为我们在生产环境的灵活使用打下基础,比如我们可以通过消息转发机制实现方法调用的防护工作等比较重要的内容,更多的功能需要我们进一步的探索和实践。