當objc_msgSend在緩存cache中找不到方法時,即源碼中執行CheckMiss
和JumpMiss
,就會進行慢速查找。 git
CheckMiss 爲loop循環中,
bucket->sel
未命中時調用的方法。JumpMiss爲結束遞歸後,仍舊查找到了第一個bucket時,進行調用的方法。緩存
CheckMiss
和JumpMiss
源碼中,都會因NORMAL
而進行__objc_msgSend_uncached
的操做。安全
查找__objc_msgSend_uncached
實現:markdown
繼續查找MethodTableLookup
:多線程
當查詢到_lookUpImpOrForward
時,就繼續搜索不到了。是由於從底層彙編往上層C/C++的代碼進行調用實現,即進行慢速查找過程。app
快速查找是在緩存中進行方法的查找;慢速查找是從類信息data()中,及其父類鏈中進行方法的查找。ide
全局搜一下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 (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
是被賦值了它的父類,而且父類不爲nil
。cache_getImp
就會從父類的緩存中進行查找。cache中查找,一樣咱們須要到彙編中搜索其實現代碼。
和快速查找相似,一樣會調用到CacheLookup
,不過傳遞的參數爲GETIMP
而不是快速查找時的NORMAL
。CacheLoopup
的過程再也不贅述,重點看當在緩存中也找不到時,CheckMiss
和JumpMiss
都會跳轉到LGetImpMiss
,其中只有返回了空。
彙編cache_getImp
返回空沒有從父類的緩存中找到方法,則會繼續for死循環
,此時的curClass
變成了最初類的父類,會再次調用到Method meth = getMethodNoSuper_nolock(curClass, sel);
,從父類的類信息中進行慢速查找,直到找到跳出循環,或者找到根類的父類nil
,進行消息轉發到unrecognized selector sent to instance
。
流程圖總結: