手撕iOS底层16 -- 消息解析&消息转发原理

1,680 阅读11分钟

前俩篇objc_msgSend快速查找objc_msgSend慢速查找的流程,主要分析了通过汇编流程快速查找缓存,通过类的方法列表慢速查找,本章着重接着上俩章深入分析没有找到方法的情况下, 苹果给开发者提供了二个建议。

  1. 动态方法解析: 在慢速查找过程中,未找到IMP,会执行一次动态方法解析
  2. 消息转发: 如果动态方法决议还是没有找到IMP,则开始消息转发

0x00 - forward_imp

如果以上俩步都没有做相应的操作,就会报日常开发常见的错误方法未实现的崩溃报错

如下示例代码:

@interface Student : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;

- (void)sayNB;
- (void)sayMaster;
- (void)say666;
- (void)sayHello;

+ (void)sayNB;
+ (void)lgClassMethod;
@end
  
@implements Student
- (void)sayHello{
    NSLog(@"%s",__func__);
}
- (void)sayNB{
    NSLog(@"%s",__func__);
}
- (void)sayMaster{
    NSLog(@"%s",__func__);
}
+ (void)lgClassMethod{
    NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [Student alloc];
        [stu say666];
        //[Student performSelector:@selector(sayNB)];
    }
    return 0;
}

main方法中分别调用实例方法类方法

  • 调用类方法

  • 分析: 在慢速查找的源码中,IMP未找到,会赋值称为forward_imp=(IMP)_objc_msgForward_impcache;,通过搜索_objc_msgForward_impcache,在相应的架构汇编找到
STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	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,根据之前总结的规则, 去掉一个下划线来搜索。

// 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;

实际的本质都是调用objc_defaultForwardHandler,这就是我们日常中经常见到的崩溃错误。 下面深入分析崩溃发生之前的补救方法

0x01 - 方法的动态解析

lookUpImpOrForward方法里,方法慢速查找走完之后,会开始走方法动态解析流程,给开发者提供第一次机会,来处理找不到消息的错误。

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

通过注释也可以得知, 这个实在IMP没有找到的时候,会走这里解决,并且只走一次。

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
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);
}
  • 主要分以下几步:

    1. 先是判断cls是否是元类

      1. 如果是,调用对象方法的动态解析resolveInstanceMethod
      2. 如果是元类,调用类方法的动态解析resolveClassMethod来处理,然后判断是否能找到sel,找不到接着再调用一次resolveInstanceMethod,因为类方法,即带+号的方法相对于元类来说也是实例方法, 调用resolveInstanceMethod,参数第一个是inst=类,第二个查找是sel方法名字,第三个cls=元类,
      if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) { 
              // Resolver not implemented.
              return;
      }
      

      如果这里查找的是类方法, 是在cls->ISA根元类里找这个解析方法的实现, 找到就去发送消息, 找不到返回默认实现。

实例方法崩溃修复

+ (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];
}

在类里边重写类方法resolveInstanceMethod,消息崩溃之前, 会执行一次实例方法动态解析,在这个方法里,通过runtime把没找到的sel指向一个存在的imp上,打印结果

这里会看到这个方法打印里俩次,这个问题留在文章末尾分析。

类方法崩溃修复

发送类方法消息找不到imp导致的崩溃修复,与实例方法类似方法修复, 重写resolveClassMethod来解决,在该方法中, 把崩溃的sel指向一个可以找到的imp

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel == @selector(sayNB)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    
    return [super resolveClassMethod:sel];
}

⚠️ 这里要注意获取类方法是要到元类,添加类方法也要到元类中,可以使用objc_getMetaClass获取元类。

总结与优化

通过上边的方法的动态解析分析, 得到这样的结论

  • 实例方法 类 -> 父类 -> 根类 -> nil
  • 类方法(resolveClassMethod) 元类 -> 父元类 -> 根元类 -> 根类 -> nil
  • 类方法(resolveInstanceMethod) 根元类 -> 根类 -> nil

之前的修复崩溃都是在对应的类中重写resolveInstanceMethod或者resolveClassMethod,通过上边这三条路线,可以根类NSObject中重写resolveInstanceMethod统一处理实例方法类方法的崩溃处理。

