# 13.动态决议与消息转发

### 前言

在上一文中我们有探究到`objc_msgSend`相关的一些底层原理。

* 我们发现在慢速查找的过程中有这样
  * forward\_imp 消息转发
  * resolveMethod\_locked 动态决议
* 本文就将对这两个地方做深入探究

```
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    
    ...

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            ...
        } else {
            ...
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        ...
    }

    // No implementation found. Try method resolver once.
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

    ...

 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
```

#### 先附上一张流程图

* 仅从源码分析会很困难，结合代码调试各种情况，和这张流程图可以更好的理解整个流程的细节
* 我自己也反复调试了很多次才理的差不多，以前知道这些东西，但没有深入底层去理解细节
* 这张图也改了好几次

![动态决议与消息转发](/files/-Me0ErvxpisqaPpvo4Jr)

### 一、 \_objc\_msgForward\_impcache

通过在源码`objc-msg-arm64.s`中搜索我们一步步找到核心代码

***\_\_objc\_msgForward\_impcache***

```
STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
```

***\_\_objc\_msgForward***

```
ENTRY __objc_msgForward

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

END_ENTRY __objc_msgForward
```

***\_\_objc\_forward\_handler***

* 我们并没有找到 `__objc_forward_handler`
  * 怀疑不是汇编代码，去掉`_`试试

***objc\_defaultForwardHandler***

找到了下面的代码。

```
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    /**
    打印一些信息
    这里就有我们常看到的 unrecognized selector 的相关崩溃新系了
    */ 
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
```

### 二、 resolveMethod\_locked

```
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    ...

    if (! cls->isMetaClass()) {
        // 对象方法（cls不是元类），理解ISA关系图这里就会比较清楚的
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // 类方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
```

### 三、实例方法动态决议

这里声明一个实例方法，但不做实现。触发调用

```
- (void)noIMP;

[obj noIMP];
```

会见到我们常见的一个崩溃信息：

```
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RYModel noIMP]: unrecognized selector sent to instance 0x100677a40'
terminating with uncaught exception of type NSException
```

#### 3.1 动态添加Method

重写 `+ (BOOL)resolveInstanceMethod:(SEL)sel` 添加`method`

```
- (void)callNoIMPSel {
    NSLog(@"调用了没IMP的SEL");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(noIMP)) {
        IMP safeIMP = class_getMethodImplementation(self, @selector(callNoIMPSel));
        Method method =  class_getInstanceMethod(self, @selector(callNoIMPSel));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"对%@进行动态决议", NSStringFromSelector(sel));
        return class_addMethod(self, sel, safeIMP, type);
    }
    return [super resolveInstanceMethod:sel];
}
```

输出：

```
对noIMP进行动态决议
调用了没IMP的SEL
```

#### 3.2 如果再次调用，还会不会进行动态决议呢？

理论分析：第一次添加了Method后，会同步插入到方法缓存中，所以第二次执行就不会再次添加。

实际运行结果也验证了刚才的分析。

```
对noIMP进行动态决议
调用了没IMP的SEL
调用了没IMP的SEL
```

#### 3.3 源码解析

```
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    ...
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    ...

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 直接调用 objc_msgSend 调用 resolveInstanceMethod:
    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 = lookUpImpOrNilTryCache(inst, sel, cls);

        if (resolved  &&  PrintResolving) {
            if (imp) {
                ...
            }
            else {
                // Method resolver didn't add anything?
                ...
            }
        }
}
```

```
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    ...

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    。。。
#endif
    if (slowpath(imp == NULL)) {
        // 再查找
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        // 需要转发
        return nil;
    }
    return imp;
}
```

### 四、类方法动态决议

#### 4.1 动态添加Method

```
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(noClassIMP)) {
        IMP safeIMP = class_getMethodImplementation(self, @selector(callClassNoIMPSel));
        Method method =  class_getClassMethod(self, @selector(callClassNoIMPSel));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"对%@进行动态决议（类方法）", NSStringFromSelector(sel));
        return class_addMethod(self, sel, safeIMP, type);
    }
    return [super resolveClassMethod:sel];
}
```

* 如果你写成上面的样子，你会发现依旧会崩溃。根本没生效。
* 为什么呢？
* 如果你不知道为什么，你需要首先搞明白 `类方法存在哪里`
  * [深入探索Class的结构](/wiki/ios/di-ceng/05.-shen-ru-tan-suo-class-de-jie-gou.md)这篇文章可以带你了解

正确的实现方式：

```
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(noClassIMP)) {
        Class meta = objc_getMetaClass("RYModel");
        IMP safeIMP = class_getMethodImplementation(meta, @selector(callClassNoIMPSel));
        Method method =  class_getClassMethod(meta, @selector(callClassNoIMPSel));
        const char *type = method_getTypeEncoding(method);
        NSLog(@"对%@进行动态决议（类方法）", NSStringFromSelector(sel));
        return class_addMethod(meta, sel, safeIMP, type);
    }
    return [super resolveClassMethod:sel];
}
```

输出：

```
对noClassIMP进行动态决议（类方法）
调用了Class没IMP的SEL
```

#### 4.2 类方法动态决议的特殊处理

在源码中及流程图中可以发现，在 `resolveClassMethod` 之后如果缓存中还没有对应的`Method`的话就会再去调用 `resolveInstanceMethod`。

```
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    resolveInstanceMethod(inst, sel, cls);
}
```

