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

  • 通过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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271