iOS-Runtime随笔——Message Forward与应用

开发中我们可能遇到过这样的异常:unrecognized selector sent to xxxxxx,当我们调用的方法没有对应的实现(IMP)时,系统会抛出如上异常。但是在抛出异常之前,我们还有三次补救的机会:

  • 动态方法解析:Dynamic Method Resolution
  • 快速转发:Fast Forwarding
  • 正常转发:Normal Forwarding

这个就是Objective-C的消息转发。从上到下,进行消息转发的开销越来越大。网上有关这三个步骤的介绍很多,这里就不再重复了。本文着重从源码角度分析一下为什么 Message Forward 是由这几个步骤组成,以及苹果设计的 Message Forward 机制有什么具体作用等。

1、objc_msgSend

我们知道,Objective-C的方法调用实际上会转换成消息发送,比如:

1
[obj message];

最终会转换成objc_msgSend函数调用:objc_msgSend(receiver, @selector(message));。有关objc_msgSend函数,它的定义是id objc_msgSend(id self, SEL _cmd, ...),第一个参数为接收消息的对象,第二个参数为要调用的函数实现的选择子SEL,这两个参数为隐式参数,由系统自动生成,...为可变参数列表。我在之前的一篇文章中从汇编的角度分析过其实现原理,objc_msgSend函数由汇编实现,它的作用是:

  • 根据传入的对象,获取其isa指针,并根据isa指针找到对象所属的类
  • 获取类的方法缓存,并根据selector从缓存中查找对应的IMP
  • 如果从缓存中找到IMP,直接调用
  • 如果没有从缓存中找到,调用C函数(__class_lookupMethodAndLoadCache3)继续查询,并将查询结果存入类的方法缓存

这一小节分析一下ARM64平台下objc_msgSend函数的实现,由于汇编代码看起来太不直观,这里就不贴出来了,将其转换成C伪代码(为了简化流程,省略TaggedPointer部分),有兴趣的同学可以看下objc-msg-arm64.s文件中的汇编源码。

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
id objc_msgSend(id self, SEL _cmd, ...) {
if (!self) {
return nil;
}
IMP imp = nil;
imp = cacheLookUp(self->isa, _cmd);
if (!imp) {
imp = objc_msgSend_uncached(self, self->isa, _cmd);
}

if (imp) {
imp(self, op, ...);
} else {
fatal_error("unrecognized selector sent to xxxxxx");
}
}

// 从缓存中查找对应的IMP
IMP cacheLookUp(id obj, Class cls, SEL sel) {
// 先从本类中查找,如果没有,继续向父类查找,直到查找到NSObject为止
Class currCls = cls;
IMP imp = nil;
while (1) {
if (!currCls) break;
if (currCls == currCls->superclass) break;
imp = getImpFromCache(obj, currCls, sel);
if (imp) break;
currCls = currCls->superclass;
}
return imp;
}

// 缓存中未找到,进入__objc_msgSend_uncached函数
IMP objc_msgSend_uncached(id obj, Class cls, SEL sel) {
// 调用MethodTableLookup从未缓存的方法列表中查找IMP
// 在源文件中,MethodTableLookup是个宏定义
IMP imp = MethodTableLookup(cls, sel);
// 如果还未找到,跳转到__class_lookupMethodAndLoadCache3
if (!imp) {
imp = class_lookupMethodAndLoadCache3(obj, cls, sel);
}
return imp;
}

// 这个函数在objc-runtime-new.mm文件中定义
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

至此,objc_msgSend函数中的IMP查找流程比较清晰了:

  1. 首先从缓存中查找,如果当前类没有找到,继续向其父类查找,直到查询到NSObject为止
  2. 如果步骤1没有找到对应的IMP,在类的未缓存的方法列表中查找,根据isa指针,直到查找到NSObject为止
  3. 如果步骤2也没有找到,转到_class_lookupMethodAndLoadCache3,调用lookUpImpOrForward继续查找并进入消息转发流程。

2、lookUpImpOrForward与Message Forwarding

先看下lookUpImpOrForward函数的源码:

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
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

runtimeLock.assertUnlocked();
// 从缓存中查找,由于objc_msgSend函数进来时,cache参数设置为NO,这一步忽略
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

runtimeLock.lock();
...

retry:
runtimeLock.assertLocked();

// 从当前类及父类的方法缓存和方法列表中查找IMP
// 如果找到,对IMP进行缓存,这部分代码省略
...

// 经过以上步骤都没有找到IMP,执行一次动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}

// IMP未找到,动态方法解析失败,进入消息转发流程
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

done:
runtimeLock.unlock();

return imp;
}

lookUpImpOrForward会从当前类及其父类的的方法缓存和方法列表中查找IMP,如果未找到,执行动态方法解析,如果动态方法解析也失败,进入Message Forwarding流程。

2.1 动态方法解析

_class_resolveMethod调用_class_resolveInstanceMethod查看当前类是否实现了resolveInstanceMethod方法(对于类方法,对应的是resolveClassMethod),如果实现了,resolvedtrue,重新执行一次lookUpImpOrNil流程,将新的IMP加入缓存并执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 未实现动态方法解析,直接返回
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
}
2.2 消息转发

上面讲到,当方法缓存、方法列表和动态方法解析都未能找到IMP时,会调用_objc_msgForward_impcache进入forwarding流程,但是关于这个函数,我只在源码中找到了如下实现:

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
/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward is the externally-callable
* function returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
* method caches.
*
********************************************************************/

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b __objc_msgForward

END_ENTRY __objc_msgForward_impcache


ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward
#endif

