前言
前面类之缓存我们讲到了缓存命中CacheHit的情况下的流程,那如果没有命中缓存呢,我们发现它调用了__objc_msgSend_uncached,那它又做了什么呢,我们这里就主要分析下它。
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
我们看到接下来会调用MethodTableLookup;
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
lookUpImpOrForward慢速查找方法
我们看到这里只有一个比较重要的跳转,其他都是简单的取值赋值,我们接着重点看_lookUpImpOrForward,这个函数带下划线标志着我们又要回到C++源码了,所以我们接着源码找lookUpImpOrForward,在objc-runtime-new.mm文件找到该函数的实现:
上图中checkIsKnownClass(cls)和realizeAndInitializeIfNeeded_locked()都是在为我们去类的methodlist查找方法做好初始化和其他一切准备,包括类、元类、父类等相关类的初始化,我们继续往下看:
我们现在按正常流程看的话,共享缓存暂时当做没有值,直接看getMethodNoSuper_nolock():
我们记得当时在类的methodlist找方法时它是个二维数组结构的,所以是遍历两层寻找的,我们继续看search_method_list_inline()函数:
二分查找
接着我们来看下函数findMethodInsortedMethodList(),这里用到一个很经典的二分查找算法:
我们为了方便讲解这里的算法,可以假设一个count值,假设count现在是10,这时候base=0,keyValue是我们要查找的sel,probe是我们从list取出用来进行比较的sel,经过probe=base+(count >> 1),probe现在就为5,若keyValue==probeValue,执行一个while循环,这里的循环是为了找到first-probe之间跟KeyValue同名的sel,然后返回,其实是为了优先找到category方法进行调用。
接着看下面,若keyValue > probeValue,base=probe(5)+1,base=6,我们可以看到完美的避过了之前算过的5,此时我们要取5-10之间的值,此时count--同时count>>1,count=4,此时再次得到probe=base+count>>1,结果为8;当我们下次再进入keyValue>probeValue时,此时base=probe+1,值为9,完美避过了之前算过的8,这里的二分查找涉及的很经典,我们如果写二分查找的算法,可以参考这里。
这里找到method后,就会执行goto done:
我们看到最后就执行了cache.insert(),插入缓存,下次我们再调用该方法时,就会执行快速查找流程CacheLookup从缓存中查找到该方法。
若从当前类的methodlist没找到该方法呢,接着就会从父类找:
首先从父类缓存找,还是那套快速查找流程CacheLookup,若父类缓存没有命中CacheHit,接着在父类的methodlist里查找,走lookUpImpOrForward慢速查找流程,若最终还是没找到,就调用forward_imp进行消息转发。当然如果是类方法,那就是走元类的快速查找流程,然后走慢速查找。当然我们要清楚这里是个死循环,直到满足某个条件跳出当前循环,curClass->getSuperclass()会一直向上找,直到父类不存在: cls-> superclass -> NSObject,所以它最终会找到NSObject这里。
总结
我们这里就可以总结下objc_msgSend消息发送的整套查找流程:首先就是从当前类的缓存里快速查找(CacheLookup)该方法,这里走的汇编那套代码,效率很高,若是在缓存里没有找到,接下来会调用__objc_msgSend_uncached->lookUpImpOrForward进入慢速查找流程,这里首先还是会进入共享缓存查找,因为我们在查找的过程中,可能有执行的方法存入;若在共享缓存没有找到,接着进入当前类的methodlist查找(二分查找),若找到该方法,调用log_and_fill_cache->cache.insert()把当前方法插入缓存中,下次再次调用该方法时,就会从缓存中快速查找到。
若没有从当前类的methodlist找到该方法,则从父类的缓存中查找CacheLookup,若缓存没有,就走父类的慢速查找流程lookUpImpOrForward,若最终还是没找到,调用forward_imp进行消息转发。