OC底层原理14-3:objc_msgSend之动态方法决议 & 消息转发

张建 lol

前言

  • 在前面两篇文章中,分别分析了 objc_msgSend快速查找(缓存)慢速查找(方法列表),其整个流程:

    • objc_msgSend -> 慢速缓存查找(没有) -> 慢速方法列表二分查找自己(有) -> cache_fill -> objc_msgSend 整个闭环

    • 慢速方法列表二分查找没有找到 -> 父类去查找(缓存 -> 方法列表) -> 根类(NSObject)也没找到

  • 在这两种方法都没找到方法实现的情况下,苹果给了两条建议

    • 动态方法决议:慢速查找流程未能找到,会执行一次动态方法决议
    • 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发
  • 如果这两个建议都没有作出任何操作,就会报我们日常开发中常见的 方法未实现崩溃报错,其步骤如下

  • 定义 ZJPerson 类,其中 say666 实例方法 和 sayNB 类方法均没有实现

  • 调用类方法 sayNB 的报错结果

方法未实现报错源码

根据 慢速查找 的源码,我们发现,其报错最后都是走到 __objc_msgForward_impcache 方法,本质是调用的 objc_defaultForwardHandler 方法

下面我们来讲讲如何在崩溃前,防止方法未实现的崩溃

三次方法查找的挽救机会

根据苹果建议,我们一共有三次挽救的机会:

  • 【第一次机会】:动态方法决议

  • 消息转发流程:

    • 【第二次机会】:快速转发
    • 【第三次机会】:慢速转发

【第一次机会】动态方法决议

慢速查找 流程 未找到 方法实现时,首先会 尝试一次动态方法决议,其源码实现如下:

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
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());

runtimeLock.unlock();
// 对象 -> 类
if (! cls->isMetaClass()) { // 类不是元类,调用对象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else { // 如果是元类,调用类的解析方法,类 -> 元类
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
// 为什么要有这行代码? -> 类方法在元类中是对象方法,所以还是需要查询元类中对象方法的动态方法决议
if (!lookUpImpOrNil(inst, sel, cls)) { // 如果没有找到或者为空,在元类的对象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
}

// chances are that calling the resolver have populated the cache
// so attempt using it
// 如果方法解析中奖其实现指向其他方法,则继续走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

主要分为以下几个步骤

  • 判断类是否是元类

    • 如果是 ,执行 实例方法 的动态方法决议 resolveInstanceMethod
    • 如果是 元类,执行 类方法 的动态方法决议 resolveClassMethod,如果在元类中 没有找到 或者为 ,则在 元类实例方法 的动态方法决议 resolveInstanceMethod 中查找,主要是因为 类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果 动态方法决议 中,将其 实现指向了其他方法,则继续 查找指定的imp,即继续慢速查找 lookupImpOrForward

实例方法

针对 实例方法 调用,在 快速、慢速查找 均没有找到 实例方法 的实现时,我们有一次挽救的机会,即尝试一次 动态方法决议,由于是 实例方法,所以会走到 resolveInstanceMethod 方法,其源码如下:

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
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);

