底层探索 -- OC消息机制(三)方法决议、消息转发

260 阅读7分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

在前一篇 底层探索 -- OC消息机制(二)lookUpImp慢速查找 中,已经完成了对于IMP快慢查找流程的分析。最终分析到未命中会返回默认的forward_imp进行执行,那么就这样简简单单的返回了吗?当然不是,返回之前还有一系列的方法决议过程,是系统提供给程序员以修复问题的机会,那本篇就将对此进行分析。(本篇总字约2360字)

本篇重点:

  1. Instance、Cls的方法决议
  2. 快速转发
  3. 普通转发

一、方法决议

还记得lookUpImpOrForward 查找有个behavior & LOOKUP_RESOLVER 的if判断吗?在系统返回forward_imp 之前,是一定会进入这个判断中进行方法决议的,目的是去尝试将IMP修复成一个正常可以执行的IMP。

mU5CDeB8PH82Mfxp2Mw9ji6yy1En83oMk6Tp3tYzQaM.png

  • objc_msgSend_unchach入参时,behavior==3就已经是带有LOOKUP_RESOLVER 标识的,当循环break后首先就是进入方法决议的判断中,去试图修复IMP,修复失败才会将forward_imp 返回出去。
  • 注意看这个if的条件:使方法决议只走一次,进入后属于LOOKUP_RESOLVER 的位置就被异或成了0。

1.1 resolveMethod_locked

首先来分析resolveMethod_locked 的逻辑:

kmm3ipjm03lz7yqVff0CcprOB2GpxGRTEGR1BX1dsuU.png

根据它的注释,OC层的两个决议方法都是“+”方法,也就是如果有实现,应该是存在MetaCls的method_lis中,而且方法名与Runtime层的相同。在看一下其内部的代码逻辑,总的来看resolveMethod_locked 是具体决议方法的入口,处理的事情也如图划分就这2点:

  1. 上部分:根据Cls是否是元类来调用不同的决议方法尝试修复IMP。
  2. 下部分:目标IMP如果被修复,在Return时就是修复后的IMP,否则命中就是系统默认的forward_imp

1.2 resolve Instance Method

先从resolveInstanceMethod 的逻辑开始分析,因为日常开发中还是Instance的方法调多一些:

5Rud22JjPjaYztAxj5iQvimyptoMxN_gK1OBKd7JpAs.png

  1. resolveInstanceMethod 中首先使用if判断对 +resolveInstanceMethod 进行了一遍快速&慢速查找,注意判断中Cls的入参是MetaCls。
    • 如果用户代码中有实现:那么会lookUpImpOrForward 中更快的找到用户实现。没有实现:那么在lookUpImpOrForward 中最终会在MetaNSObject中找到默认实现,并且 fill_cache到了元类中。
    • 所以这个判断,永远不可能进入Return。
  2. 判断过后+resolveInstanceMethod已经被fill_cache 到了MetaCls的cache中,那直接发送objc_msgSend 消息调用一次+resolveInstanceMethod 修复。
  3. 方法+resolveInstanceMethod 被调用后,再进行一遍快\慢的查找,还是没有被修复才会将{SEL, forward_imp}进行绑定,也就是将forward_imp 加入缓存。

1.3 resolve Class Method

接着再来看一下resolveClassMethod的代码逻辑:

trnHc1EVjWI3lHqSGTnH5N3WT7-MgNeWnY8d-0uwgb0.png

  • 一眼看下去,resolveClassMethodresolveInstanceMethod 在代码结构上基本是一模一样,实际分析过后其实逻辑上也一样的。只是决议方法名不同而已,查找逻辑、查找级别,包括对于方法的fill_cache 逻辑等等都是一样的,都是在MetaCls及其父中尝试找到并调用修复方法

1.4 lookUp Imp Try系列方法

分析完Instance和Cls两个修复方法,2个决议方法也有点分发入口的感觉,因为2个方法更深沉次的调用时相同的。现在就来将其展开分析:

UIQE08tDbl1p_rfoRWRHGnMvmh6ugNBmWwI3n-ylPh0.png

虽然只有3个方法,单独看代码逻辑也不难,但是它的逻辑嵌套真的是令人头秃,反反复复的将逻辑测试了好多次,注释也是改了一遍又一遍,最后才将每行代码真正意义梳理出来,从下至上的来说:

  1. 大多情况下lookUpImpOrNilTryCache 被调用的多,其目的分为2种情况
    1. imp == NULL时,去lookUpImpOrForward 中再进行一遍查找,并且执行fill_cache
    2. 作为if的条件判断条件,去判断resolver...Method 是否为nil。
  2. lookUpImpOrForwardTryCache 被调用时,{sel,imp} 已经被准备好了,return imp 就好了。
  3. 最终的方法是_lookUpImpTryCachelookUpImpOrForwardTryCachelookUpImpOrNilTryCache的目的就是为传入不同的behavior以处理不同的分支。

