美文网首页
9、objc_msgSend动态决议&转发

9、objc_msgSend动态决议&转发

作者: ChenL | 来源:发表于2020-09-27 14:53 被阅读0次

上一篇说到了慢速查找,若在慢速查找中还是没有找到,则进入动态决议做补救,流程如下:
1、找自己的methodList
2、找父类的methodList
3、找到返回imp
4、未找到,进行消息转发..

查找结束后,第一步:动态决议 通过走resolveInstanceMethod / resolveClassMethod 进行动态补救,如第一步返回nil,则进行第二步:快速转发 forwardingTargetForSelector方法,若成功返回recevier,若返回nil则 进行第三步:慢速转发 methodSignatureForSelector 及 forwardingInvocation

消息转发机制.png

一、动态决议

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);
        }
    }

    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

1、判断类是否是元类
2、如果是类,执行实例方法的动态方法决议resolveInstanceMethod
3、如果是元类,执行类方法的动态决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
4、如果动态方法决议中,将其实现指向其他方法,则继续查找指定的imp,即继续慢速查找lookupImpOrForword 流程。

1.png

实例方法的动态方法决议
针对动态方法决议可以对奔溃的修改,可以通过在类中重写resolveInstanceMethod类方法,并将其指向其他方法的实现,在其父类 CLPerson中重写rwsolveInstanceMethod类方法,将实例方法say666的实现指向sayMaster方法实现。
如下:

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

运行结果如下:


2.png

从结果中可以发现,resolveInstanceMethod动态决议方法中“来了”打印了两次,这是为什么呢?

原因:
第一次动态决议:第一次的“来了”是在查找say666方法时会进入动态方法决议
第二次动态决议:第二次的“来了”是在慢速转发流程中调用了 CoreFoundation框架中的NSObject(NSObject) methodSignatureForSelector:后,会再次进入动态决议

类方法的动态方法决议

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

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

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

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

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

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

一、消息转发

在动态方法决议补救也不行,就使用消息转发...
通过以下方法来了解一下,方法调用崩溃前都都走了哪些方法???

1、通过instrumentObjcMessagesSends 方式打印发送消息的日志。

通过lookupImpOrForward - >log_and_fill_cache - >logMessageSend,在logMessageSend源码下方法找到instrumentObjcMessagesSends的源码实现,所以,在main中调用,instrumentObjcMessagesSends打印方法调用的日志信息,有以下两点:

1、打开 objcMsgLogEnabled 开关,即调用instrumentObjcMessageSends方法时,传入YES
2、在main中通过extern 声明instrumentObjcMessageSends方法
如:

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

通过logMessageSend源码,了解到消息发送打印信息存储在/tmp/msgSends 目录,如下

3.png

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

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

4.png
2、通过hopper / IDA反编译

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

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


5.png

发现forwarding来自CoreFoundation

6.png

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

7.png

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

8.png

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


hopper选择demo版本.png hopper反汇编.png

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

9.png

通过左侧的搜索框搜索forwarding_prep_0,然后选择伪代码
以下是forwarding_prep_0
的汇编伪代码,跳转至forwarding

10.png

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

11.png

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


12.png

如果没有响应,跳转至loc_6490b,则直接报错
如果获取methodSignatureForSelector的方法签名为nil,也是直接报错

13.png

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

14.png

由上所得:

快速转发:forwardingTargetForSelector
慢速转发:methodSignatureForSelector & forwardInvocation

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

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

//     runtime + aSelector + addMethod + imp
    //将消息的接收者指定为LGStudent,在LGStudent中查找say666的实现
    return [LGStudent alloc];
}

15.png

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

16.png

针对快速转发 中还没有找到,则进入最后一次挽救机会,即在LGPerson中重写methodSignatureForSelector 如下:

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

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
}
17.png

从打印结果,发现forwardInvocation方法中不对forwardInvocation:进行处理,也不会崩溃报错,也可以处理invocation事务,修改invocation的target为[LGStudent alloc],调用[anInvocation invoke] 触发,即LGPerson类的say666实例方法的调用 会调用 LGStudent 的say666,如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    anInvocation.target = [LGStudent alloc];
    [anInvocation invoke];
}

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

补充:方法未实现报错源码

根据慢速查找的源码,我们发现,其报错最后都是走到__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,并没有找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler,有如下实现,本质是调用的objc_defaultForwardHandler方法

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

这就是我们在日常开发中最常见的错误:没有实现函数,运行程序,崩溃时报的错误提示。

相关文章

网友评论

      本文标题:9、objc_msgSend动态决议&转发

      本文链接:https://www.haomeiwen.com/subject/drvwyktx.html