OC-RunTime: 消息转发之实例方法的转发流程

本篇只是从系统函数的实现上, 来分析消息转发的流程.

下一篇结合本篇的理论和实际的例子来分析一下消息转发.

需要明确的几个问题

在往下看这篇博文之前, 有几个问题需要大家达成共识.

1.Objective-C 的特点
按照是否需要编译的原理来说, 编程语言一般可以分为静态编译类型和动态解释类型.

如 Java/C/C++ 是属于编译类型的语言, Php/Python/Ruby 属于解释类型的语言.

Objective-C 是基于 C 并具有自身特点的编译型语言, 再加上其 RunTime 机制, Objective-C 既是编译型又是动态的一门编程语言. 所谓的动态指的是在程序编译后运行中可以改变其结构.

2.函数调用
在 Objective-C 中调用函数, 被解释为向一个对象发送消息, 该对象可以是类对象也可以是实例对象.

例如:

1
[person play];

意思是向实例对象 person 发送一个 play 的消息.

还有一个比较有意思的是, 向一个空对象(nil) 发送消息不会 crash, 如果在 Java 中这个是不行的.例如:

1
2
person = nil;
[person play];

这个不会导致程序崩溃, 只是调用 play 函数没有任何反应罢了!

3.self
在 C++/Java 语言中, 有 this 指针的概念, 在 Objective-C 中, 也有类似的 this 指针即 self. self 既可以是实例对象也可以是类对象.

这里举个例子, 下面两个同名函数, 一个是类方法(+)一个是实例方法(-).
跟 Java 中的类似, 实例方法可以直接使用类方法, 但是类方法不可以直接使用实例方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (void)testClassMethod
{
[self sendMsg];
}
- (void)testInstanceMethod
{
[self sendMsg];
}
// 类方法
+ (void)sendMsg
{
NSLog(@"+ sendMsg. self: %@", self);
}
// 实例方法
- (void)sendMsg
{
NSLog(@"- sendMsg. self: %@", self);
}

在 ViewController 中分别调用两个 test 方法, 输出内容, 如下:

1
2
3
+ sendMsg. self: ViewController
- sendMsg. self: <ViewController: 0x7fc64740a5e0>

从结果可以看出类方法中的 self 代表的是该类, 实例方法中的 self 指的是类的一个实例即对象.

关于 Message Forwarding

Message Forwarding 即消息转发.

关于消息转发, 官方文档 Message Forwarding 是这么解释的:

1
2
3
Sending a message to an object that does not handle that message is an error. However,
before announcing the error,
the runtime system gives the receiving object a second chance to handle the message.

大概意思是这样的:

向一个对象发送消息, 该对象如果无法处理该消息, 系统就会报错, 但是在报错之前, 利用 Objective-C 提供的运行时机制可以防止报错的发生. 在 iOS 中类似这样的报错会导致程序直接 crash.
这里的对象, 可以是实例对象又可以是类对象.

在开发过程中, 大家一般都会遇到类似的 crash:

1
2
3
4
5
6
7
8
9
10
11
12
*** Terminating app due to uncaught exception
'NSInvalidArgumentException',
reason: '-[Controller play]: unrecognized selector sent to instance 0x7ff779f322a0'
*** First throw call stack:
(
CoreFoundation 0x000000010d89712b __exceptionPreprocess + 171
libobjc.A.dylib 0x000000010cf2bf41 objc_exception_throw + 48
CoreFoundation 0x000000010d918024 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
UIKit 0x000000010df48f51 -[UIResponder doesNotRecognizeSelector:] + 295
CoreFoundation 0x000000010d819f78 ___forwarding___ + 1432
CoreFoundation 0x000000010d819958 _CF_forwarding_prep_0 + 120
)

这个 crash 很明显, Controller 中调用了一个不存在的函数 play.

这里可以看到一个被调用的系统函数 doesNotRecognizeSelector, 今天我们剖析一下消息转发的流程.

实例方法(消息)转发的流程

在上面已经说过, OC 可以通过运行时来避免因为找不到方法而导致错误.

其实, 含义就是 OC 给了我们第二次机会来避免类似的错误.

我们重写 NSObject 中的 5 个方法, 分别是:

  1. +resolveInstanceMethod
  2. -forwardingTargetForSelector
  3. -methodSignatureForSelector
  4. -forwardInvocation
  5. -doesNotRecognizeSelector:

当无法找到对应的方法时, 调用的方法和顺序大致如下:

1
2
3
4
+resolveInstanceMethod
-forwardingTargetForSelector
-methodSignatureForSelector
-doesNotRecognizeSelector:

当找到对应的方法时, 调用方法和顺序大致如下:

1
2
3
4
+resolveInstanceMethod
-forwardingTargetForSelector
-methodSignatureForSelector
-forwardInvocation

根据实践结果, 画流程图如下:
1

下面分别解释一下这几个方法的作用和意义.

+ (BOOL)resolveInstanceMethod:(SEL)sel

解析对应的实例方法. 在该方法中允许增加一个方法的实现, 从而实现动态添加方法.

默认返回 NO.

NSObject.mm 中可以看到方法的实现:

1
2
3
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector

将对应的 selector 转发给指定的对象. 换句话说就是将之前没有实现的 selector 转交给另外一个可能实现了该 selector 的对象去处理.

默认返回 nil.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

该方法返回对 selector 实现的方法签名.

默认实现如下:

1
2
3
4
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}

- (void)forwardInvocation:(NSInvocation *)anInvocation

该方法是依赖第3个方法的, 如果第三个方法找到了对应的方法签名, 该方法就可以实现转向调用了.

- (void)doesNotRecognizeSelector:(SEL)sel

如果前面几个方法都没有处理好, 系统会调用该方法, 直接导致程序 crash, 也称之为终极死亡方法.

源码实现如下:

1
2
3
4
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}

总结, Objective-C 中给一个对象发送消息会经过如下几个步骤:

步骤1. 在类的调度表(dispatch table) 中找要执行的函数(消息), 如果找到了,到相应的函数 IMP 去执行.

每个类的结构体包含着两个必备的元素:
[1]. 指向父类的指针
[2]. class dispatch table(调度表). 调度表中包含了 method selectors 和特定 class 相应方法实现的地址.

步骤2. 如果没找到,运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试解析这个消息. 在该方法中, 允许动态添加一个方法的实现.

关于 resolveClassMethod 这个放在另一篇博客中讲解.

步骤3. 如果 resolveXX 返回 YES, 直接结束后面的流程, 执行解析得到的方法. 如果 resolveXX 返回 NO,运行时就发送 forwardingTargetForSelector 消息, 允许这个消息转发给另一个可能实现了对应 selector 的对象.

步骤4. 如果没有新的目标对象返回, 运行时就会发送methodSignatureForSelector 消息, 找到对应的方法签名.
如果找到了, 会接着调用 forwardInvocation, 如果没有找到即返回 nil, 那么直接调用 doesNotRecognizeSelector.

下一篇结合实际例子分析一下整个实例方法的转发流程.

参考文档

1.Apple 开发者文档 Message Forwarding

2.Apple RunTime 源码 objc4-723.tar.gz

推荐

OC-RunTime: 消息转发之实例方法的转发流程实例讲解

OC-RunTime: 消息转发之类方法的转发流程

OC-RunTime: 总结消息转发中用到的知识点

点击下载文中完整的 Demo.

坚持原创技术分享!