美文网首页
iOS底层探索 -- 动态方法决议 && 消息转发流程

iOS底层探索 -- 动态方法决议 && 消息转发流程

作者: iOS小木偶 | 来源:发表于2020-09-25 18:48 被阅读0次

    上一期在objc_msgSend()慢速查找 lookUpImpOrForward流程中如果一直没有找到方法,那流程会走向
    resolveMethod_locked
    -> resolveInstanceMethod / resolveClassMethod
    -> resolveInstanceMethod:/ resolveClassMethod:
    也就是当方法一直无法找到的时候,会根据对象方法或者类方法的不同,走向最终对象方法或者类方法的动态方法决议
    为了保持流程的完整性。我们研究一下 动态方法决议

    动态方法决议

    先用代码测试一下。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            FQPerson *person = [FQPerson alloc];
    
            [person sayHelloWorld];
    
        }
        return 0;
    }
    

    main中我们调用sayHelloWorld方法
    FQPerson.m的中注释掉 sayHelloWorld方法的实现,同时添加

    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"没找到 %@ 方法",NSStringFromSelector(sel));
        return [super resolveInstanceMethod:sel];
    }
    

    运行

    动态方法决议的打印.jpg

    说明之前的流程确实如我们源码看到的那样,走到了resolveInstanceMethod中。
    动态方法决议其实是苹果在我们无法找到方法时给我们提供的补救流程,在这里,我们如果实现了方法,我们还是能避免崩溃。我们来尝试一下。

    先引入头文件

    #import <objc/message.h>
    

    然后再resolveInstanceMethod内部添加代码

    + (BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"没找到 %@ 方法",NSStringFromSelector(sel));
        if(sel == @selector(sayHelloWorld)){
            IMP imp          = class_getMethodImplementation(self, @selector(eat1));
            Method eatMethod = class_getInstanceMethod(self, @selector(eat1));
            const char *type = method_getTypeEncoding(eatMethod);
            return class_addMethod(self, sel, imp, type);
        }
        return [super resolveInstanceMethod:sel];
    }
    
    

    在这里,我们将已经实现过的方法eat1赋给了sayHelloWorld

    运行


    动态方法决议中动态添加方法.jpg

    此时 并未崩溃,同时调用了eat1方法。

    继续,我们注释掉eat1的实现

    然后运行


    eat1动态方法决议.jpg

    此时,我们可以发现,在sayHelloWorld的动态决议之后,进入了eat1的动态方法决议,预估应该是在将eat1赋给sayHelloWorld后开始进入了eat1的方法转发流程。

    此时有一个问题,为什么在第一张动态方法决议的打印图中打印了两次?

    没找到 sayHelloWorld 方法
    没找到 sayHelloWorld 方法
    

    那么,我们来研究一下动态方法决议之后,系统做了什么?

    第二次 动态方法决议 的来源

    我们在+ (BOOL)resolveInstanceMethod:(SEL)sel中打个断点,在第二次进入时 bt 打印栈信息

    (lldb) bt 
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x0000000100001885 KCObjc`+[FQPerson resolveInstanceMethod:](self=FQPerson, _cmd="resolveInstanceMethod:", sel="sayHelloWorld") at FQPerson.m:59:55 [opt]
        frame #1: 0x00000001002fd3a7 libobjc.A.dylib`resolveInstanceMethod(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson) at objc-runtime-new.mm:6001:21
        frame #2: 0x00000001002e8e73 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson, behavior=0) at objc-runtime-new.mm:6043:9
        frame #3: 0x00000001002e879c libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson, behavior=0) at objc-runtime-new.mm:6192:16
        frame #4: 0x00000001002c27c9 libobjc.A.dylib`class_getInstanceMethod(cls=FQPerson, sel="sayHelloWorld") at objc-runtime-new.mm:5922:5
        frame #5: 0x00007fff2ddc8c68 CoreFoundation`__methodDescriptionForSelector + 282
        frame #6: 0x00007fff2dde457c CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
        frame #7: 0x00007fff2ddb0fc0 CoreFoundation`___forwarding___ + 408
        frame #8: 0x00007fff2ddb0d98 CoreFoundation`__forwarding_prep_0___ + 120
        frame #9: 0x0000000100001b30 KCObjc`main(argc=<unavailable>, argv=<unavailable>) + 64 [opt]
        frame #10: 0x00007fff67e65cc9 libdyld.dylib`start + 1
        frame #11: 0x00007fff67e65cc9 libdyld.dylib`start + 1
    

    通过打印的方法信息的反推,我们大概能看见流程


    栈方法逆推.jpg

    我们想研究这个流程详细流程,但是CF的代码并未开源,我们只能借助其他工具来研究。

    • 通过lldbimage list 打印镜像列表
    (lldb)  image list
    [  0] 02E8C081-F154-3A94-BF16-66811D081546 0x0000000100000000 /Users/fangqiang/Library/Developer/Xcode/DerivedData/objc-cmqgtagmrqfzeobzskdrohmygvsg/Build/Products/Debug/KCObjc 
    [  1] F9D4DEDC-8296-3E3F-B517-9C8B89A4C094 0x000000010000b000 /usr/lib/dyld 
    [  2] F9BB2E7A-E017-32C8-8DB8-5F748EE88EF9 0x00000001002bd000 /private/tmp/objc.dst/usr/lib/libobjc.A.dylib 
    [  3] 7C69F845-F651-3193-8262-5938010EC67D 0x00007fff3040a000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation 
    [  4] C0C9872A-E730-37EA-954A-3CE087C15535 0x00007fff64e4a000 /usr/lib/libSystem.B.dylib 
    [  5] C0D70026-EDBE-3CBD-B317-367CF4F1C92F 0x00007fff2dd4d000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 
    [  6] E692F14F-C65E-303B-9921-BB7E97D77855 0x00007fff65183000 /usr/lib/libc++abi.dylib 
    [  7] 59A8239F-C28A-3B59-B8FA-11340DC85EDC 0x00007fff65130000 /usr/lib/libc++.1.dylib 
    

    得到CF的镜像地址

    • 找到镜像地址后 通过hopper我们来追踪一下CF内部的实现流程


      hop1.jpg
    • 使用其中的伪代码模式,方便我们阅读 搜索__forwarding_prep_0___
      参考栈方法流程往下走

      hop2.jpg
    • 然后跳转loc_649bb
      hop3.jpg
    • 其中调用了判断了方法forwardingTargetForSelector是否实现,为空的话继续跳转loc_64a67

      hop4.jpg
    • 判断是否为_NSZombie对象,不是则继续跳转loc_64dc1

      hop5.jpg
    • 继续跳转loc_64dd7

      hop6.jpg

    *继续跳转loc_64e3c

    hop7.jpg hop8.jpg

    上面大概是一个简略的消息转发失败流程,似乎没有找到答案。
    我们回到上面的栈打印,其中有一个

    CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:]
    
    • 在流程__forwarding_prep_0___走完之后,如果没有实现中间的forwardingTargetForSelector的方法,那后续根据栈的打印,会走到methodSignatureForSelector

    • 我们来搜索一下methodSignatureForSelector

      methodSingnatureForSelector.jpg
    • 跳转其中的__methodDescriptionForSelector

    __methodDescriptionForSelector 1.jpg
    • 再跳转loc_7c68b

      hop10.jpg
    • 其中得到class_getInstanceMethod(),再回到源码

    Method class_getInstanceMethod(Class cls, SEL sel)
    {
        if (!cls  ||  !sel) return nil;
    
    
    #warning fixme build and search caches
            
    
        lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
    
    #warning fixme build and search caches
    
        return _class_getMethod(cls, sel);
    }
    

    在这里,我们再次进入lookUpImpOrForward流程,会进行第二次动态方法决议的打印

    消息转发

    在我们刚刚通过hopper探索的过程中
    我们还看到了其中一些其他处理

    1. 判断是否响应forwardingTargetForSelector
    2. 如果不响应,会跳转判断是否响应methodSignatureForSelector
    3. 如果也不响应 则直接报错
    4. 如果获取 methodSignatureForSelector方法签名nil,也将直接报错。
    5. 如果返回值methodSignatureForSelector不为空,则在forwardInvocation中进行处理。

    以上也就是我们消息转发的流程。

    我们通过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源码,我们定位到打印文件的位置

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

    得到文件结果


    logMsg打印.jpg

    所以最终我们的消息转发流程为


    消息转发流程.png

    其实这里的消息转发流程和动态决议是系统给予我们的三次补救机会,可以在这里避免程序崩溃。
    但在实际使用过程中还会有一些坑点,还有一些实际的使用,我们有空再细说

    相关文章

      网友评论

          本文标题:iOS底层探索 -- 动态方法决议 && 消息转发流程

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