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

2020-09-25  本文已影响0人  iOS小木偶

上一期在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的代码并未开源,我们只能借助其他工具来研究。

(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的镜像地址

*继续跳转loc_64e3c

hop7.jpg hop8.jpg

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

CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:]
__methodDescriptionForSelector 1.jpg
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

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

上一篇 下一篇

猜你喜欢

热点阅读