iOS Runtime 消息转发机制完全解析
写在前面
在Objective-C的世界里,方法调用并不是像C++那样在编译时就确定要执行的函数地址,而是一个运行时动态绑定的过程。当我们写下 [receiver message] 这样的代码时,编译器实际上会将其转换为 objc_msgSend(receiver, @selector(message)) 的调用。这个 objc_msgSend 函数会负责在接收者所属的类及其父类的方法列表中查找对应的实现并执行。
那么问题来了:如果一直找到根类NSObject都没有找到这个方法的实现,会发生什么?
很多开发者都见过这样的崩溃信息:unrecognized selector sent to instance 0xXXXXXXXX。这正是因为消息发送失败,而Runtime也没有找到合适的方式处理这条消息,最终通过 doesNotRecognizeSelector: 抛出的异常。
但在这个崩溃发生之前,Objective-C的Runtime给了我们三次"拯救"的机会,这就是本文要详细讲解的消息转发机制。
第一章:消息发送机制回顾
在深入探讨消息转发之前,有必要先回顾一下完整的消息发送流程,因为消息转发正是这个流程中处理失败情况的最后保障。
1.1 objc_msgSend的工作流程
当我们向一个对象发送消息时,Runtime系统会按照以下步骤查找方法的实现:
-
检查目标对象是否为nil:如果接收者为nil,Objective-C的特性是忽略该消息,程序不会崩溃(这在很多情况下简化了代码逻辑)。如果为nil且消息有返回值,基本数据类型的返回值为0,对象类型的返回值为nil。
-
查找缓存:每个类都有一个缓存(cache),用于存储最近使用过的方法。Runtime会首先在该类的缓存中查找方法的实现(IMP)。如果找到,直接调用该实现。
-
查找当前类的方法列表:如果在缓存中没有找到,Runtime会从当前类的方法列表中查找。方法列表以数组形式组织,查找过程会遍历整个列表(已排序的列表使用二分查找,否则线性查找)。
-
沿着继承链向上查找:如果在当前类中没有找到,Runtime会沿着继承链逐级向上查找父类的方法列表和缓存,直到根类NSObject为止。
-
动态方法解析:如果一直找到根类都没有找到方法的实现,Runtime会进入"动态方法解析"阶段,给类一个机会动态添加方法的实现。
-
消息转发:如果动态方法解析没有添加实现(或者添加后仍然无法处理),Runtime会进入"消息转发"流程。
-
抛出异常:如果所有转发尝试都失败,最终会调用 doesNotRecognizeSelector: 抛出异常,程序崩溃。
这个流程可以用下面的流程图清晰地展示:
flowchart TD
A[向对象发送消息] --> B{接收者为nil?}
B -->|是| C[忽略消息/返回0/nil]
B -->|否| D[查找缓存]
D --> E{缓存中找到IMP?}
E -->|是| F[调用IMP]
E -->|否| G[在当前类方法列表中查找]
G --> H{当前类中找到?}
H -->|是| F
H -->|否| I[在父类方法列表中查找]
I --> J{父类中找到?}
J -->|是| F
J -->|否| I
J -->|一直查到NSObject仍未找到| K[动态方法解析]
K --> L{动态添加了实现?}
L -->|是| F
L -->|否| M[消息转发流程]
M --> N{转发成功?}
N -->|是| F
N -->|否| O[doesNotRecognizeSelector:\n抛出异常]
1.2 方法的本质:SEL、IMP与Method
要深入理解消息转发,我们需要先了解Objective-C中方法的三个核心概念:
SEL(选择器):是方法的名字,在Runtime中用 objc_selector 结构体表示。在运行时,不同类的同名方法的选择器是相同的。SEL在Runtime中会被唯一化,因此可以使用 == 来比较两个SEL是否相等。
IMP(函数指针):是方法的实现,本质上是一个函数指针,指向方法实现的首地址。它的定义如下:
typedef id (*IMP)(id self, SEL _cmd, ...);
每个IMP都至少包含两个参数:self(消息接收者)和_cmd(这个方法的SEL)。
Method(方法):是用于表示方法定义的结构体,包含三个成员:
struct method_t {
SEL name; // 方法名
const char *types; // 方法类型编码
IMP imp; // 方法实现
}
当我们调用一个方法时,就是从SEL到IMP的映射过程。Runtime维护了每个类的方法列表(method list),这个列表存储了该类定义的所有方法。消息转发机制本质上是在这个映射过程失败后的补救措施。
第二章:消息转发的三个阶段
当消息发送流程无法找到对应的IMP时,Runtime会启动消息转发机制。这个机制分为三个阶段,每个阶段都给开发者一次处理这条"无法识别"的消息的机会。
2.1 第一阶段:动态方法解析
这是消息转发的第一道防线。当Runtime在当前类和父类中都找不到方法的实现时,会首先调用 +resolveInstanceMethod:(对于实例方法)或 +resolveClassMethod:(对于类方法)。
2.1.1 resolveInstanceMethod的工作原理
这个方法的定义如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel
当这个方法被调用时,Runtime给了我们一个机会:可以动态地为这个SEL添加一个实现。如果添加成功并返回YES,Runtime会重新启动消息发送流程,这次就能找到方法的实现了。
这个方法最典型的应用场景是处理 @dynamic 属性。@dynamic 告诉编译器不要自动生成属性的getter和setter方法,我们会在运行时动态提供它们。
2.1.2 实战:动态添加方法实现
让我们通过一个具体的例子来理解这个过程:
#import <objc/runtime.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name; // 注意:我们使用@dynamic
@end
@implementation Person
@dynamic name; // 告诉编译器不要自动生成getter/setter
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(name)) {
// 动态添加getter方法
class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
return YES;
}
else if (sel == @selector(setName:)) {
// 动态添加setter方法
class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// getter方法的实现
id dynamicNameGetter(id self, SEL _cmd) {
// 通过关联对象获取存储的值
return objc_getAssociatedObject(self, @selector(name));
}
// setter方法的实现
void dynamicNameSetter(id self, SEL _cmd, NSString *newName) {
// 通过关联对象存储值
objc_setAssociatedObject(self, @selector(name), newName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
现在,当我们调用:
Person *p = [[Person alloc] init];
[p setName:@"张三"];
NSLog(@"%@", [p name]); // 输出:张三
尽管Person类并没有真正实现name的getter和setter方法,但在消息发送过程中,Runtime调用了 resolveInstanceMethod:,我们动态添加了这两个方法的实现,因此程序能够正常运行。
2.1.3 方法签名的类型编码
在调用 class_addMethod 时,我们需要指定方法的类型编码(types)。这个编码字符串描述了方法的返回类型和参数类型。例如:
-
"v@:" 表示返回void,有两个参数:id和SEL(即标准的实例方法)
-
"@@" 表示返回id,有两个参数:id和SEL(标准的getter方法)
-
"v@:@" 表示返回void,有三个参数:id、SEL和id(标准的setter方法)
完整的类型编码表:
| 编码 |
含义 |
c |
char |
i |
int |
s |
short |
l |
long |
q |
long long |
C |
unsigned char |
I |
unsigned int |
S |
unsigned short |
L |
unsigned long |
Q |
unsigned long long |
f |
float |
d |
double |
B |
BOOL/C++ bool |
v |
void |
* |
char* (字符串) |
@ |
id (对象) |
# |
Class (类对象) |
: |
SEL (选择器) |
^type |
指向type的指针 |
2.1.4 类方法的动态解析
对于类方法,我们需要重写 +resolveClassMethod::
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(classMethod)) {
// 注意:这里添加方法的目标是元类(metaclass)
Class metaClass = objc_getMetaClass(class_getName(self));
class_addMethod(metaClass, sel, (IMP)dynamicClassMethodImp, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
需要注意的是,类方法是存储在元类(metaclass)中的,因此我们需要获取元类来添加类方法的实现。
2.1.5 动态方法解析的时机
动态方法解析发生在消息发送流程失败之后,但在消息转发之前。如果你希望每次调用这个方法时都能走动态解析,注意这个方法只会被调用一次(因为一旦添加了实现,后续调用就能直接找到IMP了)。
2.2 第二阶段:快速消息转发
如果动态方法解析没有添加实现(或者返回NO),Runtime会进入消息转发的第二阶段:快速消息转发。
这个阶段的核心是 forwardingTargetForSelector: 方法。Runtime会调用这个方法,期望它能返回一个能够处理这条消息的对象。
2.2.1 forwardingTargetForSelector的定义
- (id)forwardingTargetForSelector:(SEL)aSelector
这个方法的职责是:当对象无法处理某个消息时,返回一个能够处理该消息的对象。Runtime会将原始消息转发给这个返回的对象,就好像它才是原始的消息接收者一样。
这个机制非常高效,因为它只是简单地改变消息的接收者,不需要创建 NSInvocation 对象,也没有复杂的参数处理。
2.2.2 实战:将消息转发给备用对象
假设我们有一个 Person 类,它不包含 run 方法,但我们有一个 Car 类实现了 run 方法:
@interface Car : NSObject
- (void)run;
@end
@implementation Car
- (void)run {
NSLog(@"Car is running");
}
@end
@interface Person : NSObject
@end
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
// 返回一个可以处理run消息的Car对象
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
现在执行以下代码:
Person *person = [[Person alloc] init];
[person run]; // 输出:Car is running
尽管 Person 对象并没有 run 方法,但通过 forwardingTargetForSelector:,我们将 run 消息转发给了 Car 对象,程序能够正常运行。
2.2.3 模拟多重继承
Objective-C不支持多重继承,但通过快速消息转发,我们可以实现类似多重继承的效果。一个对象可以将自己没有实现的方法转发给其他对象,从外部看就像这个对象继承了多个类的功能。
例如,我们可以创建一个类,它能够处理来自多个不同类的方法:
@interface MultiClass : NSObject
@property (nonatomic, strong) Car *car;
@property (nonatomic, strong) House *house;
@end
@implementation MultiClass
- (instancetype)init {
if (self = [super init]) {
_car = [[Car alloc] init];
_house = [[House alloc] init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([_car respondsToSelector:aSelector]) {
return _car;
} else if ([_house respondsToSelector:aSelector]) {
return _house;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
这样,MultiClass 的实例就能同时响应 Car 和 House 的方法,达到了类似多重继承的效果。
2.2.4 注意事项
使用 forwardingTargetForSelector: 时有几点需要注意:
-
不要返回self:如果在这个方法中返回self,会造成无限循环,因为Runtime会再次尝试向self发送消息。
-
这个方法主要用于转发给其他对象,不适合修改消息本身。
-
返回的对象不必与原始接收者有继承关系,任何对象都可以。
- 如果返回nil或self,则进入下一阶段:完整消息转发。
2.3 第三阶段:完整消息转发
如果前两个阶段都无法处理消息,Runtime会进入最后一个阶段:完整消息转发。这是消息转发机制中最强大、最灵活但也最复杂的阶段。
这个阶段涉及两个方法:
-
methodSignatureForSelector::获取方法的签名(参数类型和返回类型)
-
forwardInvocation::转发封装了消息的 NSInvocation 对象
flowchart TD
A[消息转发第二阶段返回nil] --> B[调用methodSignatureForSelector:]
B --> C{返回有效的方法签名?}
C -->|否| D[调用doesNotRecognizeSelector:\n抛出异常]
C -->|是| E[创建NSInvocation对象]
E --> F[调用forwardInvocation:\n并将NSInvocation传入]
F --> G{在forwardInvocation:中\n处理消息?}
G -->|否| D
G -->|是| H[消息处理成功]
H --> I[将返回值传递给\n原始消息发送者]
2.3.1 methodSignatureForSelector: 的作用
methodSignatureForSelector: 的定义如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
Runtime调用这个方法的目的是获取方法的签名信息,包括方法的返回类型和参数类型。有了这些信息,Runtime才能创建 NSInvocation 对象。
如果这个方法返回nil,Runtime会直接调用 doesNotRecognizeSelector: 并抛出异常,程序崩溃。因此,在实现完整消息转发时,我们必须为无法处理的消息提供一个有效的方法签名。
2.3.2 创建方法签名
方法签名可以通过多种方式创建:
// 方式1:使用字符串创建(类型编码)
NSMethodSignature *signature1 = [NSMethodSignature signatureWithObjCTypes:"v@:"];
// 方式2:从已有方法获取
NSMethodSignature *signature2 = [self methodSignatureForSelector:@selector(existingMethod)];
// 方式3:从协议获取
struct objc_method_description desc = protocol_getMethodDescription(protocol, selector, YES, YES);
NSMethodSignature *signature3 = [NSMethodSignature signatureWithObjCTypes:desc.types];
类型编码字符串的格式和之前 class_addMethod 中使用的格式一致。
2.3.3 forwardInvocation: 的核心作用
forwardInvocation: 的定义如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation
当 methodSignatureForSelector: 返回了有效的方法签名后,Runtime会创建一个 NSInvocation 对象,该对象封装了这条消息的所有信息:
- 消息的目标(target)
- 消息的选择器(selector)
- 所有的参数
- 等待填充的返回值
然后将这个 NSInvocation 对象作为参数传递给 forwardInvocation: 方法。在这个方法中,我们可以:
- 将消息转发给其他对象
- 修改消息的选择器、参数或目标
- 直接处理消息并设置返回值
- 甚至"吃掉"消息,什么都不做(这样就不会崩溃)
2.3.4 实战:完整消息转发的实现
下面是一个完整的示例,演示如何实现完整消息转发:
@interface Person : NSObject
@end
@interface Car : NSObject
- (void)run;
@end
@implementation Car
- (void)run {
NSLog(@"Car is running");
}
@end
@implementation Person
// 第一步:提供方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
// 返回run方法的签名:"v@:" 表示返回void,两个参数:id, SEL
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 第二步:转发调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
// 创建备用对象
Car *car = [[Car alloc] init];
// 检查备用对象是否能响应这个选择器
if ([car respondsToSelector:selector]) {
// 将消息转发给备用对象
[anInvocation invokeWithTarget:car];
} else {
// 如果备用对象也不能处理,调用父类实现(最终会抛出异常)
[super forwardInvocation:anInvocation];
}
}
@end
执行测试代码:
Person *person = [[Person alloc] init];
[person run]; // 输出:Car is running
2.3.5 修改消息内容后转发
完整消息转发的一个强大之处在于,我们可以在转发前修改消息的内容。例如,我们可以修改方法的选择器:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL originalSelector = [anInvocation selector];
if (originalSelector == @selector(run)) {
// 修改选择器为drive
[anInvocation setSelector:@selector(drive)];
Car *car = [[Car alloc] init];
if ([car respondsToSelector:@selector(drive)]) {
[anInvocation invokeWithTarget:car];
return;
}
}
[super forwardInvocation:anInvocation];
}
我们也可以修改参数:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
if (selector == @selector(setAge:)) {
// 获取原始参数
int age;
[anInvocation getArgument:&age atIndex:2]; // 前两个参数是self和_cmd
// 修改参数值(例如:限制年龄范围)
if (age < 0) age = 0;
if (age > 150) age = 150;
// 设置修改后的参数
[anInvocation setArgument:&age atIndex:2];
}
// 转发给实际处理的对象
if ([_realObject respondsToSelector:selector]) {
[anInvocation invokeWithTarget:_realObject];
} else {
[super forwardInvocation:anInvocation];
}
}
2.3.6 处理返回值
NSInvocation 也能处理返回值。我们可以从 anInvocation 中获取返回值,修改它,或者设置自己的返回值:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 先尝试转发给备用对象
if ([_backup respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:_backup];
// 获取返回值
char returnType[10];
strcpy(returnType, [[anInvocation methodSignature] methodReturnType]);
if (returnType[0] == '@') { // 返回对象类型
id result = nil;
[anInvocation getReturnValue:&result];
// 可以修改返回值
if (result == nil) {
result = @"Default Value";
[anInvocation setReturnValue:&result];
}
}
return;
}
[super forwardInvocation:anInvocation];
}
2.3.7 转发给多个对象
完整消息转发甚至可以将一个消息转发给多个对象。这在某些设计模式中很有用,例如观察者模式或责任链模式:
@interface MessageChain : NSObject
@property (nonatomic, strong) NSArray *handlers;
@end
@implementation MessageChain
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
BOOL handled = NO;
for (id handler in self.handlers) {
if ([handler respondsToSelector:selector]) {
[anInvocation invokeWithTarget:handler];
handled = YES;
// 可以选择是否继续转发给下一个处理器
// break;
}
}
if (!handled) {
[super forwardInvocation:anInvocation];
}
}
@end
2.4 三个阶段的关系与选择
这三个阶段是递进的关系:如果第一阶段处理了,第二阶段就不会触发;如果第二阶段处理了,第三阶段就不会触发。
选择使用哪个阶段取决于你的需求:
-
动态方法解析:适合在运行时动态添加方法实现,例如处理
@dynamic 属性、实现轻量级的代理模式。
-
快速消息转发:适合简单地将消息转发给另一个对象,性能最好,但不能修改消息内容。
-
完整消息转发:最强大、最灵活,可以修改消息内容、参数、返回值,甚至可以将消息转发给多个对象,但性能开销也最大。
第三章:深入源码分析
了解理论之后,让我们深入Runtime的源码,看看消息转发机制究竟是如何实现的。这里我们基于苹果开源的objc4源码进行分析。
3.1 从消息发送到消息转发的转折点
在 objc_msgSend 的核心实现中,如果方法查找失败,会调用 lookUpImpOrForward 函数。这个函数的简化逻辑如下:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver) {
IMP imp = nil;
bool triedResolver = NO;
// 尝试从缓存和方法列表中查找
// ...
// 如果没有找到实现
if (resolver && !triedResolver) {
// 调用动态方法解析
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
// 重新尝试查找
goto retry;
}
// 动态解析失败,返回转发IMP
imp = (IMP)_objc_msgForward_impcache;
return imp;
}
关键点在于:当动态方法解析失败后,lookUpImpOrForward 会返回一个特殊的IMP:_objc_msgForward_impcache。这个IMP指向的是消息转发的入口函数。
3.2 消息转发的入口:__objc_msgForward
_objc_msgForward_impcache 最终会调用到 __objc_msgForward 函数。在x86_64架构的汇编实现中,这个函数的逻辑大致是:
ENTRY __objc_msgForward
// 跳转到消息转发的核心实现
jmp __objc_forward_handler
END_ENTRY __objc_msgForward
而 __objc_forward_handler 是一个C函数,它会调用到CoreFoundation框架中的 __forwarding__ 函数。这就是消息转发的真正核心实现。
3.3 CoreFoundation中的__forwarding__函数
__forwarding__ 函数是消息转发机制的心脏。虽然苹果没有开源CoreFoundation的全部代码,但通过反汇编和分析,我们可以还原其大致逻辑:
int __forwarding__(void *frameStackPointer, int isStret) {
// 获取消息的接收者和选择器
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + sizeof(id));
// 尝试快速转发
id forwardingTarget = nil;
if ([receiver respondsToSelector:@selector(forwardingTargetForSelector:)]) {
forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget != nil && forwardingTarget != receiver) {
// 转发给目标对象
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 快速转发失败,尝试完整转发
NSMethodSignature *signature = nil;
if ([receiver respondsToSelector:@selector(methodSignatureForSelector:)]) {
signature = [receiver methodSignatureForSelector:sel];
}
if (signature == nil) {
// 没有方法签名,无法继续
[receiver doesNotRecognizeSelector:sel];
return 0;
}
// 创建NSInvocation对象
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:frameStackPointer];
// 调用forwardInvocation:
if ([receiver respondsToSelector:@selector(forwardInvocation:)]) {
[receiver forwardInvocation:invocation];
} else {
[receiver doesNotRecognizeSelector:sel];
}
// 获取返回值
// ...
return 0;
}
从这个伪代码可以看出,__forwarding__ 函数完整地实现了我们之前讨论的消息转发流程:
- 尝试快速转发
- 如果快速转发没有返回合适的对象,尝试获取方法签名
- 如果方法签名有效,创建
NSInvocation 并调用 forwardInvocation:
- 如果所有步骤都失败,调用
doesNotRecognizeSelector: 抛出异常
3.4 日志调试技巧
Runtime提供了一个调试函数 instrumentObjcMessageSends,可以让我们查看消息发送和转发的详细过程:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 开启消息发送日志
instrumentObjcMessageSends(YES);
Person *person = [[Person alloc] init];
[person run];
// 关闭日志
instrumentObjcMessageSends(NO);
}
return 0;
}
运行程序后,在 /tmp/msgSends- 目录下会生成日志文件,内容类似于:
+ Person NSObject initialize
+ Person NSObject new
- Person NSObject init
- Person forwardingTargetForSelector: run
- Person methodSignatureForSelector: run
- Person forwardInvocation:
- Person doesNotRecognizeSelector: run
通过这个日志,我们可以清楚地看到消息转发的每一步调用过程,对于理解和调试消息转发非常有帮助。
第四章:消息转发的应用场景
消息转发机制不仅仅是理论上的知识点,它在实际开发中有很多实用的应用场景。
4.1 防止崩溃:安全的消息调用
一个常见的应用场景是防止因为调用未实现方法而导致的崩溃。例如,我们可以创建一个安全的代理对象,当目标对象不能响应某个消息时,不是崩溃而是返回一个默认值:
@interface SafeProxy : NSObject
@property (nonatomic, weak) id target;
@end
@implementation SafeProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 如果target可以响应,直接转发
if ([_target respondsToSelector:aSelector]) {
return _target;
}
return self; // 让完整转发来处理
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 为任何方法提供默认签名(返回对象类型)
return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 不处理消息,只设置返回值为nil
id nilValue = nil;
[anInvocation setReturnValue:&nilValue];
}
@end
使用这个SafeProxy,我们可以安全地调用任何方法:
Person *person = [[Person alloc] init];
SafeProxy *proxy = [[SafeProxy alloc] init];
proxy.target = person;
// 如果person实现了run方法,正常执行
[proxy run];
// 如果person没有实现fly方法,不会崩溃,而是返回nil
id result = [proxy fly]; // result = nil,没有崩溃
4.2 模拟多继承
如前所述,通过消息转发可以实现类似多继承的效果。这在某些设计模式中非常有用,例如"装饰器"模式或"代理"模式。
4.3 API兼容性处理
在开发中,我们经常会遇到iOS系统版本升级导致API变化的情况。通过消息转发,我们可以优雅地处理这种变化:
@interface CompatibilityHandler : NSObject
@end
@implementation CompatibilityHandler
+ (void)load {
// 交换forwardInvocation:方法
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIDevice class];
SEL originalSelector = @selector(forwardInvocation:);
SEL swizzledSelector = @selector(compatibility_forwardInvocation:);
// 方法交换的实现...
});
}
- (void)compatibility_forwardInvocation:(NSInvocation *)invocation {
SEL selector = [invocation selector];
if (selector == @selector(isLowPowerModeEnabled)) {
// 低电量模式是iOS 9.0引入的
if (@available(iOS 9.0, *)) {
// 如果系统支持,转发给原始实现
[invocation invoke];
} else {
// 如果不支持,返回默认值NO
BOOL defaultValue = NO;
[invocation setReturnValue:&defaultValue];
}
} else {
// 其他消息正常转发
[self compatibility_forwardInvocation:invocation];
}
}
@end
4.4 实现AOP(面向切面编程)
通过消息转发,我们可以实现简单的AOP编程,在不修改原有类的情况下添加额外的逻辑:
@interface AspectProxy : NSObject
@property (nonatomic, strong) id target;
@property (nonatomic, copy) void (^beforeBlock)(SEL);
@property (nonatomic, copy) void (^afterBlock)(SEL);
@end
@implementation AspectProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 必须返回nil才能进入完整转发
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [_target methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
// 执行前置逻辑
if (_beforeBlock) {
_beforeBlock(selector);
}
// 转发给目标对象
if ([_target respondsToSelector:selector]) {
[anInvocation invokeWithTarget:_target];
}
// 执行后置逻辑
if (_afterBlock) {
_afterBlock(selector);
}
}
@end
4.5 实现动态代理
在RxSwift等响应式编程框架中,消息转发被广泛用于实现动态代理,拦截方法调用并将它们转换为信号流:
// RxSwift中拦截方法的简化实现
@interface RXMessageSentObserver : NSObject
// ...
@end
@implementation _RXObjCRuntime
- (void)interceptMethod:(SEL)selector ofClass:(Class)cls {
// 1. 创建子类
// 2. 重写forwardInvocation:
// 3. 在forwardInvocation:中创建信号
}
@end
4.6 JSPatch等热修复框架的实现原理
热修复框架如JSPatch利用消息转发机制来实现动态替换OC方法的实现。基本原理是:
- 将要修复的类的
forwardInvocation: 方法替换为自己的实现
- 将原方法的IMP指向
_objc_msgForward,强制进入消息转发流程
- 在
forwardInvocation: 中,执行JavaScript代码
第五章:性能考量与最佳实践
消息转发机制虽然强大,但使用不当可能会带来性能问题。
5.1 性能开销分析
不同阶段的消息转发性能开销不同:
| 阶段 |
性能开销 |
主要原因 |
| 正常消息发送 |
极小 |
直接查找IMP并调用 |
| 动态方法解析 |
较小 |
只执行一次,后续调用正常 |
| 快速消息转发 |
中等 |
需要调用Cocoa方法,但流程简单 |
| 完整消息转发 |
较大 |
需要创建NSInvocation对象,处理参数和返回值 |
为什么完整消息转发开销大:
- 需要调用
methodSignatureForSelector: 获取方法签名
- Runtime需要根据方法签名创建
NSInvocation 对象
-
NSInvocation 需要拷贝参数和设置返回值
- 整个流程涉及多次Objective-C方法调用
5.2 性能优化建议
根据性能开销,我们应遵循以下最佳实践:
-
优先使用快速消息转发:如果只是简单地将消息转发给另一个对象,尽量使用 forwardingTargetForSelector:,避免使用完整转发。
-
缓存方法签名:如果在完整转发中经常处理同一类消息,可以缓存方法签名,避免每次调用 methodSignatureForSelector::
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
static NSMutableDictionary *signatureCache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
signatureCache = [NSMutableDictionary dictionary];
});
NSString *selString = NSStringFromSelector(aSelector);
NSMethodSignature *signature = signatureCache[selString];
if (!signature) {
signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
signatureCache[selString] = signature;
}
return signature;
}
-
避免频繁触发转发:如果一个方法经常被调用,最好不要依赖消息转发来处理它。考虑在
resolveInstanceMethod: 中动态添加IMP,这样后续调用就和正常方法一样快了。
5.3 调试消息转发
当遇到与消息转发相关的bug时,可以使用以下调试技巧:
-
使用instrumentObjcMessageSends:开启日志,查看消息转发的每一步。
-
添加日志输出:在转发方法中添加日志:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"📱 Forwarding %@ to another target", NSStringFromSelector(aSelector));
// ...
}
-
使用断点调试:在 forwardInvocation: 中设置断点,检查 NSInvocation 的内容。
-
检查方法签名:常见的崩溃原因是 methodSignatureForSelector: 返回了不正确的签名。可以使用以下代码验证签名:
NSMethodSignature *sig = [self methodSignatureForSelector:@selector(someMethod:)];
NSLog(@"Signature: %s", [sig methodReturnType]); // 检查返回类型
for (NSUInteger i = 0; i < [sig numberOfArguments]; i++) {
NSLog(@"Arg %lu: %s", i, [sig getArgumentTypeAtIndex:i]);
}
5.4 与其他动态特性的比较
消息转发与Objective-C的其他动态特性既有联系又有区别:
| 特性 |
目的 |
触发时机 |
| 消息转发 |
处理无法识别的消息 |
方法查找失败后 |
| 方法交换 |
交换两个方法的IMP |
运行时主动执行 |
| 动态添加方法 |
为类添加新方法 |
运行时主动执行 |
| KVO |
监听属性变化 |
创建子类并重写setter |
重要区别:
- 消息转发是被动的,只有在正常消息发送失败后才会触发
- 方法交换、动态添加方法是主动的,我们可以在任何时候执行
- KVO是利用Runtime创建子类并重写方法,本质上也是动态特性的一种应用
第六章:面试深度解析
消息转发是iOS面试中的高级话题。下面梳理一些常见的面试题和深度解析。
6.1 基础问题
Q1:OC中给nil对象发送消息会发生什么?
解析:给nil发送消息是安全的,不会崩溃。Runtime在 objc_msgSend 中会首先检查接收者是否为nil,如果是nil,直接返回。返回值的类型取决于方法声明的返回类型:
- 如果返回对象类型,返回nil
- 如果返回整型,返回0
- 如果返回结构体,返回的结构体各字段都是0
- 如果返回浮点类型,返回0.0
Q2:unrecognized selector sent to instance 这个异常是怎么产生的?
解析:当向一个对象发送它无法处理的消息,且消息转发机制也无法处理时,Runtime最终会调用 doesNotRecognizeSelector: 方法。NSObject 中该方法的默认实现就是抛出这个异常。也就是说,这个异常是消息转发流程失败的最后结果。
Q3:消息转发分哪几个阶段?每个阶段的作用是什么?
解析:消息转发分为三个阶段:
-
动态方法解析:调用 resolveInstanceMethod:/resolveClassMethod:,允许开发者动态添加方法实现。
-
快速消息转发:调用 forwardingTargetForSelector:,允许将消息转发给另一个对象。
-
完整消息转发:调用 methodSignatureForSelector: 获取方法签名,然后创建 NSInvocation 对象并调用 forwardInvocation:,允许修改消息内容或转发给多个对象。
6.2 进阶问题
Q4:如何在运行时动态添加方法?
解析:在 resolveInstanceMethod: 中使用 class_addMethod 函数:
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态添加的方法被调用");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
Q5:快速转发和完整转发有什么区别?如何选择?
解析:主要区别在于:
-
需要重载的方法数量:快速转发只需重载 forwardingTargetForSelector:,完整转发需要重载 methodSignatureForSelector: 和 forwardInvocation: 两个方法。
-
功能强大程度:快速转发只能简单地改变消息接收者,不能修改消息内容;完整转发可以修改消息的参数、选择器、返回值等。
-
性能开销:快速转发性能更好,完整转发需要创建 NSInvocation 对象,开销较大。
选择建议:
- 如果只是想将消息转发给另一个对象,且不需要修改消息内容,优先使用快速转发
- 如果需要修改消息内容、参数、返回值,或者需要将消息转发给多个对象,使用完整转发
Q6:消息转发可以用来实现多重继承吗?和真正的多重继承有什么区别?
解析:可以通过消息转发实现类似多重继承的效果。区别在于:
- 真正的多重继承是将多个类的功能合并到一个对象中
- 通过消息转发实现的"伪多继承",功能仍然分散在不同的对象中,只是通过转发机制让外部看起来像一个对象处理了所有消息
Q7:如果消息转发的方法本身也找不到实现会怎样?
解析:这是一个容易忽略的细节。如果消息转发的方法(如 forwardingTargetForSelector:)本身没有实现,Runtime也会按照同样的流程查找它的实现。如果找不到,同样会触发消息转发。但通常情况下,这些方法在 NSObject 中都有默认实现,所以不会出现这种情况。
Q8:如何调试消息转发过程?
解析:可以使用以下方法:
- 使用
instrumentObjcMessageSends(YES) 开启日志
- 查看
/tmp/msgSends- 目录下的日志文件
- 在转发方法中添加断点和日志输出
- 使用反汇编工具分析
__forwarding__ 函数
6.3 高级问题
Q9:消息转发和method swizzling有什么关系?能结合使用吗?
解析:消息转发和method swizzling是两种不同的动态特性,但可以结合使用。例如,可以实现一个通用的方法拦截机制:
// 1. 先将原方法的IMP替换为_objc_msgForward
Method method = class_getInstanceMethod(cls, originalSelector);
method_setImplementation(method, _objc_msgForward);
// 2. 再添加一个转发方法
class_addMethod(cls, @selector(customForward:), (IMP)customForwardIMP, "v@:@");
// 3. 交换forwardInvocation:方法
Method originalForwardMethod = class_getInstanceMethod(cls, @selector(forwardInvocation:));
Method swizzledForwardMethod = class_getInstanceMethod(cls, @selector(customForwardInvocation:));
method_exchangeImplementations(originalForwardMethod, swizzledForwardMethod);
这种技术被用于RxSwift等框架的方法拦截功能。
Q10:如何实现一个通用的消息转发中心,能够记录所有无法识别的消息?
解析:可以创建一个基类,所有需要日志功能的类都继承自这个基类:
@interface LoggingBase : NSObject
@property (nonatomic, strong) NSMutableArray *unrecognizedMessages;
@end
@implementation LoggingBase
- (instancetype)init {
if (self = [super init]) {
_unrecognizedMessages = [NSMutableArray array];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 记录无法识别的消息
NSString *message = [NSString stringWithFormat:@"%@: %@", self, NSStringFromSelector(aSelector)];
[_unrecognizedMessages addObject:message];
// 可以选择转发给默认处理对象
return [DefaultHandler sharedHandler];
}
// 可以添加一个方法来导出日志
- (void)exportUnrecognizedMessages {
NSLog(@"Unrecognized messages: %@", _unrecognizedMessages);
}
@end
Q11:消息转发机制在ARC下有什么特别需要注意的地方?
解析:ARC下使用消息转发时需要注意:
-
内存管理:在 forwardInvocation: 中处理对象参数时,ARC会自动处理内存管理,但要注意不要造成循环引用。
-
方法签名:方法签名的类型编码必须准确,特别是在有对象参数或返回值时。错误的类型编码可能导致ARC下的内存管理错误。
-
返回值处理:当从 forwardInvocation: 返回时,Runtime会根据方法签名自动处理返回值的retain/release。如果方法签名不准确,可能导致内存泄漏或崩溃。
-
使用 __unsafe_unretained:在某些情况下,可能需要使用 __unsafe_unretained 来避免ARC自动插入的retain/release操作干扰转发逻辑。
Q12:从源码层面分析,消息转发和消息发送的性能差异主要体现在哪些方面?
解析:从源码层面看,性能差异主要体现在:
-
正常消息发送:汇编实现,查找缓存后直接跳转,几条指令就能完成。
-
动态方法解析:需要调用Objective-C方法,但只执行一次,后续调用恢复正常。
-
快速转发:需要调用 forwardingTargetForSelector:,这是一个完整的Objective-C方法调用,涉及消息发送流程。但无需创建复杂的对象。
-
完整转发:
- 需要调用
methodSignatureForSelector: 获取签名
- Runtime需要遍历方法签名,解析每个参数的类型
- 创建
NSInvocation 对象需要分配内存
-
NSInvocation 需要拷贝参数值
- 调用
forwardInvocation: 方法
- 转发后需要处理返回值
这些步骤加起来,完整转发的性能开销可能是正常消息发送的几十倍甚至上百倍。
第七章:总结与展望
7.1 消息转发机制的核心价值
Objective-C的消息转发机制是其动态性的集中体现,它给了开发者三次机会来处理无法识别的消息:
-
动态方法解析:让我们能够在运行时动态添加方法实现
-
快速消息转发:让我们能够将消息简单地转发给其他对象
-
完整消息转发:让我们能够完全掌控消息的处理过程
这三次机会形成了一个从简单到复杂的递进结构,开发者可以根据需求选择合适的层次进行干预。
7.2 设计思想解读
消息转发机制的设计体现了几个重要的软件工程思想:
-
容错性:系统提供了容错机制,允许程序在出现问题时尝试恢复,而不是直接崩溃。
-
渐进式干预:提供了三个层次的干预机会,每个层次都有不同的复杂度和能力,开发者可以根据需要选择。
-
开闭原则:通过消息转发,我们可以在不修改原有类的情况下,扩展类的功能,符合开闭原则。
-
责任链模式:消息转发本质上是一个责任链模式的实现,每个阶段都有机会处理消息,如果处理不了就传递给下一阶段。
7.3 与其他语言动态特性的对比
与其他动态语言相比,Objective-C的消息转发机制有独特之处:
| 语言 |
类似特性 |
特点 |
| Objective-C |
消息转发 |
分三个阶段,功能强大,与Runtime紧密结合 |
| Ruby |
method_missing |
类似forwardInvocation:,但更简洁 |
| Python |
getattr |
属性访问的fallback机制 |
| JavaScript |
Proxy |
可以拦截对象的各种操作 |
其中,Ruby的 method_missing 和Objective-C的 forwardInvocation: 最为相似。不同之处在于,Objective-C提供了更细粒度的控制(三个阶段),而Ruby只提供了一个统一的入口。
7.4 未来展望
随着Swift的兴起,Objective-C的使用场景在减少,但消息转发机制的设计思想仍然值得学习:
-
Swift的动态特性:Swift虽然强调静态类型安全,但也提供了反射机制和 @objc 动态特性。理解消息转发有助于理解Swift中与Objective-C交互的部分。
-
跨平台开发:像Flutter这样的跨平台框架,在实现平台通道时也借鉴了消息转发的思想。
-
AOP编程:面向切面编程在现代开发中越来越重要,消息转发是实现AOP的基础技术之一。
7.5 最后的思考
消息转发机制是Objective-C Runtime皇冠上的明珠,它展示了动态语言的强大能力。掌握消息转发,不仅能帮助我们写出更健壮的代码,还能让我们更深入地理解Objective-C的设计哲学。
在实际开发中,我们应当合理使用消息转发机制:
- 在需要的地方使用,但不要滥用
- 优先考虑性能更好的方案(如快速转发优先于完整转发)
- 做好日志和调试,确保转发逻辑正确
最终,消息转发机制体现了编程语言设计中的一个重要思想:给予开发者更多的控制权,同时也赋予更多的责任。当我们决定使用消息转发时,我们实际上是在说:"我知道这条消息可能无法被正常处理,但我有办法解决这个问题。"
这种思想超越了具体的编程语言,是每个优秀程序员都应该具备的能力——在系统无法自动处理的情况发生时,能够提供优雅的降级方案。
参考资料
- Apple官方文档:forwardInvocation:
- Objective-C Runtime源码 (objc4-818.2)
- 《Effective Objective-C 2.0》 - Matt Galloway
- 《Objective-C Runtime Programming Guide》 - Apple Inc.