# Objective-C消息转发机制

[相关demo](https://github.com/jsonwang/iOS-BasicKnowledge.git)

在看消息转发之前,我们先看一段代码,调用一个 nil 对象方法会咋样?

```
//Person.h
@interface Person : NSObject

//年龄
- (int)age;

@end

//Person.m
@implementation Person

- (int)age
{
    return 18;

}
@end

//调用地方
Person * p = nil;
NSLog(@"my age is :%d",[p age]);
```

要想几个问题,1,会 crash 吗? 为什么,2, log出来的值是18吗? 答案就是不会 crash 因为OC的函数调用都是通过objc\_msgSend进行消息发送来实现的，相对于C和C++来说，对于空指针的操作会引起Crash的问题，而objc\_msgSend会通过判断self来决定是否发送消息，如果self为nil，那么selector也会为空，直接返回，所以不会出现问题。视方法返回值，向nil发消息可能会返回nil(返回值为对象)、0（返回值为一些基础数据类型）或0X0（返回值为id）等,同时不能输出18. 使用clang -rewrite-objc xxxx.m 可以把一个类编译.

在如下代码,在MRC下创建别一个 p2 对象

```
Person * p2 = [[Person alloc] init];
[p2 release];

//PS:XXXXXX 对一个object做了release之后，这个object的引用计数会立即减1，但这个object并不一定就立即被free了。直到其引用计数变成0的时候，它才可能真正被free掉在没 free 之前 调用age前加一句log 就是等free了再调用 age 才会crash. 使用 sleep(10)效果是一样的,是不是有同学有时候加一句log后就有不同的运行结果可以查看是不是同样问题
NSLog(@"p2 count %ld",[p2 retainCount]);
NSLog(@"my age is :%d",[p2 age]);
```

猜一下会咋样?会看到

```
Thread 1: EXC_BAD_ACCESS (code=1, address=0x10
```

what? why? どういう意味ですか? 原因:一个对象已经被释放了,那么这个时候再去调用方法肯定是会Crash的，因为这个时候这个对象就是一个野指针（指向僵尸对象的指针）了，安全的做法是释放后将对象重新置为nil，使它成为一个空指针即 p2 = nil;

```
//3,调用一个定义了但没有实现的方法
Person * p3 = [Person new];
[p3 run];
```

运行后会出现这个错误

```
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x101555eb0'
*** First throw call stack:
```

run方法在调用时，系统会查看这个对象能否接收这个消息（没有实现这个方法），如果不能接收，就会调用下面这几个方法，给你“补救”的机会，你可以先理解为几套防止程序crash的备选方案，我们就是利用这几个方案进行消息转发，注意一点，前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理，那么程序就会crash。 让程序不 crash 的方法 要用到 oc 的 消息转发机制 (message forwarding) ![](/files/-LAdZW4CE-ToFtLXSvsD)

## 方案一 动态方法解析 ：

```
////////////////////////////////////////////////////////////////////
//1.1动态方法解析 类方法
+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"%s",__func__);
    if (sel == @selector(number))
    {
        return class_addMethod(object_getClass(self), sel, (IMP)number, "v@:");
    }
    return [super resolveClassMethod:sel];
}

void number(id self,SEL _cmd)
{
    NSLog(@"类方法: %s",__func__);

}

//1.2 动态方法解析 对象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"%s",__func__);
    if (sel == @selector(run))
    {
        return class_addMethod([self class], sel, (IMP)run, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

void run(id self,SEL _cmd)
{
    NSLog(@"对象方法: %s",__func__);
}
////////////////////////////////////////////////////////////////////

调用方法
Person * p3 = [Person new];
[p3 run];
[Person number];
```

class\_addMethod 方法说明

```
class_addMethodBOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types),参数说明:

cls:要添加方法的类
name:选择器
imp:方法实现,IMP在objc.h中的定义是：typedef id (*IMP)(id, SEL, ...);该方法至少有两个参数,self(id)和_cmd(SEL)
types:方法,参数和返回值的描述,"v@:"表示返回值为void,没有参数
```

[types 参考表](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html),如果你想让该方法选择器被传送到转发机制，那么就让resolveInstanceMethod:返回NO

## 方案二：消息重定向,

这个方案可以把消息转给其他对象, 换个说法就是人类处理不了这方法,让猴子类(其它类)来处理这个方法,有同学是不是想问猴子类也没有实现这个 run 方法咋办,代码会 crash 给你看

```
//Monkey.m
@implementation Monkey

- (void)run
{
    NSLog(@"猴子跑起来.");

}
@end

//Person.m 中添加如下方法 并注释上resolveInstanceMethod方法或返回 NO
//2 消息重定向,让别的类开处理这个消息
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"消息重定向 %s",__func__);
    if (aSelector == @selector(run))
    {
        return [Monkey new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
```

这样把消息重定向给Monkey classe 来处理 程序就不会 crash 了

## 方案三：消息转发

也是改变调用对象,使该消息在新对象上调用;不同是forwardInvocation方法带有一个NSInvocation对象,这个对象保存了这个方法调用的所有信息,包括SEL，参数和返回值描述等,[JSPatch](https://github.com/bang590/JSPatch)就是基于消息转发实现的,这一步需要实现两个方法:

```
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"消息转发:%s",__func__);
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"消息转发:%s",__func__);
    if (anInvocation.selector == @selector(run))
    {
        [anInvocation invokeWithTarget:[Monkey new]];
        return;
    }
    [super forwardInvocation:anInvocation];
}
```

小结

* 使用场景比如在 crash 之前 弹出一个aleart"数据出现异常需要重启 APP" 比崩用户一脸体验好一些.
* 当接收无法处理的 selector 时，则进入消息转发流程
* 消息转发流程可分为两阶段，一共有 3 次机会来处理未知的 selector

  第一阶段为 动态方法解析 阶段，用来为类动态添加方法，第二阶段才是正在的消息转发阶段，该阶段可以将未知的 selector 转发到一个或者多个对象中来处理
* 消息转发流程完成后，都不做任何处理的话，这进入 doesNotRecognizeSelector 方法从而抛出异常
* 如果将消息转发到其他对象来处理，则需要重写 respondsToSelector 方法来保证该方法正常工作
* NSProxy 类是基于消息转发机制来实现的动态代理模式
* 消息转发机制可用来实现 @dynamic 属性、代理模式、多重继承等

其它资料:

* [runtime源码](https://opensource.apple.com/source/objc4/)
* [JSPatch-实现原理详解](https://github.com/bang590/JSPatch/wiki/JSPatch-实现原理详解)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://philm.gitbook.io/philm-ios-wiki/mei-zhou-yue-du/objectivec-xiao-xi-zhuan-fa-ji-zhi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
