从源代码看 ObjC 中消息的发送
ValenciaK87
8年前
<p> </p> <p>关注仓库,及时获得更新: <a href="/misc/goto?guid=4959671674863698147" rel="nofollow,noindex">iOS-Source-Code-Analyze</a></p> <p>因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 x86_64 架构下运行的,对于在 arm64 中运行的代码会特别说明。</p> <h2>写在前面</h2> <p>如果你点开这篇文章,相信你对 Objective-C 比较熟悉,并且有多年使用 Objective-C 编程的经验,这篇文章会假设你知道:</p> <ol> <li>在 Objective-C 中的“方法调用”其实应该叫做消息传递</li> <li>[receiver message] 会被翻译为 objc_msgSend(receiver, @selector(message))</li> <li>在消息的响应链中 <strong>可能</strong> 会调用 - resolveInstanceMethod: - forwardInvocation: 等方法</li> <li> <p>关于选择子 SEL 的知识</p> <p>如果对于上述的知识不够了解,可以看一下这篇文章 <a href="/misc/goto?guid=4958863526773227196" rel="nofollow,noindex">Objective-C Runtime</a> ,但是其中关于 objc_class 的结构体的代码已经过时了,不过不影响阅读以及理解。</p> </li> <li> <p>方法在内存中存储的位置, <a href="/misc/goto?guid=4959671674987737652" rel="nofollow,noindex">深入解析 ObjC 中方法的结构</a></p> <p>文章中不会刻意区别方法和函数、消息传递和方法调用之间的区别。</p> </li> <li> <p>能KX上网(会有一个 油Tube 的链接)</p> </li> </ol> <h2>概述</h2> <p>关于 Objective-C 中的消息传递的文章真的是太多了,而这篇文章又与其它文章有什么不同呢?</p> <p>由于这个系列的文章都是对 Objective-C 源代码的分析,所以会 从 Objective-C 源代码中分析并合理地推测一些关于消息传递的问题 。</p> <p><img src="https://simg.open-open.com/show/9e974d8a595cdf75449f94f13972d7ec.png"></p> <h2>关于 @selector() 你需要知道的</h2> <p>因为在 Objective-C 中,所有的消息传递中的“消息“都会被转换成一个 selector 作为 objc_msgSend 函数的参数:</p> <pre> <code class="language-objectivec">[object hello] -> objc_msgSend(object, @selector(hello)) </code></pre> <p>这里面使用 @selector(hello) 生成的选择子 <strong>SEL</strong> 是这一节中关注的重点。</p> <p>我们需要预先解决的问题是:使用 @selector(hello) 生成的选择子,是否会因为类的不同而不同?各位读者可以自己思考一下。</p> <p>先放出结论:使用 @selector() 生成的选择子不会因为类的不同而改变,其内存地址在编译期间就已经确定了。也就是说 向不同的类发送相同的消息时,其生成的选择子是完全相同的 。</p> <pre> <code class="language-objectivec">XXObject *xx = [[XXObject alloc] init] YYObject *yy = [[YYObject alloc] init] objc_msgSend(xx, @selector(hello)) objc_msgSend(yy, @selector(hello)) </code></pre> <p>接下来,我们开始验证这一结论的正确性,这是程序主要包含的代码:</p> <pre> <code class="language-objectivec">// XXObject.h #import <Foundation/Foundation.h> @interface XXObject : NSObject - (void)hello; @end // XXObject.m #import "XXObject.h" @implementation XXObject - (void)hello { NSLog(@"Hello"); } @end // main.m #import <Foundation/Foundation.h> #import "XXObject.h" int main(int argc, const char * argv[]) { @autoreleasepool { XXObject *object = [[XXObject alloc] init]; [object hello]; } return 0; } </code></pre> <p>在主函数任意位置打一个断点, 比如 -> [object hello]; 这里,然后在 lldb 中输入:</p> <p><img src="https://simg.open-open.com/show/5252c61c56299bde90ae3a836fc3f706.jpg"></p> <p>这里面我们打印了两个选择子的地址 @selector(hello) 以及 @selector(undefined_hello_method) ,需要注意的是:</p> <p>@selector(hello) 是在编译期间就声明的选择子,而后者在编译期间并不存在, undefined_hello_method 选择子由于是在运行时生成的,所以内存地址明显比 hello 大很多</p> <p>如果我们修改程序的代码:</p> <p><img src="https://simg.open-open.com/show/3b89b09d6b3512f7b0c814cc5797a053.jpg"></p> <p>在这里,由于我们在代码中显示地写出了 @selector(undefined_hello_method) ,所以在 lldb 中再次打印这个 sel 内存地址跟之前相比有了很大的改变。</p> <p>更重要的是,我没有通过指针的操作来获取 hello 选择子的内存地址,而只是通过 @selector(hello) 就可以返回一个选择子。</p> <p>从上面的这些现象,可以推断出选择子有以下的特性:</p> <ol> <li>Objective-C 为我们维护了一个巨大的选择子表</li> <li>在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL 。如果没有找到,则会生成一个 SEL 并添加到表中</li> <li>在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中</li> </ol> <p>在运行时初始化之前,打印 hello 选择子的的内存地址:</p> <p><img src="https://simg.open-open.com/show/7927f779508279eb6d479bdf8f6692c2.jpg"></p> <h2>message.h 文件</h2> <p>Objective-C 中 objc_msgSend 的实现并没有开源,它只存在于 message.h 这个头文件中。</p> <pre> <code class="language-objectivec">/** * @note When it encounters a method call, the compiler generates a call to one of the * functions \c objc_msgSend, \c objc_msgSend_stret, \c objc_msgSendSuper, or \c objc_msgSendSuper_stret. * Messages sent to an object’s superclass (using the \c super keyword) are sent using \c objc_msgSendSuper; * other messages are sent using \c objc_msgSend. Methods that have data structures as return values * are sent using \c objc_msgSendSuper_stret and \c objc_msgSend_stret. */ OBJC_EXPORT id objc_msgSend(id self, SEL op, ...) </code></pre> <p>在这个头文件的注释中对 <strong>消息发送的一系列方法</strong> 解释得非常清楚:</p> <p>当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个 objc_msgSend 、 objc_msgSend_stret 、 objc_msgSendSuper 和 objc_msgSendSuper_stret 。 发送给对象的父类的消息会使用 objc_msgSendSuper 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret 或 objc_msgSend_stret 其它的消息都是使用 objc_msgSend 发送的</p> <p>在这篇文章中,我们只会对 <strong>消息发送的过程</strong> 进行分析,而不会对 <strong>上述消息发送方法的区别</strong> 进行分析,默认都使用 objc_msgSend 函数。</p> <h2>objc_msgSend 调用栈</h2> <p>这一小节会以向 XXObject 的实例发送 hello 消息为例,在 Xcode 中观察整个消息发送的过程中调用栈的变化,再来看一下程序的代码:</p> <pre> <code class="language-objectivec">// XXObject.h #import <Foundation/Foundation.h> @interface XXObject : NSObject - (void)hello; @end // XXObject.m #import "XXObject.h" @implementation XXObject - (void)hello { NSLog(@"Hello"); } @end // main.m #import <Foundation/Foundation.h> #import "XXObject.h" int main(int argc, const char * argv[]) { @autoreleasepool { XXObject *object = [[XXObject alloc] init]; [object hello]; } return 0; } </code></pre> <p>在调用 hello 方法的这一行打一个断点,当我们尝试进入(Step in)这个方法只会直接跳入这个方法的实现,而不会进入 objc_msgSend :</p> <p><img src="https://simg.open-open.com/show/9711d28ab156f4e96d8d54a0daed0790.gif"></p> <p>因为 objc_msgSend 是一个私有方法,我们没有办法进入它的实现,但是,我们却可以在 objc_msgSend 的调用栈中“截下”这个函数调用的过程。</p> <p>调用 objc_msgSend 时,传入了 self 以及 SEL 参数。</p> <p>既然要执行对应的方法,肯定要寻找选择子对应的实现。</p> <p>在 objc-runtime-new.mm 文件中有一个函数 lookUpImpOrForward ,这个函数的作用就是查找方法的实现,于是运行程序,在运行到 hello 这一行时,激活 lookUpImpOrForward 函数中的断点。</p> <p><img src="https://simg.open-open.com/show/b8be71abddb7f554583165e845c60f12.jpg"></p> <p>由于转成 gif 实在是太大了,笔者试着用各种方法生成动图,然而效果也不是很理想,只能贴一个 油Tube 的视频链接,不过对于能够KX上网的开发者们,应该也不是什么问题吧(手动微笑)</p> <p>如果跟着视频看这个方法的调用栈有些混乱的话,也是正常的。在下一个节中会对其调用栈进行详细的分析。</p> <h2>解析 objc_msgSend</h2> <p>对 objc_msgSend 解析总共分两个步骤,我们会向 XXObject 的实例发送两次 hello 消息,分别模拟无缓存和有缓存两种情况下的调用栈。</p> <h2>无缓存</h2> <p>在 -> [object hello] 这里增加一个断点, <strong>当程序运行到这一行时</strong> ,再向 lookUpImpOrForward 函数的第一行添加断点,确保是捕获 @selector(hello) 的调用栈,而不是调用其它选择子的调用栈。</p> <p><img src="https://simg.open-open.com/show/d5554d981bb62e97ca12e5b679840ba7.png"></p> <p>由图中的变量区域可以了解,传入的选择子为 "hello" ,对应的类是 XXObject 。所以我们可以确信这就是当调用 hello 方法时执行的函数。在 Xcode 左侧能看到方法的调用栈:</p> <pre> <code class="language-objectivec">0 lookUpImpOrForward 1 _class_lookupMethodAndLoadCache3 2 objc_msgSend 3 main 4 start </code></pre> <p>调用栈在这里告诉我们: lookUpImpOrForward 并不是 objc_msgSend 直接调用的,而是通过 _class_lookupMethodAndLoadCache3 方法:</p> <pre> <code class="language-objectivec">IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } </code></pre> <p>这是一个 <strong>仅提供给派发器(dispatcher)</strong> 用于方法查找的函数,其它的代码都应该使用 lookUpImpOrNil() (不会进行方法转发)。 _class_lookupMethodAndLoadCache3 会传入 cache = NO 避免在 <strong>没有加锁</strong> 的时候对缓存进行查找,因为派发器已经做过这件事情了。</p> <h3>实现的查找 lookUpImpOrForward</h3> <p>由于实现的查找方法 lookUpImpOrForward 涉及很多函数的调用,所以我们将它分成以下几个部分来分析:</p> <ol> <li>无锁的缓存查找</li> <li>如果类没有实现(isRealized)或者初始化(isInitialized),实现或者初始化类</li> <li>加锁</li> <li>缓存以及当前类中方法的查找</li> <li>尝试查找父类的缓存以及方法列表</li> <li>没有找到实现,尝试方法解析器</li> <li>进行消息转发</li> <li>解锁、返回实现</li> </ol> <p>无锁的缓存查找</p> <p>下面是在没有加锁的时候对缓存进行查找,提高缓存使用的性能:</p> <pre> <code class="language-objectivec">runtimeLock.assertUnlocked(); // Optimistic cache lookup if (cache) { imp = cache_getImp(cls, sel); if (imp) return imp; } </code></pre> <p>不过因为 _class_lookupMethodAndLoadCache3 传入的 cache = NO ,所以这里会直接跳过 if 中代码的执行,在 objc_msgSend 中已经使用汇编代码查找过了。</p> <p>类的实现和初始化</p> <p>在 <em>Objective-C 运行时</em> 初始化的过程中会对其中的类进行第一次初始化也就是执行 realizeClass 方法,为类分配可读写结构体 class_rw_t 的空间,并返回正确的类结构体。</p> <p>而 _class_initialize 方法会调用类的 initialize 方法,我会在之后的文章中对类的初始化进行分析。</p> <pre> <code class="language-objectivec">if (!cls->isRealized()) { rwlock_writer_t lock(runtimeLock); realizeClass(cls); } if (initialize && !cls->isInitialized()) { _class_initialize (_class_getNonMetaClass(cls, inst)); } </code></pre> <p>加锁</p> <p>加锁这一部分只有一行简单的代码,其主要目的保证方法查找以及缓存填充(cache-fill)的原子性,保证在运行以下代码时不会有 <strong>新方法添加导致缓存被冲洗(flush)</strong> 。</p> <pre> <code class="language-objectivec">runtimeLock.read(); </code></pre> <p>在当前类中查找实现</p> <p>实现很简单,先调用了 cache_getImp 从某个类的 cache 属性中获取选择子对应的实现:</p> <pre> <code class="language-objectivec">imp = cache_getImp(cls, sel); if (imp) goto done; </code></pre> <p><img src="https://simg.open-open.com/show/413ee28ded4a358626ed52e56c35d778.png"></p> <p>不过 cache_getImp 的实现目测是不开源的,同时也是汇编写的,在我们尝试 step in 的时候进入了如下的汇编代码。</p> <p><img src="https://simg.open-open.com/show/32f09300b6ee641f9c66afb9842714da.jpg"></p> <p>它会进入一个 CacheLookup 的标签,获取实现,使用汇编的原因还是因为要加速整个实现查找的过程,其原理推测是在类的 cache 中寻找对应的实现,只是做了一些性能上的优化。</p> <p>如果查找到实现,就会跳转到 done 标签,因为我们在这个小结中的假设是无缓存的(第一次调用 hello 方法),所以会进入下面的代码块,从类的方法列表中寻找方法的实现:</p> <pre> <code class="language-objectivec">meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } </code></pre> <p>调用 getMethodNoSuper_nolock 方法查找对应的方法的结构体指针 method_t :</p> <pre> <code class="language-objectivec">static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) { for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { method_t *m = search_method_list(*mlists, sel); if (m) return m; } return nil; } </code></pre> <p>因为类中数据的方法列表 methods 是一个二维数组 method_array_t ,写一个 for 循环遍历整个方法列表,而这个 search_method_list 的实现也特别简单:</p> <pre> <code class="language-objectivec">static method_t *search_method_list(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) { return findMethodInSortedMethodList(sel, mlist); } else { for (auto& meth : *mlist) { if (meth.name == sel) return &meth; } } return nil; } </code></pre> <p>findMethodInSortedMethodList 方法对有序方法列表进行线性探测,返回方法结构体 method_t 。</p> <p>如果在这里找到了方法的实现,将它加入类的缓存中,这个操作最后是由 cache_fill_nolock 方法来完成的:</p> <pre> <code class="language-objectivec">static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { if (!cls->isInitialized()) return; if (cache_getImp(cls, sel)) return; cache_t *cache = getCache(cls); cache_key_t key = getKey(sel); mask_t newOccupied = cache->occupied() + 1; mask_t capacity = cache->capacity(); if (cache->isConstantEmptyCache()) { cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { } else { cache->expand(); } bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); bucket->set(key, imp); } </code></pre> <p>如果缓存中的内容大于容量的 3/4 就会扩充缓存,使缓存的大小翻倍。</p> <p>在缓存翻倍的过程中,当前类 <strong>全部的缓存都会被清空</strong> ,Objective-C 出于性能的考虑不会将原有缓存的 bucket_t 拷贝到新初始化的内存中。</p> <p>找到第一个空的 bucket_t ,以 (SEL, IMP) 的形式填充进去。</p> <p>在父类中寻找实现</p> <p>这一部分与上面的实现基本上是一样的,只是多了一个循环用来判断根类:</p> <ol> <li>查找缓存</li> <li>搜索方法列表</li> </ol> <pre> <code class="language-objectivec">curClass = cls; while ((curClass = curClass->superclass)) { imp = cache_getImp(curClass, sel); if (imp) { if (imp != (IMP)_objc_msgForward_impcache) { log_and_fill_cache(cls, imp, sel, inst, curClass); goto done; } else { break; } } meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } } </code></pre> <p>与当前类寻找实现的区别是:在父类中寻找到的 _objc_msgForward_impcache 实现会交给当前类来处理。</p> <p>方法决议</p> <p>选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve)的过程:</p> <pre> <code class="language-objectivec">if (resolver && !triedResolver) { _class_resolveMethod(cls, sel, inst); triedResolver = YES; goto retry; } </code></pre> <p>这部分代码调用 _class_resolveMethod 来解析没有找到实现的方法。</p> <pre> <code class="language-objectivec">void _class_resolveMethod(Class cls, SEL sel, id inst) { if (! cls->isMetaClass()) { _class_resolveInstanceMethod(cls, sel, inst); } else { _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); } } } </code></pre> <p>根据当前的类是不是 <a href="/misc/goto?guid=4959671675064298101" rel="nofollow,noindex">元类</a> 在 _class_resolveInstanceMethod 和 _class_resolveClassMethod 中选择一个进行调用。</p> <pre> <code class="language-objectivec">static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) { if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { // 没有找到 resolveInstanceMethod: 方法,直接返回。 return; } BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend; bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // 缓存结果,以防止下次在调用 resolveInstanceMethod: 方法影响性能。 IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); } </code></pre> <p>这两个方法的实现其实就是判断当前类是否实现了 resolveInstanceMethod: 或者 resolveClassMethod: 方法,然后用 objc_msgSend 执行上述方法,并传入需要决议的选择子。</p> <p>关于 resolveInstanceMethod 之后可能会写一篇文章专门介绍,不过关于这个方法的文章也确实不少,在 Google 上搜索会有很多的文章。</p> <p>在执行了 resolveInstanceMethod: 之后,会跳转到 retry 标签, <strong>重新执行查找方法实现的流程</strong> ,只不过不会再调用 resolveInstanceMethod: 方法了(将 triedResolver 标记为 YES )。</p> <p>消息转发</p> <p>在缓存、当前类、父类以及 resolveInstanceMethod: 都没有解决实现查找的问题时,Objective-C 还为我们提供了最后一次翻身的机会,进行方法转发:</p> <pre> <code class="language-objectivec">imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); </code></pre> <p>返回实现 _objc_msgForward_impcache ,然后加入缓存。</p> <p>====</p> <p>这样就结束了整个方法第一次的调用过程,缓存没有命中,但是在当前类的方法列表中找到了 hello 方法的实现,调用了该方法。</p> <p><img src="https://simg.open-open.com/show/56a375c6ffabc668f32b1129e7439a4c.png" alt="从源代码看 ObjC 中消息的发送" width="550" height="344"></p> <h2>缓存命中</h2> <p>如果使用对应的选择子时,缓存命中了,那么情况就大不相同了,我们修改主程序中的代码:</p> <pre> <code class="language-objectivec">int main(int argc, const char * argv[]) { @autoreleasepool { XXObject *object = [[XXObject alloc] init]; [object hello]; [object hello]; } return 0; } </code></pre> <p>然后在第二次调用 hello 方法时,加一个断点:</p> <p><img src="https://simg.open-open.com/show/15b9f15a3ab53a9b918fc68e5bc8aedf.gif"></p> <p>objc_msgSend 并没有走 lookupImpOrForward 这个方法,而是直接结束,打印了另一个 hello 字符串。</p> <p>我们如何确定 objc_msgSend 的实现到底是什么呢?其实我们没有办法来 <strong>确认</strong> 它的实现,因为这个函数的实现使用汇编写的,并且实现是不开源的。</p> <p>不过,我们需要确定它是否真的 <strong>访问了类中的缓存</strong> 来加速实现寻找的过程。</p> <p>好,现在重新运行程序至第二个 hello 方法调用之前:</p> <p><img src="https://simg.open-open.com/show/9eac4de675769ad6d31d3f1f2b85d7ad.jpg"></p> <p>打印缓存中 bucket 的内容:</p> <pre> <code class="language-objectivec">(lldb) p (objc_class *)[XXObject class] (objc_class *) $0 = 0x0000000100001230 (lldb) p (cache_t *)0x0000000100001240 (cache_t *) $1 = 0x0000000100001240 (lldb) p *$1 (cache_t) $2 = { _buckets = 0x0000000100604bd0 _mask = 3 _occupied = 2 } (lldb) p $2.capacity() (mask_t) $3 = 4 (lldb) p $2.buckets()[0] (bucket_t) $4 = { _key = 0 _imp = 0x0000000000000000 } (lldb) p $2.buckets()[1] (bucket_t) $5 = { _key = 0 _imp = 0x0000000000000000 } (lldb) p $2.buckets()[2] (bucket_t) $6 = { _key = 4294971294 _imp = 0x0000000100000e60 (debug-objc`-[XXObject hello] at XXObject.m:17) } (lldb) p $2.buckets()[3] (bucket_t) $7 = { _key = 4300169955 _imp = 0x00000001000622e0 (libobjc.A.dylib`-[NSObject init] at NSObject.mm:2216) } </code></pre> <p>在这个缓存中只有对 hello 和 init 方法实现的缓存,我们要将其中 hello 的缓存清空:</p> <pre> <code class="language-objectivec">(lldb) expr $2.buckets()[2] = $2.buckets()[1] (bucket_t) $8 = { _key = 0 _imp = 0x0000000000000000 } </code></pre> <p><img src="https://simg.open-open.com/show/972d129926c32c18dedfb52f0eb6ea19.jpg"></p> <p>这样 XXObject 中就不存在 hello 方法对应实现的缓存了。然后继续运行程序:</p> <p><img src="https://simg.open-open.com/show/ba5042fa860a293c7ced9c1a67c95151.jpg"></p> <p>虽然第二次调用 hello 方法,但是因为我们清除了 hello 的缓存,所以,会再次进入 lookupImpOrForward 方法。</p> <p>下面会换一种方法验证猜测: <strong>在 hello 调用之前添加缓存</strong> 。</p> <p>添加一个新的实现 cached_imp :</p> <pre> <code class="language-objectivec">#import <Foundation/Foundation.h> #import <objc/runtime.h> #import "XXObject.h" int main(int argc, const char * argv[]) { @autoreleasepool { __unused IMP cached_imp = imp_implementationWithBlock(^() { NSLog(@"Cached Hello"); }); XXObject *object = [[XXObject alloc] init]; [object hello]; [object hello]; } return 0; } </code></pre> <p>我们将以 @selector(hello), cached_imp 为键值对,将其添加到类结构体的缓存中,这里的实现 cached_imp 有一些区别,它会打印 @"Cached Hello" 而不是 @"Hello" 字符串:</p> <p>在第一个 hello 方法调用之前将实现加入缓存:</p> <p><img src="https://simg.open-open.com/show/275a5135758a756ba3dfcfae4ba7c57d.jpg"></p> <p>然后继续运行代码:</p> <p><img src="https://simg.open-open.com/show/2c311165676cca82c6f1322b6787aa5e.jpg"></p> <p>可以看到,我们虽然没有改变 hello 方法的实现,但是在 <strong>objc_msgSend</strong> 的消息发送链路中,使用错误的缓存实现 cached_imp 拦截了实现的查找,打印出了 Cached Hello 。</p> <p>由此可以推定, objc_msgSend 在实现中确实检查了缓存。如果没有缓存会调用 lookupImpOrForward 进行方法查找。</p> <p>为了提高消息传递的效率,ObjC 对 objc_msgSend 以及 cache_getImp 使用了汇编语言来编写。</p> <p>如果你想了解有关 objc_msgSend 方法的汇编实现的信息,可以看这篇文章 <a href="/misc/goto?guid=4959671675146096267" rel="nofollow,noindex">Let's Build objc_msgSend</a></p> <h2>小结</h2> <p>这篇文章与其说是讲 ObjC 中的消息发送的过程,不如说是讲方法的实现是如何查找的。</p> <p>Objective-C 中实现查找的路径还是比较符合直觉的:</p> <ol> <li>缓存命中</li> <li>查找当前类的缓存及方法</li> <li>查找父类的缓存及方法</li> <li>方法决议</li> <li>消息转发</li> </ol> <p>文章中关于方法调用栈的视频最开始是用 gif 做的,不过由于 gif 时间较长,试了很多的 gif 转换器,都没有得到一个较好的质量和合适的大小,所以最后选择用一个 油Tube 的视频。</p> <h2>参考资料</h2> <ul> <li><a href="/misc/goto?guid=4959671675237795408" rel="nofollow,noindex">深入解析 ObjC 中方法的结构</a></li> <li><a href="/misc/goto?guid=4958863526773227196" rel="nofollow,noindex">Objective-C Runtime</a></li> <li><a href="/misc/goto?guid=4959671675146096267" rel="nofollow,noindex">Let's Build objc_msgSend</a></li> </ul> <p>来自: <a href="/misc/goto?guid=4959671675339674810" rel="nofollow">http://draveness.me/message/</a> </p>