可以看到这里涉及到了__objc_forward_handler的设置和读取。

1
2
3
4
2   CoreFoundation     0x00000001086a1f44 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 UIKitCore 0x0000000115f0bb4a -[UIResponder doesNotRecognizeSelector:] + 287
4 CoreFoundation 0x0000000108687ed6 ___forwarding___ + 1446
5 CoreFoundation 0x0000000108689da8 _CF_forwarding_prep_0 + 120

unrecognized selector sent to xxx崩溃是的堆栈信息可以看到,这里还涉及到了CF框架中的_CF_forwarding_prep_0___forwarding___两个函数,有关转发的所有逻辑都在___forwarding___中,而苹果公布的CoreFoundation源码中删去了这部分内容。网上大神对__CFInitialize进行逆向后得到了___forwarding___的伪代码:

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
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);

// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwarding != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}

// 僵尸对象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}

// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** 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.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

[receiver forwardInvocation:invocation];

void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}

SEL *registeredSel = sel_getUid(selName);

// selector 是否已经在 Runtime 注册过
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}

// The point of no return.
kill(getpid(), 9);
}

___forwarding___包含了整个消息转发路径的逻辑,概括如下:

  1. 先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),那就进入第二步。
  2. 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardingTargetForSelector 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进入第三步。
  3. 调用 doesNotRecognizeSelector 方法。
2.3 小结

借用网上的一张图片对Objective-C的消息发送和转发流程总结一下:

Objective-C的消息发送和转发流程

3、消息转发机制的应用

说了这么多,了解消息转发的过程对开发有什么帮助呢,或者消息转发有什么实际的应用呢?

3.1 weak proxy

在使用NSTimer或者CADisplayLink等定时器时,Timer会对其target(一般是self)进行强引用,稍微不注意可能会出现内存泄露问题,这时可以创建一个WeakProxy对象,由该对象弱引用self,将Timer的target设置为WeakProxy对象,并使用消息转发机制,将所有发送给WeakProxy的消息转发给self。由此打破retain cycle链,解决内存泄露问题。YYKit框架中就使用了这种方法(YYWeakProxy)。

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
@interface WeakProxy: NSProxy
// 注意这里是weak属性,打破循环引用链
@property (nullable, nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation WeakProxy

- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}

+ (instancetype)proxyWithTarget:(id)target {
return [[WeakProxy alloc] initWithTarget:target];
}

/// 消息转发
- (id)forwardingTargetForSelector:(SEL)selector {
// 所有发送给WeakProxy的消息均转发给target处理
return _target;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}

@end

// 创建Timer的时候使用WeakProxy作为target
_link = [CADisplayLink displayLinkWithTarget:[WeakProxy proxyWithTarget:self] selector:@selector(step:)];

3.2 多重代理

代理一般是一对一的,如果开发中遇到这样的问题:对某处做了修改,希望同时在多个页面更新。比如在聊天时,给对方加个备注,需要在资料页面、聊天详情页面、联系人列表页同步更新。这时可以用多重代理的方法实现。而多重代理的一种实现方式就是用消息转发。
设计一个中间层,中间层接收消息并负责转发给不同的页面。
具体代码可以参考别人写的一个Demo

3.3 模拟多继承

来自Wikipedia:
面向对象的程序设计中,继承描述了两种类型或两个类的对象,其中一种是另外一种的“子类型”或“子类”。子类继承了父类的特征,允许分享功能。例如,可以创造一个“哺乳类动物”类别,拥有进食、繁殖等的功能;然后定义一个子类型“猫”,它可以从父类继承上述功能,不需重新编写程序,同时增加属于自己的新功能,例如“追赶老鼠”。

然而,如果想同时自多于一个结构继承,例如容许“猫”继承“哺乳类动物”之余,同时继承“卡通角色”和“宠物”,缺乏多重继承往往会导致十分笨拙的混合继承,或迫使同一个功能在多于一个地方被重写。(这带来了维护上的问题)

虽然多重继承可以导致某些令人混淆的情况,但是在OOP中它还是有优点的。但是OC并没有实现真正意义上的多继承 (Swift与Java类似,同样不允许一个类继承自多个类,只能通过遵循多个协议来间接实现多继承),对于多继承,我们可以借助消息转发来实现。

示例代码如下:

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
// Father类,有一个方法:work
- (void)work {
NSLog(@"I'm working.");
}

// Mother类,有一个方法:cook
- (void)cook {
NSLog(@"I'm cooking.");
}

// Son类,继承自Father,持有Mother对象
@interface Son : Father
@property (nonatomic, strong) Mother *mother;
@end

@implementation Son
- (instancetype)init
{
self = [super init];
if (self) {
_mother = [Mother new];
}
return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([_mother respondsToSelector:aSelector]) {
return _mother;
}
return nil;
}

@end

Son类中重载了- (id)forwardingTargetForSelector:(SEL)aSelector;方法,当它无法处理消息时,系统会调用这个方法将消息转发给指定的target。测试代码和结果如下:

1
2
3
4
5
6
7
Son *son = [[Son alloc] init];
[son work];
[son performSelector:@selector(cook)];

// 输出
I'm working.
I'm cooking.

我们还可以利用forwardInvocation进行消息转发,对上述的Son类进行改造,也能达到同样的类似多继承的效果。

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
@implementation Son

- (instancetype)init
{
self = [super init];
if (self) {
_mother = [Mother new];
}
return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (signature) {
return signature;
}
if ([_mother respondsToSelector:aSelector]) {
signature = [_mother methodSignatureForSelector:aSelector];
}
return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([_mother respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_mother];
}
}

@end

参考文章