> 可见苹果工程师也是操碎了心啊，给了好多机会来让我们不要崩溃

### 五、消息转发

在 `lookUpImpOrForward` 中我们找到 `_objc_msgForward_impcache`

```
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    ...
}
```

#### 5.1 寻找切入点

发现其实现在 `objc-msg-arm64.s` 中

***\_\_objc\_msgForward\_impcache***

```
STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
```

***\_\_objc\_msgForward***

```
ENTRY __objc_msgForward

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

END_ENTRY __objc_msgForward
```

***\_objc\_forward\_handler***

* 我们有进一步找到了 `_objc_forward_handler`
  * 拥有一个默认的实现： `objc_defaultForwardHandler`
  * 以及一个`set`方法：`objc_setForwardHandler`

```
#if !__OBJC2__

...

#else
    // Default forward handler halts the process.
    __attribute__((noreturn, cold)) void
    objc_defaultForwardHandler(id self, SEL sel)
    {
        _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                    "(no message forward handler is installed)", 
                    class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                    object_getClassName(self), sel_getName(sel), self);
    }
    void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

    #if SUPPORT_STRET (ARM64 = 0)
    ...
    #endif

#endif

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}
```

进一步搜索 `setForwardHandler` 并未发现相关调用，推测调用处不在OC源码中。

![消息转发01](/files/-Me0Erw2avbz1Y0pxQpX)

***分析Log***

发现调用了 `CoreFoundation` 中的 `___forwarding___` 方法。

```
*** First throw call stack:
(
    ...
	3   CoreFoundation                      0x00007fff203f790b ___forwarding___ + 1448
	4   CoreFoundation                      0x00007fff203f72d8 _CF_forwarding_prep_0 + 120
    ...
)
```

那么怎么去进一步分析呢？

#### 5.2 汇编分析 CoreFoundation\`***forwarding***:

**forwardingTargetForSelector:**

`0x7fff203f741b <+184>: mov r14, qword ptr [rip + 0x604ed906] ; "forwardingTargetForSelector:"`

通过这条指令我们发现一个 `forwardingTargetForSelector:`

我们找下源码，根据命名推测这个方法用来是转发消息给某个对象的。

***我们下个断点：***

```
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
```

***发现断点确实走到了这里，输出堆栈信息：***

```
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001003548f2 libobjc.A.dylib`-[NSObject forwardingTargetForSelector:](self=0x000000010132f350, _cmd="forwardingTargetForSelector:", sel="noIMP") at NSObject.mm:2451:5
    frame #1: 0x00007fff203f7444 CoreFoundation`___forwarding___ + 225
    frame #2: 0x00007fff203f72d8 CoreFoundation`_CF_forwarding_prep_0 + 120
    frame #3: 0x00000001000033d0 KCObjcBuild`main(argc=1, argv=0x00007ffeefbff4a0) at main.m:23:9 [opt]
    frame #4: 0x00007fff20337f5d libdyld.dylib`start + 1
```

***接下来我们试着在这里返回一个对象来接收消息看看效果。***

```
@implementation RYModel
- (id)forwardingTargetForSelector:(SEL)sel {
    return [[RYSubModel alloc] init];
}
@end

@implementation RYSubModel
- (void)noIMP {
    NSLog(@"%s",__func__);
}
@end
```

运行后执行正常，输出了 `-[RYSubModel noIMP]`。

#### 5.3 转发对象为空的情况

当不实现 `forwardingTargetForSelector` 的时候，我们发现，经过一次`动态决议`默认返回 `nil` 后，又一次进入了 `动态决议`

```
// 一次动态决议
+[RYModel resolveInstanceMethod:]--noIMP

// 打印堆栈信息
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
  * frame #0: 0x000000010031682a libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="noIMP", cls=RYModel, behavior=2) at objc-runtime-new.mm:6536:9
    frame #1: 0x00000001002edcb9 libobjc.A.dylib`class_getInstanceMethod(cls=RYModel, sel="noIMP") at objc-runtime-new.mm:6192:5
    frame #2: 0x00007fff2040d653 CoreFoundation`__methodDescriptionForSelector + 276
    frame #3: 0x00007fff20426fa0 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 30
    frame #4: 0x00007fff203f74ef CoreFoundation`___forwarding___ + 396
    frame #5: 0x00007fff203f72d8 CoreFoundation`_CF_forwarding_prep_0 + 120
    frame #6: 0x00000001000034d0 KCObjcBuild`main + 64
    frame #7: 0x00007fff20337f5d libdyld.dylib`start + 1

// 打印变量信息
(lldb) po inst
 nil
(lldb) po sel
"noIMP"

(lldb) po cls
objc[3185]: mutex incorrectly locked
objc[3185]: mutex incorrectly locked
RYModel

(lldb) po behavior
2
```

* `behavior == 2 == LOOKUP_RESOLVER`
  * 满足动态决议的条件，再次进行`动态决议`
  * `behavior ^= LOOKUP_RESOLVER`
    * `behavior`变成`0`了

### 六、总结

这部分的理解还是需要结合调试，单纯看源码很难理清楚，有太多嵌套调用。

希望我的流程图能帮助理解，有不对的地方也欢迎指出。


---

# 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://ryukiedev.gitbook.io/wiki/ios/di-ceng/13.-dong-tai-jue-yi-yu-xiao-xi-zhuan-fa.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.