resolveInstanceMethodNSObject有默认实现

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

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

如下,创建一个NSObject的分类,统一处理如下,因为有默认实现,所以返回NO,不能调用[super resolveInstanceMethod:sel]

+ (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("LGPerson"), @selector(lgClassMethod));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}

当然这种统一处理的方式,还是会有一些问题, 一些系统的方法会走进这里, 可以针对类中的方法名统一前缀,根据前缀判断对应的模块来处理,比如mine模块, 属于这个模块的崩溃统一跳转到mine模块首页, 也可以做一些错误上报的操作。

0x03 - 消息转发流程

快速查找+慢速查找没有找到以及动态消息解析也未处理,就会进入消息转发过程

lookUpImpOrForward的函数末尾, 在log_and_fill_cache有这么一个控制条件objcMsgLogEnabled,通过它可以控制日志保存到本地,通过日志可以看到调用流程

控制这个objcMsgLogEnabled的是这个函数instrumentObjcMessageSends,给它传入true,控制开启本地日志保存

通过lookUpImpOrForward -> log_and_fill_cache -> logMessageSend 找到以下源码实现

bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

因为这个instrumentObjcMessageSends是内部函数,在外部使用需要使用extern外部声明

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

通过以上源码了解到日志的保存路径在/tmp/msgSends目录中,运行代码,就可以看到如下内容

在目录中打开msgSends开头的文件, 调用完resolveInstanceMethod方法,并没有在方法动态解析处理,所以来到forwardingTargetForSelector快速转发以及后续 的慢速转发

0x04 - Hopper/IDA 反汇编看流程

HopperIDA是一个可以帮助我们静态分析的反汇编工具,将可执行文件反汇编为伪代码 和流程图形式,帮助我们去分析,由于IDA在mac上不稳定,可以在windows系统上测试, 以下使用Hopper来分析。

运行崩溃后,通过bt看堆栈信息,

通过汇编查看,__forwarding___也是在CoreFoundation中。

通过image list调试命令查看CoreFoundation image的位置

找到CoreFoundation后,用Hopper打开它

打开Hopper, 选择Try the Demo,将CoreFoundation拖入里边

点击OK

默认点击Next

等待加载完成,

搜索__forwarding_prep_0___,查看伪代码, 跳转到___forwarding___里边的伪代码

首先判断是否实现forwardingTargetForSelector

  • 没有实现跳转到loc_64a67
  • 可以找到实现走loc_649fc,通过forwardingTargetForSelector获取接受对象给rax, 再对rax作容错处理,有错误跳到loc_64e3c
loc_64a67伪代码

跳到这里后,首先判断是否为僵尸对象,在下边继续判断是否响应 methodSignatureForSelector

  • 不响应跳转到loc_64dd7, 直接报错
  • 响应的话接着往下走, 获取返回值, 作容错处理,有错误跳到loc_64e3c

loc_64dd7伪代码和loc_64e3c伪代码

通过获取methodSignatureForSelector方法签名为nil也直接报错

上边的流程获取到方法签名,开始在forwardInvocation方法中进行处理

所以通过以上分析, 消息转发有俩种

  • 快速转发 forwardingTargetForSelector
  • 慢速转发methodSignatureForSelector +forwardingTargetForSelector实现

方法动态决议-动态决议和转发流程

lookUpImpOrForward中,慢速也没有找到imp

  • 第一步开始方法的动态解析处理,这步未处理, 即走消息转发
  • 消息转发第一步开始forwardingTargetForSelector,即快速消息转发,将消息转发给别等对象处理,这步未处理,交给慢速转发
  • 慢速转发使用methodSignatureForSelector返回方法签名,不可以返回nil或者签名内容为空,使用方法签名生成NSInvocation对象, 所以需要重写forwardInvocation进行消息转发。

0x05 - resolveInstanceMethod为什么执行俩次?

解决之前遗留的问题, 在实例动态方法解析的时候, 只重写了, 并未对未找到的sel作处理, 会调用俩次

上帝视角探索

实例动态方法解析的时候, 会走到lookUpImpOrForward -> resolveMethod_locked -> resolveInstanceMethod,是通过这里触发

