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

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

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

0x00 - forward_imp

若是以上俩步都没有作相应的操做,就会报平常开发常见的错误方法未实现的崩溃报错objective-c

以下示例代码:windows

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

// 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);
}
复制代码
  • 主要分如下几步:oop

    1. 先是判断cls是不是元类post

      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不兼容奔溃,模拟多继承)


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

相关文章
相关标签/搜索