注意一点:
方法决议的这几个方法中,为了多次尝试修复未找到的IMP,存在的递归比较多。

png_batch.png

至此,OC中方法动态决议的流程就分析梳理完了。最后在动态决议方法执行完成后,还是没有正确的IMP的话,就可以返回forward_imp 了。但是系统真的要此返回了吗?并没有!

二、快速转发 和 普通转发

系统为了程序的健壮稳定在动态决议之后依然做了很多的努力,首先在要返回forward_imp的代码上打一个断点,当执行完1遍动态决议后跳过当前断点,然后来观察比较两次的堆栈调用,这样来测试一下是不是真的在决议方法完成一次之后还做了什么努力:

vW43LjktWdhfRIu72EsdbwIHM0y5GNX0PR1tFOevnf8.png

通过比较两次的调用堆栈,发现决议方法之后并没有直接返回,而是还会被__forwarding_prep_0______forwarding___ 再次唤起。而且还可以注意到其中有一个好辨识方法:methodSignatureForSelector ,但是想要点击看它实现确实和两个符号一样,实现都定义再了CoreFoundation 中,可是CoreFoundation 并没有开源,那接下来该怎么继续探索分析呢?

2.1 logMessageSend

好在除了反汇编这个复杂方式外,在源码中发现了另外一个容易操作的小空间。在log_and_fill_cache 中有这样一个方法,根据其入参、及其内部的这几句代码,大概可以判断是在将入参的信息写入到了磁盘文件中的。那么只要让程序执行这个方法,岂不是就可以将当前堆栈中调用的sel、Clsname等信息写到文件中么,查看文件至少是可以知道之后的调用的方法、及调用顺序。

aH3flblVEbjiRih5_YFecmB5Fj77Iq3GZBazzI7GSig.png

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector){
    ...
    snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());    
    ...
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
    isClassMethod ? '+' : '-',
    objectsClass,
    implementingClass,
    sel_getName(selector));
    ...
    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();
}

经过一步步反推,最终找到了能够进入if判断的条件开关:instrumentObjcMessageSends() ,只需要将这个开发方法加到即将查找的IMP代码前运行即可:

I7gM7lxGzuWCHRd7QSeYBkg2WZF9mUTGKBJRZ-Ju7zI.png

如果所示,运行后在tmp路径下就会生成msgSends-xx 文件:

mTJ60lUjlzoRofL9-Uwo80H5LoMLVB71Tl28gHlXtPo.png

根据文件内容,就能知道在决议方法之后,系统还进行了消息转发forwardingTargetForSelector、和普通转发methodSignatureForSelector 的处理。

2.2 forwardingTargetForSelector

知道方法名就可通过文档搜索当前的方法,了解其用法。

dem3QBtxkpvQHDcg4QdJICqqcEtVwr7fzapGBnZA_zU.png

  • 只需要重写forwardingTargetForSelector 返回一个可以处理当前sel的对象及可实现转发。
  • 但是不能返回self,否则就会死循环。因为self本来就处理不了当前的sel。

2.3 methodSignatureForSelector

OmPdYPbAttfprboM8L_kGM4ug0rW1TLoHWPWGA8X6tg.png

  • methodSignatureForSelector 需同 forwardInvocation: 一同使用才能有效果。
  • methodSignatureForSelector:目的是创建一个有效的方法签名,而且必须实现不可为空
  • forwardInvocation:将选择器转发给一个真正实现了该消息的对象,可以是空实现,并且在NSInvocation中可以保留调用的methodSignaturetarget相关信息。

关于方法签名: 在clang 后的源码中之前就见到过,比如V16@0:8 ,在methodSignatureForSelector中就要创建一个类似这样的NSMethodSignature类型的返回。
更多类型符号,可到官方文档中查看 Objective-C Runtime Programming Guide-Type Encodings

三、总结


以上就是本篇对于方法的动态决议、消息转发、消息签名等逻辑流程的探索、分析。文章内容依旧是除了文字总结外,大部分源码的代码解释、逻辑分析都写到了图中,以方便对照查看。

至此,通过3篇博客将OC消息机制的整体代码逻辑、执行流程进行了分析、记录。

篇中分析、记录的内容如有帮助,欢迎点赞👍、收藏✨、评论✍️。如果错误,欢迎评论指正🙆🏻‍♂️