OC底層原理07:消息流程分析之慢速查找過程

慢速查找

當objc_msgSend在緩存cache中找不到方法時,即源碼中執行CheckMissJumpMiss,就會進行慢速查找。 git

CheckMiss 爲loop循環中,bucket->sel未命中時調用的方法。JumpMiss爲結束遞歸後,仍舊查找到了第一個bucket時,進行調用的方法。緩存

CheckMissJumpMiss源碼中,都會因NORMAL而進行__objc_msgSend_uncached的操做。安全

查找__objc_msgSend_uncached實現:markdown

繼續查找MethodTableLookup多線程

當查詢到_lookUpImpOrForward時,就繼續搜索不到了。是由於從底層彙編往上層C/C++的代碼進行調用實現,即進行慢速查找過程。app

快速查找是在緩存中進行方法的查找;慢速查找是從類信息data()中,及其父類鏈中進行方法的查找。ide


lookUpImpOrForward解析

全局搜一下lookUpImpOrForward函數

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{  const IMP forward_imp = (IMP)_objc_msgForward_impcache;  IMP imp = nil;  Class curClass;   runtimeLock.assertUnlocked();   // Optimistic cache lookup  if (fastpath(behavior & LOOKUP_CACHE)) {  imp = cache_getImp(cls, sel);  if (imp) goto done_nolock;  }   // runtimeLock is held during isRealized and isInitialized checking  // to prevent races against concurrent realization.   // runtimeLock is held during method search to make  // method-lookup + cache-fill atomic with respect to method addition.  // Otherwise, a category could be added but ignored indefinitely because  // the cache was re-filled with the old value after the cache flush on  // behalf of the category.   runtimeLock.lock();   // We don't want people to be able to craft a binary blob that looks like  // a class but really isn't one and do a CFI attack.  //  // To make these harder we want to make sure this is a class that was  // either built into the binary or legitimately registered through  // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.  //  // TODO: this check is quite costly during process startup.  checkIsKnownClass(cls);   if (slowpath(!cls->isRealized())) {  // 若是當前類沒有實現,先要確認類及其父類鏈和方法屬性等  cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);  // runtimeLock may have been dropped but is now locked again  }   if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {  cls = initializeAndLeaveLocked(cls, inst, runtimeLock);  // runtimeLock may have been dropped but is now locked again   // If sel == initialize, class_initialize will send +initialize and   // then the messenger will send +initialize again after this   // procedure finishes. Of course, if this is not being called   // from the messenger then it won't happen. 2778172  }   runtimeLock.assertLocked();  curClass = cls;   // The code used to lookpu the class's cache again right after  // we take the lock but for the vast majority of the cases  // evidence shows this is a miss most of the time, hence a time loss.  //  // The only codepath calling into this without having performed some  // kind of cache lookup is class_getInstanceMethod().   for (unsigned attempts = unreasonableClassCount();;) {  // curClass method list.  Method meth = getMethodNoSuper_nolock(curClass, sel);  if (meth) {  imp = meth->imp;  goto done;  }   if (slowpath((curClass = curClass->superclass) == nil)) {  // No implementation found, and method resolver didn't help.  // Use forwarding.  imp = forward_imp;  break;  }   // Halt if there is a cycle in the superclass chain.  if (slowpath(--attempts == 0)) {  _objc_fatal("Memory corruption in class list.");  }   // Superclass cache.  imp = cache_getImp(curClass, sel);  if (slowpath(imp == forward_imp)) {  // Found a forward:: entry in a superclass.  // Stop searching, but don't cache yet; call method  // resolver for this class first.  break;  }  if (fastpath(imp)) {  // Found the method in a superclass. Cache it in this class.  goto done;  }  }   // No implementation found. Try method resolver once.   if (slowpath(behavior & LOOKUP_RESOLVER)) {  behavior ^= LOOKUP_RESOLVER;  return resolveMethod_locked(inst, sel, cls, behavior);  }   done:  log_and_fill_cache(cls, imp, sel, inst, curClass);  runtimeLock.unlock();  done_nolock:  if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {  return nil;  }  return imp; } 複製代碼

1.
if (fastpath(behavior & LOOKUP_CACHE)) {
    imp = cache_getImp(cls, sel);
    if (imp) goto done_nolock;
}
複製代碼

該判斷,是爲了防止在多線程操做時,恰好調用到lookUpImpOrForward時,緩存中insert了要查找的方法,那麼能夠直接從緩存獲取imp,並goto done_nolock,畢竟慢速查找是個耗時的過程,蘋果儘可能不想太過耗時。oop


2.
runtimeLock.lock();
複製代碼

加鎖,從註釋也能夠看到,是爲了保證操做時的線程安全問題。post


3.
checkIsKnownClass(cls);
複製代碼

確保傳入的類是合法註冊的類,是否已經加載過。


4.
if (slowpath(!cls->isRealized())) {
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
複製代碼

若是當前類沒有實現,若是沒有,則要實現並確認其父類鏈和方法屬性等。

而在realizeClassWithoutSwift方法中,會確認當前類的父類鏈

...
supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
...
cls->superclass = supercls;
cls->initClassIsa(metacls);
...
// Connect this class to its superclass's subclass lists
if (supercls) {
    addSubclass(supercls, cls);
} else {
    addRootClass(cls);
}
複製代碼

而且會調用到methodizeClass,關聯類信息中的數據。

5.
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
複製代碼

內部會有一個遞歸函數,判斷類及其父類是否實現isInitialized,沒有則會先進行類的初始化。

以上前幾段代碼,都是慢速查找的準備工做,此後的for死循環是關鍵部分。


慢速查找for循環

for (unsigned attempts = unreasonableClassCount();;) 這個for循環沒有判斷條件等,它是個死循環。

1.
Method meth = getMethodNoSuper_nolock(curClass, sel);
複製代碼

經過getMethodNoSuper_nolock -> search_method_list_inline -> findMethodInSortedMethodList

2.
if (meth) {
    imp = meth->imp;
    goto done;
}
...
done:
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
複製代碼

當二分查找到方法時,會調用到log_and_fill_cache log_and_fill_cache會調用到cache_fill緩存填充,而在cache_fill中,會調用到cache->insert方法。

類結構cache中詳述了方法插入到緩存的過程。


3
if (slowpath((curClass = curClass->superclass) == nil)) {
    // No implementation found, and method resolver didn't help.
    // Use forwarding.
    imp = forward_imp;
    break;
}
複製代碼

當二分查找沒有找到時,此時curClass會被賦值它的父類superclass,並判斷是否爲nil,是的話imp被賦值forward_imp後跳出循環,不然繼續向下。

往上看forward_imp

全局搜_objc_msgForward_impcache,在彙編代碼objc-msg-arm64.s 再查找到C++代碼objc-runtime.mm 看到了常見的unrecognized selector sent to instance方法未找到。

那麼該段代碼的含義就是,當根據類及其父類直到根類,都沒有找到方法時,objc_msgSend消息轉發到了objc_defaultForwardHandler


4. 重點中的重點
// Superclass cache.
imp = cache_getImp(curClass, sel);
複製代碼

由第三步,此時的curClass是被賦值了它的父類,而且父類不爲nilcache_getImp就會從父類的緩存中進行查找。cache中查找,一樣咱們須要到彙編中搜索其實現代碼。

和快速查找相似,一樣會調用到CacheLookup,不過傳遞的參數爲GETIMP而不是快速查找時的NORMALCacheLoopup的過程再也不贅述,重點看當在緩存中也找不到時,CheckMissJumpMiss都會跳轉到LGetImpMiss,其中只有返回了空。

彙編cache_getImp返回空沒有從父類的緩存中找到方法,則會繼續for死循環,此時的curClass變成了最初類的父類,會再次調用到Method meth = getMethodNoSuper_nolock(curClass, sel);,從父類的類信息中進行慢速查找,直到找到跳出循環,或者找到根類的父類nil,進行消息轉發到unrecognized selector sent to instance


流程圖總結:

相關文章
相關標籤/搜索