// look的是 resolveInstanceMethod 相当于是发送消息前的容错处理
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel); // 发送resolve_sel消息

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 查找say666
IMP imp = lookUpImpOrNil(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

主要分为以下几个步骤

  • 发送resolveInstanceMethod消息 前,需要查找 cls类 中是否有该方法的 实现,即通过 lookupImpOrNil 方法又会进入 lookupImpOrForward 慢速查找流程查找 resolveInstanceMethod 方法

    • 如果没有,则直接返回
    • 如果有,则发送 resolveInstanceMethod 消息
  • 再次慢速查找实例方法的实现,即通过 lookupImpOrNil 方法又会进入 lookupImpOrForward 慢速查找流程查找 实例方法

崩溃修改

所以,针对 实例方法sya666 未实现的报错崩溃,可以通过在 重写resolveInstanceMethod类方法,并将其指向其他方法的实现,即在 ZJPerson重写resolveInstanceMethod类方法,将 实例方法sya666 的实现指向 sayMaster 方法实现,如下所示

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
@interface ZJPerson : NSObject
- (void)say666;
- (void)sayMaster;
@end

@implementation ZJPerson
- (void)sayMaster{
NSLog(@"say666");
}
// 重写resolveInstanceMethod
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了",NSStringFromSelector(sel));
// 获取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
// 获取sayMaster的实例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
// 获取sayMaster的丰富签名
const char * type = method_getTypeEncoding(sayMethod);
// 将sel的实现指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}

@end

重新运行,其打印结果如下:

类方法

针对 类方法,与实例方法类似,同样可以通过重写 resolveClassMethod 类方法来解决前文的崩溃问题,即在 ZJPerson 类中重写该方法,并将 sayNB 类方法的实现 指向类方法zjClassMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ZJPerson : NSObject
+ (void)sayNB;
+ (void)zjClassMethod;
@end

@implementation ZJPerson
+ (void)zjClassMethod{
NSLog(@"sayNB");
}
+ (BOOL)resolveClassMethod:(SEL)sel{
if (sel == @selector(sayNB)) {
NSLog(@"%@ 来了",NSStringFromSelector(sel));

IMP imp = class_getMethodImplementation(objc_getMetaClass("ZJPerson"), @selector(zjClassMethod));
Method zjClassMethod = class_getInstanceMethod(objc_getMetaClass("ZJPerson"), @selector(zjClassMethod));
const char * type = method_getTypeEncoding(zjClassMethod);
return class_addMethod(objc_getMetaClass("ZJPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
@end

resolveClassMethod 类方法的重写需要注意一点,传入的 cls 不再是类,而是 元类,可以通过 objc_getMetaClass 方法 获取元类,原因是因为 类方法在元类中是实例方法

优化

上面的这种方式是单独再每个类中重写,有没有更好的,一劳永逸的方法呢?其实通过方法慢速查找流程可以发现其查找路径有两条

  • 实例方法:类 -> 父类 -> 根类 -> nil
  • 类方法:元类 -> 根元类 -> 根类 -> nil

它们的共同点是如果前面没找到,都会来到 根类即NSObject中查找,所以我们是否可以将上述的两个方法统一整合在一起呢?答案是 可以的,可以通过 NSObject添加分类 的方式来 实现同意处理,而且由于类方法的查找,在其集成链,查找的也是实例方法,所以将 实例方法类方法 的统一处理放在 resolveInstanceMethod 方法中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface NSObject (ZJ)
@end
@implementation NSObject (ZJ)
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 来了",NSStringFromSelector(sel));

IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(sayNB)){
NSLog(@"%@ 来了",NSStringFromSelector(sel));

IMP imp = class_getMethodImplementation(objc_getMetaClass("ZJPerson"), @selector(zjClassMethod));
Method zjClassMethod = class_getInstanceMethod(objc_getMetaClass("ZJPerson"), @selector(zjClassMethod));
const char * type = method_getTypeEncoding(zjClassMethod);
return class_addMethod(objc_getMetaClass("ZJPerson"), sel, imp, type);
}
return NO;
}

@end

这样方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法的动态方法决议,其根本原因还是 类方法在元类中的实例方法

当然,上面这种写法还是会有其他问题,比如 系统方法也会被更改,针对这一点,是可以优化的,即我们 可以针对自定义类中方法同意方法名的前缀,根据前缀来判断是否是自定义方法,然后 统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于 app线上防崩溃的处理,提升用户的体验

消息转发流程

在慢速查找的流程中,我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用 消息转发,但是,我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解,方法调用崩溃前都走了哪些方法

  • 通过 instrumentObjcMessageSends 方式打印发小消息的日志
  • 通过 hopper/IDA反编译

通过 instrumentObjcMessageSends

  • 通过 lookUpImpOrForward -> log_and_fill_cache -> logMessageSend,在logMessageSend源码下方找到 instrumentObjcMessageSends 的源码实现,在main中调用 instrumentObjcMessageSends 打印方法调用的日志信息,有以下两点准备工作

    • 打开 objcMsgLogEnabled 开关,即调用 instrumentObjcMessageSends 方法时,传入 YES
    • main 中通过 extern 声明 instrumentObjcMessageSends 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {

ZJPerson * person = [ZJPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);

appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
  • 通过 logMessageSend 源码,了解到消息发送打印信息存储在 /tmp/msgSends 目录,如下所示:

  • 运行代码,并前往 /tmp/msgSends/ 目录,发现有 msgSends 开头的日志文件,打开发现崩溃前,执行了以下方法:

    • 两次 动态方法决议resolveInstanceMethod 方法
    • 两次 消息快速转发forwardingTargetForSelector 方法
    • 两次 消息慢速转发methodSignatureForSelector + resolveInvocation

通过hopper/IDA反编译

HopperIDA 是一个可以帮助我们静态分析可视性文件的工具,可以将执行文件反编译成伪代码、控制流程图等,下面以Hopper为例(注:hopper高级版本是一款收费软件,针对比较简单的反汇编需求来说,demo版本足够使用了)

  • 运行程序崩溃,查看堆栈信息

  • 发现 __forwarding__ 来自 CoreFoundation

  • 通过 image list,读取整个镜像文件,然后搜索 CoreFoundation,查看其可执行文件的路径

  • 通过文件路径,找到 CoreFoundation可执行文件

  • 打开hopper,选择Try the Demo,然后将上一步的可执行文件拖入hopper进行反汇编,选择x86(64 bits)

  • 以下是反汇编后的界面,主要使用上面的三个功能,分别是 汇编、流程图、伪代码

  • 通过左侧的搜索框搜索__forwarding_prep_0___,然后选择伪代码

    • 以下是__forwarding_prep_0___的汇编伪代码,跳转至___forwarding___

  • 以下是___forwarding___的伪代码实现,首先是查看是否实现forwardingTargetForSelector方法,如果没有响应,跳转至loc_6459b即快速转发没有响应,进入慢速转发流程

  • 跳转至loc_6459b,在其下方判断是否响应methodSignatureForSelector方法

  • 如果没有响应,跳转至loc_6490b,则直接报错

  • 如果获取methodSignatureForSelector的方法签名为nil,也是直接报错

  • 如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理

所以,通过上面两种查找方式可以验证,消息转发 的方法有3个

  • 【快速转发】forwardingTargetForSelector
  • 【慢速转发】
    • methodSignatureForSelector
    • forwardInvocation

所以,综上所述,消息转发整体的流程如下

消息转发 的处理主要分为两部分:

  • 【快速转发】当慢速查找以及动态方法决议没有找到实现时,进行消息转发,首先是进行 快速消息转发,即走到 forwardingTargetForSelector 方法

    • 如果返回 消息接收者,在消息接收者中还是没有找到,则进入另一个方法的查找流程

    • 如果返回 nil,则进入 慢速消息转发

  • 【慢速转发】执行到 methodSignalForSelector 方法

    • 如果返回的 方法签名nil,则直接 崩溃报错

    • 如果返回的方法签名 不为nil,走到 forwardInvocation 方法中,对invocation事务进行处理,如果不处理也不会报错

【第二次机会】快速转发

针对前文的崩溃问题,如果 动态方法决议 也没有找到方法实现,则需要在 ZJPerson 中重写 forwardingTargetForSelector 方法,将ZJPerson的实例方法的 接收者指定为ZJStudent 的对象(ZJStudent类中有say666的具体实现),如下所示

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
// ZJPerson类
@interface ZJPerson : NSObject
- (void)say666;
@end
@implementation ZJPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

// 将消息接收者指定为ZJStudent,在ZJStudent中查找say666的实现
return [ZJStudent alloc];
}
@end

// ZJStudent类
@interface ZJStudent : NSObject
@end
@implementation ZJStudent
- (void)say666{
NSLog(@"say666");
}
@end

// main中
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZJPerson * person = [ZJPerson new];
[person say666];
}
return 0;
}

执行结果如下

也可以直接不指定消息接收者,直接调用父类的该方法,如果还是没有找到,则 直接报错

【第三次机会】慢速转发

针对 第二次机会即快速转发 中还是没有找到,则进入最后的一次挽救机会,即在 ZJPerson 中重写 methodSignalForSelector,如下所示

1
2
3
4
5
6
7
8
9
// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}

打印结果如下,发现 forwardInvocation 方法中不对 invocation 进行处理,也不会崩溃

也可以 处理invocation 事务,如下所示,修改 invocationtarget[ZJStudent alloc],调用 [anInvocation invoke] 触发 即ZJPerson 类的 say666 实例方法的调用会调用 ZJStudentsay666 方法

1
2
3
4
5
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
anInvocation.target = [ZJStudent alloc];
[anInvocation invoke];
}

