Runtime原始碼 方法呼叫的過程
Objective-C語言的一大特性就是動態的,根據官方文件的描述:在runtime之前,訊息和方法並不是繫結在一起的,編譯器會把方法呼叫轉換為 objc_msgSend(receiver, selector)
,如果方法中帶有引數則轉換為 objc_msgSend(receiver, selector, arg1, arg2, ...)
接下來我們通過原始碼一窺究竟,在次之前我們先了解幾個基本概念
- SEL 在objc.h檔案中我們可以看到如下程式碼:
/// An opaque type that represents a method selector. typedef struct objc_selector *SEL; 複製程式碼
SEL其實就是一個不透明的型別它代表一個方法選擇子,在編譯期,會根據方法名字生成一個ID。
- IMP 在objc.h檔案中我們可以看到IMP:
/// A pointer to the function of a method implementation. #if !OBJC_OLD_DISPATCH_PROTOTYPES typedef void (*IMP)(void /* id, SEL, ... */ ); #else typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); #endif 複製程式碼
他是一個函式指標,指向方法實現的首地址。
- Method
/// An opaque type that represents a method in a class definition. typedef struct objc_method *Method; struct objc_method { SEL _Nonnull method_nameOBJC2_UNAVAILABLE; char * _Nullable method_typesOBJC2_UNAVAILABLE; IMP _Nonnull method_impOBJC2_UNAVAILABLE; }OBJC2_UNAVAILABLE; 複製程式碼
它儲存了SEL到IMP和方法型別,所以我們可以通過SEL呼叫對應的IMP
方法呼叫的流程
objc_msgSend的訊息分發分為以下幾個步驟: 我們找到objc _msgSend原始碼,都是彙編,不過註釋比較詳盡
/******************************************************************** * * id objc_msgSend(id self, SEL_cmd,...); * IMP objc_msgLookup(id self, SEL _cmd, ...); * * objc_msgLookup ABI: * IMP returned in r11 * Forwarding returned in Z flag * r10 reserved for our use but not used * ********************************************************************/ .data .align 3 .globl _objc_debug_taggedpointer_classes _objc_debug_taggedpointer_classes: .fill 16, 8, 0 .globl _objc_debug_taggedpointer_ext_classes _objc_debug_taggedpointer_ext_classes: .fill 256, 8, 0 ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame MESSENGER_START NilTestNORMAL GetIsaFast NORMAL// r10 = self->isa CacheLookup NORMAL, CALL// calls IMP on success NilTestReturnZero NORMAL GetIsaSupport NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r10 MESSENGER_END_SLOW jmp__objc_msgSend_uncached END_ENTRY _objc_msgSend ENTRY _objc_msgLookup NilTestNORMAL GetIsaFast NORMAL// r10 = self->isa CacheLookup NORMAL, LOOKUP// returns IMP on success NilTestReturnIMP NORMAL GetIsaSupport NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r10 jmp__objc_msgLookup_uncached END_ENTRY _objc_msgLookup ENTRY _objc_msgSend_fixup int3 END_ENTRY _objc_msgSend_fixup STATIC_ENTRY _objc_msgSend_fixedup // Load _cmd from the message_ref movq8(%a2), %a2 jmp_objc_msgSend END_ENTRY _objc_msgSend_fixedup 複製程式碼
就此我們大概可以瞭解到其呼叫流程:
-
判斷receiver是否為nil,也就是objc_msgSend的第一個引數self,也就是要呼叫的那個方法所屬物件
-
從快取裡尋找,找到了則分發,否則
-
利用objc-class.mm中_ class _lookupMethodAndLoadCache3方法去尋找selector
- 如果支援GC,忽略掉非GC環境的方法(retain等)
- 從本class的method list尋找selector,如果找到,填充到快取中,並返回selector,否則
- 尋找父類的method list,並依次往上尋找,直到找到selector,填充到快取中,並返回selector,否則
- 呼叫_class_resolveMethod,如果可以動態resolve為一個selector,不快取,方法返回,否則
- 轉發這個selector,否則
- 報錯,丟擲異常
這裡的**_ class _lookupMethodAndLoadCache3 其實就是對 lookUpImpOrForward**方法的呼叫:
/*********************************************************************** * _class_lookupMethodAndLoadCache. * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). * This lookup avoids optimistic cache scan because the dispatcher * already tried that. **********************************************************************/ IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } 複製程式碼
對第五個引數 cache 傳值為NO,因為在此之前已經做了一個查詢這裡 CacheLookup NORMAL, CALL ,這裡是對快取查詢的一個優化。
接下來看一下lookUpImpOrForward的一些關鍵實現細節
- 快取查詢優化
// Optimistic cache lookup if (cache) methodPC = _cache_getImp(cls, sel); if (methodPC) return methodPC; } 複製程式碼
這裡有個判斷,是否需要快取查詢,如果cache為NO則進入下一步
- 檢查被釋放類
// Check for freed class if (cls == _class_getFreedObjectClass()) return (IMP) _freedHandler; 複製程式碼
_class _getFreedObjectClass的實現:
/*********************************************************************** * _class_getFreedObjectClass.Return a pointer to the dummy freed * object class.Freed objects get their isa pointers replaced with * a pointer to the freedObjectClass, so that we can catch usages of * the freed object. **********************************************************************/ static Class _class_getFreedObjectClass(void) { return (Class)freedObjectClass; } 複製程式碼
註釋寫到,這裡返回的被釋放物件的指標,不是太理解,備註這以後再看看
- 懶載入+initialize
// Check for +initialize if (initialize&&!cls->isInitialized()) { _class_initialize (_class_getNonMetaClass(cls, inst)); // 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 } 複製程式碼
在方法呼叫過程中,如果類沒有被初始化的時候,會呼叫_class_initialize對類進行初始化,關於+initialize可以看之前的 ofollow,noindex">Runtime原始碼 +load 和 +initialize 。
- 加鎖保證原子性
// The lock is held 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. retry: methodListLock.lock(); // Try this class's cache. methodPC = _cache_getImp(cls, sel); if (methodPC) goto done; 複製程式碼
這裡又做了一次快取查詢,因為上一步執行了+initialize
加鎖這一部分只有一行簡單的程式碼,其主要目的保證方法查詢以及快取填充(cache-fill)的原子性,保證在執行以下程式碼時不會有新方法新增導致快取被沖洗(flush)。
- 本類的方法列表查詢
// Try this class's method lists. meth = _class_getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, cls, meth, sel); methodPC = method_getImplementation(meth); goto done; } 複製程式碼
這裡呼叫了 log_ and_ fill_cache 這個後面來看,接下里就是
- 父類方法列表查詢
// Try superclass caches and method lists. curClass = cls; while ((curClass = curClass->superclass)) { // Superclass cache. meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache); if (meth) { if (meth != (Method)1) { // Found the method in a superclass. Cache it in this class. log_and_fill_cache(cls, curClass, meth, sel); methodPC = method_getImplementation(meth); goto done; } else { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } } // Superclass method list. meth = _class_getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, curClass, meth, sel); methodPC = method_getImplementation(meth); goto done; } } 複製程式碼
關於訊息在列表方法查詢的過程,根據官方文件如下:

這裡沿著整合體系對父類的方法列表進行查詢,找到了就呼叫 log_ and_ fill_cache
log_ and_ fill_cach的實現:
記錄:
/*********************************************************************** * log_and_fill_cache * Log this method call. If the logger permits it, fill the method cache. * cls is the method whose cache should be filled. * implementer is the class that owns the implementation in question. **********************************************************************/ static void log_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel) { #if SUPPORT_MESSAGE_LOGGING if (objcMsgLogEnabled) { bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(), implementer->nameForLogging(), sel); if (!cacheIt) return; } #endif _cache_fill (cls, meth, sel); } 複製程式碼
內部呼叫了**_cache _fill**,填充快取:
/*********************************************************************** * _cache_fill.Add the specified method to the specified class' cache. * Returns NO if the cache entry wasn't added: cache was busy, *class is still being initialized, new entry is a duplicate. * * Called only from _class_lookupMethodAndLoadCache and * class_respondsToMethod and _cache_addForwardEntry. * * Cache locks: cacheUpdateLock must not be held. **********************************************************************/ bool _cache_fill(Class cls, Method smt, SEL sel) { uintptr_t newOccupied; uintptr_t index; cache_entry **buckets; cache_entry *entry; Cache cache; cacheUpdateLock.assertUnlocked(); // Never cache before +initialize is done if (!cls->isInitialized()) { return NO; } // Keep tally of cache additions totalCacheFills += 1; mutex_locker_t lock(cacheUpdateLock); entry = (cache_entry *)smt; cache = cls->cache; // Make sure the entry wasn't added to the cache by some other thread // before we grabbed the cacheUpdateLock. // Don't use _cache_getMethod() because _cache_getMethod() doesn't // return forward:: entries. if (_cache_getImp(cls, sel)) { return NO; // entry is already cached, didn't add new one } // Use the cache as-is if it is less than 3/4 full newOccupied = cache->occupied + 1; if ((newOccupied * 4) <= (cache->mask + 1) * 3) { // Cache is less than 3/4 full. cache->occupied = (unsigned int)newOccupied; } else { // Cache is too full. Expand it. cache = _cache_expand (cls); // Account for the addition cache->occupied += 1; } // Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. buckets = (cache_entry **)cache->buckets; for (index = CACHE_HASH(sel, cache->mask); buckets[index] != NULL; index = (index+1) & cache->mask) { // empty } buckets[index] = entry; return YES; // successfully added new cache entry } 複製程式碼
這裡還沒找到實現則進入下一步,動態方法解析和訊息轉發,關於訊息轉發的細節我們下篇再看。
方法快取
在上面截出的原始碼中我們多次看到了 cache ,下面我們就來看看這個,在 runtime.h
和 objc-runtime-new
cache的定義如下
struct objc_cache { unsigned int mask /* total = mask + 1 */OBJC2_UNAVAILABLE; unsigned int occupiedOBJC2_UNAVAILABLE; Method _Nullable buckets[1]OBJC2_UNAVAILABLE; }; 複製程式碼
struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; ... } 複製程式碼
這就是cache在runtime層面的表示,裡面的欄位和代表的含義類似
- buckets
陣列表示的hash表,每個元素代表一個方法快取 - mask
當前能達到的最大index(從0開始),,所以快取的size(total)是mask+1 - occupied
被佔用的槽位,因為快取是以散列表的形式存在的,所以會有空槽,而occupied表示當前被佔用的數目
而在_ buckets中包含了一個個的 cache_entry 和 bucket_t (objc2.0的變更):
typedef struct { SEL name;// same layout as struct old_method void *unused; IMP imp;// same layout as struct old_method } cache_entry; 複製程式碼
cache_entry定義也包含了三個欄位,分別是:
- name,被快取的方法名字
- unused,保留欄位,還沒被使用。
- imp,方法實現
struct bucket_t { private: cache_key_t _key; IMP _imp; ... } 複製程式碼
而bucket_t則沒有了老的unused,包含了兩個欄位:
- key,方法的標誌(和之前的name對應)
- imp, 方法的實現
後記
從runtime的原始碼我們知道了方法呼叫的流程和方法快取,有些附帶的問題答案也就呼之欲出了:
- 方法快取在元類的上,由第一節( Runtime原始碼 類、物件、isa )我們就知道在objc_class的isa指向了他的元類,所以每個類都只有一份方法快取,而不是每一個類的object都儲存一份。
- 在方法呼叫的父類方法列表查詢過程中,如果命中了也會呼叫
_cache_fill (cls, meth, sel);
,所以即便是從父類取到的方法,也會存在類本身的方法快取裡。而當用一個父類物件去呼叫那個方法的時候,也會在父類的metaclass裡快取一份。 - 快取容量限制,在上面的程式碼中我們注意到這個判斷:
// Use the cache as-is if it is less than 3/4 full mask_t newOccupied = cache->occupied() + 1; mask_t capacity = cache->capacity(); if (cache->isConstantEmptyCache()) { // Cache is read-only. Replace it. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // Cache is less than 3/4 full. Use it as-is. } else { // Cache is too full. Expand it. cache->expand(); } 複製程式碼
當cache為空時建立;當新的被佔用槽數小於等於其容量的3/4時,直接使用;否則呼叫 cache->expand();
擴充容量:
void cache_t::expand() { cacheUpdateLock.assertLocked(); uint32_t oldCapacity = capacity(); uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; if ((uint32_t)(mask_t)newCapacity != newCapacity) { // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } 複製程式碼
-
為什麼類的方法列表不直接做成散列表呢,做成list,還要單獨快取,多費事?
散列表是沒有順序的,Objective-C的方法列表是一個list,是有順序的 這個問題麼,我覺得有以下三個原因:
- Objective-C在查詢方法的時候會順著list依次尋找,並且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的順序就沒法保證。
- list的方法還儲存了除了selector和imp之外其他很多屬性。
- 散列表是有空槽的,會浪費空間。
相關資料:
美團酒旅博文: 深入理解Objective-C:方法快取
官方文件:Messaging