IMP imp = lookUpImpOrNil(inst, sel, cls);加个断点, 当selsay666停下来,打印了了say66 来了通过bt查看堆栈,

第一次打印的信息, 通过堆栈可以看出是第一次通过方法动态解析执行打印的。

通过第二次打印, 通过[NSObject(NSObject) methodSignatureForSelector:] -> __methodDescriptionForSelector -> class_getInstanceMethod再次来到方法的动态解析并打印了第二次,通过堆栈分析, 可以通过Hopper反汇编CoreFoundation文件,查看methodSignatureForSelector的伪代码

在跳进到___methodDescriptionForSelector看它的实现

结合之前的堆栈信查看, 这里调用了objc 的方法 class_getInstanceMethod,在源码工程查看

/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

通过源码查看,这里又调用了lookUpImpOrForward, 又走了一次方法动态解析,系统在调用完methodSignatureForSelector,返回方法签名,在调用invocation之前,又去调用class_getInstanceMethod,所以又走了一遍lookUpImpOrForward,查询一遍sel,没查到再走方法动态解析消息转发流程。

无上帝视角探索

因为在源码工程里探索, 所以有上帝视角, 如果没有环境, 如何验证上边的流程?

普通工程里重写resolveInstanceMethod ,在方法里解决sel找不到的错误,使用class_addMethod添加一个IMP, 看看这个方法是否会走俩次?

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

通过结果看,通过动态方法解析,赋值了IMP, 只执行了一次,说明第二次不在这里。按照消息转发流程, 把resolveInstanceMethod里的imp去掉,重写forwardingTargetForSelector,并指定[LGStudent alloc],重新运行, 看是否resolveInstanceMethod打印俩次, 打印俩次,说明在forwardingTargetForSelector之前执行了方法动态解析,反之,则在之后执行的方法动态解析

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(say666)) {
        NSLog(@"%s -- %@ 来了",__func__, NSStringFromSelector(sel));
//        //获取sayMaster方法的imp
//        IMP imp = class_getMethodImplementation(self, @selector(sayHello));
//        //获取sayMaster的实例方法
//        Method sayMethod  = class_getInstanceMethod(self, @selector(sayHello));
//        //获取sayMaster的方法签名
//        const char *type = method_getTypeEncoding(sayMethod);
//        //将sel的实现指向sayMaster
//        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    // runtime + aSelector + addMethod + imp
    return [LGStudent alloc];
}

通过运行结果看, 并没有在之前答应俩次, 说明在forwardingTargetForSelector之后执行的方法动态解析

接着根据流程,重写methodSignatureForSelectorforwardInvocation

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    NSLog(@"%p", [NSMethodSignature signatureWithObjCTypes:"v@:@"]);
    return [NSMethodSignature signatureWithObjCTypes:"v@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    // GM  sayHello - anInvocation - 漂流瓶 - anInvocation
    anInvocation.target = [LGStudent alloc];
    // anInvocation 保存 - 方法
    [anInvocation invoke];
}

经过上边的分析,第二次动态决议是在methodSignatureForSelectorforwardInvocation之间调用的,第二种分析方法验证结果和第一种反汇编的结果是一样的。得到如下的图

总结

本篇是消息流程分析方法动态解析消息转发的最后一篇

  • 首先消息通过汇编流程快速查找,没有找到跳到lookupImpOrForward开始慢速查找
  • 慢速查找消息也没有找到,开始方法动态决议
  • 方法动态决议根据消息是类方法还是实例方法重写resolveInstanceMethodresolveClassMethod方法,开始第一次补救
  • 方法动态决议也没有处理, 开始进行消息转发即【快速转发】
  • 快速转发, 即重写forwardingTargetForSelector方法, 将消息甩给可以处理的对象,进行第二次补救
  • 慢速转发使用methodSignatureForSelector返回方法签名,不可以返回nil或者签名内容为空,使用方法签名生成NSInvocation对象, 所以需要重写forwardInvocation进行消息转发。

Objective-C 方法签名和调用

iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding) (消息机制,方法未实现+API不兼容奔溃,模拟多继承)


欢迎大佬留言指正😄,码字不易,觉得好给个赞👍 有任何表达或者理解失误请留言交流;共同进步;