执行结果如下

https://user-images.githubusercontent.com/25925248/160738899-270d3fb2-e047-4f64-a962-23b5278f8ddd.jpg

所以,由上述可知,无论在 forwardInvocation 方法中 是否处理invocation 事务,程序都不会崩溃

总结

到目前为止,objc_msgSend 发送消息的流程就分析完成了,在这里简单总结下

  • 【快速查找流程】在类的 缓存cache 中查找指定方法的实现

  • 【慢速查找流程】如果缓存中 没有找到,则在 类的方法列表 中查找,如果还是没有找到,则去 父类的缓存和方法列表中 查找

  • 【动态方法决议】如果慢速查找 还是没有找到,第一次挽救的机会就是 尝试一次动态方法决议,即 重写resolveInstanceMethod/resolveClassMethod 方法

  • 【消息转发】如果动态方法决议还是没有找到,则进行 消息转发,消息转发中有 两次挽救几会:快速转发+慢速转发

  • 如果转发之后也没有,则程序直接报错崩溃 unrecognized selector sent to instance

  • Post title:OC底层原理14-3:objc_msgSend之动态方法决议 & 消息转发
  • Post author:张建
  • Create time:2020-10-06 10:13:46
  • Post link:https://redefine.ohevan.com/2020/10/06/OC底层原理/OC底层原理14-3:objc_msgSend之动态方法决议&消息转发/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.