iOS-OC底层09:动态方法决议 & 消息转发

前沿

我们在oc层面调用对象方法实质是向某对象发送消息也就是objc_msgSend,objc_msgSend需要找到对应方法的实现也就是函数指针IMP,查找IMP首先在缓存中查找也就是快速查找,然后慢速查找也就是在类的方法类表中查找,如果这两种方法都找不到IMP,则在源码中有lookUpImpOrForward,可以看到会走resolveMethod_locked函数也就是动态方法决议

动态方法决议

在NSObject头文件中有两个方法去实现动态方法决议

//类方法动态决议
+ (BOOL)resolveClassMethod:(SEL)sel ;
//对象方法动态决议
+ (BOOL)resolveInstanceMethod:(SEL)sel;

我们看一下在OC底层是怎么调用动态决议方法的

对象方法动态决议resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // lookup resolveInstanceMethod
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

}

1.先慢速查询resolveInstanceMethod的IMP,如果查到进行下一步,如果查不到能返回
2.调用resolveInstanceMethod方法

  1. resolveInstanceMethod之后重新查询方法的IMP,并返回

类方法动态决议resolveClassMethod

我们需要注意一点,在调用resolveClassMethod代码

   resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {  // 为什么要有这行代码
            resolveInstanceMethod(inst, sel, cls);
        }

通过继承树我们知道类方法的最后继承自NSObject,会最终调用NSObject的实例方法(对象方法)

动态方法决议使用

我们可以在resolveInstanceMethod或resolveClassMethod方法中添加对应方法的实现,在慢速查询方法中,调用resolveInstanceMethod方法之后查询sel的IMP,如果我们在resolveInstanceMethod方法中添加方法,所以在查询IMP时就能找到,程序就能正常运行。

动态方法决议项目中的运行

我们可以在NSObject 的resolveInstanceMethod收集信息,如果类方法和对象方法都过继承树都要走到NSObject的resolveInstanceMethod,我们可以添加NSObject的Category在来实现resolveInstanceMethod。我们可以在resolveInstanceMethod中收集崩溃信息,我们也可以添加一个静态IMP,来防止程序崩溃。
如果动态方法决议不能解决方法未找到的情况就要走消息转发了
示例如下

 LGPerson *person = [LGPerson alloc];
   [person sayNB];
//sayNB只声明不实现打印日志如下

-[LGPerson sayNB]: unrecognized selector sent to instance 0x1018560a0
//实现resolveInstanceMethod
void sayNB(){
    NSLog(@"12345");
    
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayNB)) {
       return  class_addMethod([self class], sel, sayNB, nil);
    }
    return [super resolveInstanceMethod:sel];
}
//打印结果如下
//2020-09-25 17:15:38.507991+0800 KCObjc[61134:553937] 12345

消息转发

消息转发分快速转发和慢速转发

快速转发forwardingTargetForSelector:

如果动态方法决议中没有给对应方法加IMP,则会走快速转发流程
快速转发就是让这个消息让其他对象去接收

-(id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayNB)) {
        
        return [LGTeacher new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
//LGTeacher实现sayNB方法
//打印日志
//我是老师,我当然牛逼

如果返回的对象时nil则[LGPerson sayNB]: unrecognized selector sent to instance

慢速转发

当在快速转返回nil时,则进入最后的一次挽救机会,重写methodSignatureForSelector,

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    
    return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s",__func__);
}
打印结果
 ---+[LGPerson resolveInstanceMethod:]--sayNB
-[LGPerson methodSignatureForSelector:]
---+[LGPerson resolveInstanceMethod:]--_forwardStackInvocation:
-[LGPerson forwardInvocation:]

我们在forwardInvocation中只是简单的做一个NSLog,程序就不会崩溃。
我们也可以对invocation事务进行处理,修改invocation的target为[LGStudent alloc],调用 [anInvocation invoke] 触发 即LGPerson类的say666实例方法的调用会调用LGStudent的say666方法。
动态方法决议和消息转发的流程图


消息转发.png

从日志出发查看方法动态方法决议和消息转发的先后顺序

我们在lookUpImpOrForward发现一个函数log_and_fill_cache,我们知道fillCache是怎么回事,那log呢?我走进函数,看到打印日志的标记是objcMsgLogEnabled,而objcMsgLogEnabled是可以我们设置的通过instrumentObjcMessageSends

extern void instrumentObjcMessageSends(BOOL flag); //声明
     LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];//只声明未实现
        instrumentObjcMessageSends(NO);

在logMessageSend中我们看到日志是写到/tmp文件下,运行之后出现文件msgSends-38368

+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject class
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:

我们在源码中看到resolveInstanceMethod之后就没有forwardingTargetForSelector,methodSignatureForSelector方法的调用,bt打印堆栈信息,看到关于CoreFoundation,我们猜想动态方法决议之后CoreFoundation接下来处理,查看CoreFoundation之后没看到关于堆栈信息的forwarding_prep_0_ forwarding

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x00007fff2fdbb6e6 CoreFoundation`CFStringGetLength + 6
    frame #1: 0x00007fff2fde5816 CoreFoundation`CFStringAppend + 229
    frame #2: 0x00007fff2fe25e4b CoreFoundation`-[NSArray componentsJoinedByString:] + 309
    frame #3: 0x00007fff2ff2b95f CoreFoundation`__handleUncaughtException + 761
    frame #4: 0x00007fff68cfb5a3 libobjc.A.dylib`_objc_terminate() + 90
    frame #5: 0x00007fff671ce887 libc++abi.dylib`std::__terminate(void (*)()) + 8
    frame #6: 0x00007fff671d11a2 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
    frame #7: 0x00007fff671d1169 libc++abi.dylib`__cxa_throw + 113
    frame #8: 0x00007fff68cf96ed libobjc.A.dylib`objc_exception_throw + 350
    frame #9: 0x00007fff2ff31be7 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    frame #10: 0x00007fff2fe173bb CoreFoundation`___forwarding___ + 1427
    frame #11: 0x00007fff2fe16d98 CoreFoundation`__forwarding_prep_0___ + 120
  * frame #12: 0x0000000100000e80 002-instrumentObjcMessageSends辅助分析`main(argc=1, argv=0x00007ffeefbff260) at main.m:19:9
    frame #13: 0x00007fff69ea1cc9 libdyld.dylib`start + 1

反编译查看CoreFoundation image list查看工程中使用的库

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 

前往该目录

image.png

使用反汇编工具 hopper Disassembler搜索forwarding_prep_0_ 或者forwarding查看消息转发流程
image.png

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