<簡書 — 劉小壯> https://www.jianshu.com/p/014af0de67cdgit
在OC中方法調用是經過Runtime
實現的,Runtime
進行方法調用本質上是發送消息,經過objc_msgSend()
函數進行消息發送。github
例以下面的OC代碼會被轉換爲Runtime
代碼。數組
原方法:[object testMethod] 轉換後的調用:objc_msgSend(object, @selector(testMethod));
發送消息的第二個參數是一個SEL
類型的參數,在項目裏常常會出現,不一樣的類定義了相同的方法,這樣就會有相同的SEL
。那麼問題就來了,也是不少人博客裏都問過的一個問題,不一樣類的SEL
是同一個嗎?緩存
然而,事實是經過咱們的驗證,建立兩個不一樣的類,並定義兩個相同的方法,經過@selector()
獲取SEL
並打印。咱們發現SEL
都是同一個對象,地址都是相同的。由此證實,不一樣類的相同SEL
是同一個對象。多線程
@interface TestObject : NSObject - (void)testMethod; @end @interface TestObject2 : NSObject - (void)testMethod; @end // TestObject2實現文件也同樣 @implementation TestObject - (void)testMethod { NSLog(@"TestObject testMethod %p", @selector(testMethod)); } @end // 結果: TestObject testMethod 0x100000f81 TestObject2 testMethod 0x100000f81
在Runtime
中維護了一個SEL
的表,這個表存儲SEL
不按照類來存儲,只要相同的SEL
就會被看作一個,並存儲到表中。在項目加載時,會將全部方法都加載到這個表中,而動態生成的方法也會被加載到表中。架構
咱們在方法內部能夠經過
self
獲取到當前對象,可是self
又是從哪來的呢?函數
方法實現的本質也是C函數,C函數除了方法傳入的參數外,還會有兩個默認參數,這兩個參數在經過objc_msgSend()
調用時也會傳入。這兩個參數在Runtime
中並無聲明,而是在編譯時自動生成的。源碼分析
從objc_msgSend
的聲明中能夠看出這兩個隱藏參數的存在。佈局
objc_msgSend(void /* id self, SEL op, ... */ )
self
,調用當前方法的對象。_cmd
,當前被調用方法的SEL
。雖然這兩個參數在調用和實現方法中都沒有明確聲明,可是咱們仍然可使用它。響應對象就是self
,被調用方法的selector
是_cmd
。優化
- (void)method { id target = getTheReceiver(); SEL method = getTheMethod(); if ( target == self || method == _cmd ) return nil; return [target performSelector:method]; }
一個對象被建立後,自身的類及其父類一直到NSObject
類的部分,都會包含在對象的內存中,例如其父類的實例變量。當經過[super class]
的方式調用其父類的方法時,會建立一個結構體。
struct objc_super { id receiver; Class class; };
對super
的調用會被轉化爲objc_msgSendSuper()
的調用,並在其內部調用objc_msgSend()
函數。有一點須要注意,儘管是經過[super class]
的方式調用的,但傳入的receiver
對象仍然是self
,返回結果也是self
的class
。由此可知,當前對象不管調用任何方法,receiver都是當前對象。
objc_msgSend(objc_super->receiver, @selector(class))
在objc_msg.s
中,存在多個版本的objc_msgSend
函數。內部實現邏輯大致一致,都是經過彙編實現的,只是根據不一樣的狀況有不一樣的調用。
objc_msgSend objc_msgSend_fpret objc_msgSend_fp2ret objc_msgSend_stret objc_msgSendSuper objc_msgSendSuper_stret objc_msgSendSuper2 objc_msgSendSuper2_stret
在上面源碼中,帶有super
的會在外界傳入一個objc_super
的結構體對象。stret
表示返回的是struct
類型,super2
是objc_msgSendSuper()
的一種實現方式,不對外暴露。
struct objc_super { id receiver; Class class; };
fp
則表示返回一個long double
的浮點型,而fp2
則返回一個complex long double
的複雜浮點型,其餘float
、double
的普通浮點型都用objc_msgSend
。除了上面這些狀況外,其餘都經過objc_msgSend()
調用。
當一個對象被建立時,系統會爲其分配內存,並完成默認的初始化工做,例如對實例變量進行初始化。對象第一個變量是指向其類對象的指針-isa
,isa
指針能夠訪問其類對象,而且經過其類對象擁有訪問其全部繼承者鏈中的類。
isa
指針不是語言的一部分,主要爲Runtime
機制提供服務。
當對象接收到一條消息時,消息函數隨着對象isa
指針到類的結構體中,在method list
中查找方法selector
。若是在本類中找不到對應的selector
,則objc_msgSend
會向其父類的method list
中查找selector
,若是還不能找到則沿着繼承關係一直向上查找,直到找到NSObject
類。
Runtime
在selector
查找的過程作了優化,爲類的結構體中增長了cache
字段,每一個類都有獨立的cache
,在一個selector
被調用後就會加入到cache
中。在每次搜索方法列表以前,都會先檢查cache
中有沒有,若是沒有才調用方法列表,這樣會提升方法的查找效率。
若是經過OC代碼的調用都會走消息發送的階段,若是不想要消息發送的過程,能夠獲取到方法的函數指針直接調用。經過NSObject
的methodForSelector:
方法能夠獲取到函數指針,獲取到指針後須要對指針進行類型轉換,轉換爲和調用函數相符的函數指針,而後發起調用便可。
void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES);
在Runtime
中,objc_msgSend
函數也是開源的,但其是經過彙編代碼實現的,arm64
架構代碼能夠在objc-msg-arm64.s
中找到。在Runtime
中,不少執行頻率比較高的函數,都是用匯編寫的。
objc_msgSend
並非徹底開源的,在_class_lookupMethodAndLoadCache3
函數中已經獲取到Class
參數了。因此在下面中有一個確定是對象中獲取isa_t
的過程,從方法命名和註釋來看,應該是GetIsaFast
彙編命令。若是這樣的話,就能夠從消息發送到調用流程銜接起來了。
ENTRY _objc_msgSend MESSENGER_START NilTest NORMAL GetIsaFast NORMAL // r11 = self->isa CacheLookup NORMAL // calls IMP on success NilTestSupport NORMAL GetIsaSupport NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r11 MethodTableLookup %a1, %a2 // r11 = IMP cmp %r11, %r11 // set eq (nonstret) for forwarding jmp *%r11 // goto *imp END_ENTRY _objc_msgSend
MESSENGER_START
:消息開始執行。NilTest
:判斷接收消息的對象是否爲nil
,若是爲nil
則直接返回,這就是對nil
發送消息無效的緣由。GetIsaFast
:快速獲取到isa
指向的對象,是一個類對象或元類對象。CacheLookup
:從ache list
中獲取緩存selector
,若是查到則調用其對應的IMP
。LCacheMiss
:緩存沒有命中,則執行此條彙編下面的方法。MethodTableLookup
:若是緩存中沒有找到,則從method list
中查找。若是每次進行方法調用時,都按照對象模型來進行方法列表的查找,這樣是很消耗時間的。Runtime
爲了優化調用時間,在objc_class
中添加了一個cache_t
類型的cache
字段,經過緩存來優化調用時間。
在執行objc_msgSend
函數的消息發送過程當中,同一個方法第一次調用是沒有緩存的,但調用以後就會存在緩存,以後的調用就直接調用緩存。因此方法的調用,能夠分爲有緩存和無緩存兩種,這兩種狀況下的調用堆棧是不一樣的。
首先是從緩存中查找IMP
,可是因爲cache3
調用lookUpImpOrForward
函數時,已經查找過cache
了,因此傳入的是NO
,不進入查找cahce
的代碼塊中。
struct cache_t { // 存儲被緩存方法的哈希表 struct bucket_t *_buckets; // 佔用的總大小 mask_t _mask; // 已使用大小 mask_t _occupied; } struct bucket_t { cache_key_t _key; IMP _imp; };
當給一個對象發送消息時,Runtime
會沿着isa
找到對應的類對象,但並不會馬上查找method_list
,而是先查找cache_list
,若是有緩存的話優先查找緩存,沒有再查找方法列表。
這是Runtime
對查找method
的優化,理論上來講在cache
中的method
被訪問的頻率會更高。cache_list
由cache_t
定義,內部有一個bucket_t
的數組,數組中保存IMP
和key
,經過key
找到對應的IMP
並調用。具體源碼能夠查看objc-cache.mm
。
若是類對象沒有被初始化,而且lookUpImpOrForward
函數的initialize
參數爲YES
,則表示須要對該類進行建立。函數內部主要是一些基礎的初始化操做,並且會遞歸檢查父類,若是父類未初始化,則先初始化其父類對象。
STATIC_ENTRY _cache_getImp mov r9, r0 CacheLookup NORMAL // cache hit, IMP in r12 mov r0, r12 bx lr // return imp CacheLookup2 GETIMP // cache miss, return nil mov r0, #0 bx lr END_ENTRY _cache_getImp
下面會進入cache_getImp
的代碼中,然而這個函數不是開源的,可是有一部分源碼能夠看到,是經過彙編寫的。其內部調用了CacheLookup
和CacheLookup2
兩個函數,這兩個函數也都是彙編寫的。
通過第一次調用後,就會存在緩存。進入objc_msgSend
後會調用CacheLookup
命令,若是找到緩存則直接調用。可是Runtime
並非徹底開源的,內部不少實現咱們依然看不到,CacheLookup
命令內部也同樣,只能看到調用完命令後就開始執行咱們的方法了。
CacheLookup NORMAL, CALL
在上面objc_msgSend
彙編實現中,存在一個MethodTableLookup
的彙編調用。在這條彙編調用中,調用了查找方法列表的C函數。下面是精簡版代碼。
.macro MethodTableLookup // 調用MethodTableLookup並在內部執行cache3函數(C函數) blx __class_lookupMethodAndLoadCache3 mov r12, r0 // r12 = IMP .endmacro
在MethodTableLookup
中經過調用_class_lookupMethodAndLoadCache3
函數,來查找方法列表。函數內部是經過lookUpImpOrForward
函數實現的,在調用時cache
字段傳入NO
,表示不須要查找緩存了,由於在cache3
函數上面已經經過彙編查找過了。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { // 經過cache3內部調用lookUpImpOrForward函數 return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
lookUpImpOrForward
函數是支持多線程的,因此內部會有不少鎖操做。其內部有一個rwlock_t
類型的runtimeLock
變量,有runtimeLock
控制讀寫鎖。其內部有不少邏輯代碼,這裏把函數內部實現作了精簡,把核心代碼貼到下面。
經過類對象的isRealized
函數,判斷當前類是否被實現,若是沒有被實現,則經過realizeClass
函數實現該類。在realizeClass
函數中,會設置version
、rw
、superClass
等一些信息。
// 執行查找imp和轉發的代碼 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = nil; bool triedResolver = NO; runtimeLock.assertUnlocked(); // 若是cache是YES,則從緩存中查找IMP。若是是從cache3函數進來,則不會執行cache_getImp()函數 if (cache) { // 經過cache_getImp函數查找IMP,查找到則返回IMP並結束調用 imp = cache_getImp(cls, sel); if (imp) return imp; } runtimeLock.read(); // 判斷類是否已經被建立,若是沒有被建立,則將類實例化 if (!cls->isRealized()) { // 對類進行實例化操做 realizeClass(cls); } // 第一次調用當前類的話,執行initialize的代碼 if (initialize && !cls->isInitialized()) { // 對類進行初始化,並開闢內存空間 _class_initialize (_class_getNonMetaClass(cls, inst)); } retry: runtimeLock.assertReading(); // 嘗試獲取這個類的緩存 imp = cache_getImp(cls, sel); if (imp) goto done; { // 若是沒有從cache中查找到,則從方法列表中獲取Method Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { // 若是獲取到對應的Method,則加入緩存並從Method獲取IMP log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } { unsigned attempts = unreasonableClassCount(); // 循環獲取這個類的緩存IMP 或 方法列表的IMP for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // 獲取父類緩存的IMP 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; } } // 在父類的方法列表中,獲取method_t對象。若是找到則緩存查找到的IMP Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } } } // 若是沒有找到,則嘗試動態方法解析 if (resolver && !triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); triedResolver = YES; goto retry; } // 若是沒有IMP被發現,而且動態方法解析也沒有處理,則進入消息轉發階段 imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlockRead(); return imp; }
在方法第一次調用時,能夠經過cache_getImp
函數查找到緩存的IMP
。但若是是第一次調用,就查不到緩存的IMP
,就會進入到getMethodNoSuper_nolock
函數中執行。下面是getMethod
函數的關鍵代碼。
getMethodNoSuper_nolock(Class cls, SEL sel) { // 根據for循環,從methodList列表中,從頭開始遍歷,每次遍歷後向後移動一位地址。 for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { // 對sel參數和method_t作匹配,若是匹配上則返回。 method_t *m = search_method_list(*mlists, sel); if (m) return m; } return nil; }
當調用一個對象的方法時,查找對象的方法,本質上就是遍歷對象isa
所指向類的方法列表,並用調用方法的SEL
和遍歷的method_t
結構體的name
字段作對比,若是相等則將IMP
函數指針返回。
// 根據傳入的SEL,查找對應的method_t結構體 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) { // SEL本質上就是字符串,查找的過程就是進行字符串對比 if (meth.name == sel) return &meth; } } if (mlist->isFixedUp()) { for (auto& meth : *mlist) { if (meth.name == sel) { _objc_fatal("linear search worked when binary search did not"); } } } return nil; }
在getMethod
函數中,主要是對Class
的methods
方法列表進行查找和匹配。類的方法列表都在Class
的class_data_bits_t
中,經過data()
函數從bits
中獲取到class_rw_t
的結構體,而後獲取到方法列表methods
,並遍歷方法列表。
若是從當前類中獲取不到對應的IMP
,則進入循環中。循環是從當前類出發,沿着繼承者鏈的關係,一直向根類查找,直到找到對應的IMP
實現。
查找步驟和上面也同樣,先經過cache_getImp
函數查找父類的緩存,若是找到則調用對應的實現。若是沒找到緩存,表示第一次調用父類的方法,則調用getMethodNoSuper_nolock
函數從方法列表中獲取實現。
for (Class curClass = cls->superclass; curClass != nil; 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; } } Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } }
若是沒有找到方法實現,則會進入動態方法決議的步驟。在if
語句中會判斷傳入的resolver
參數是否爲YES
,而且會判斷是否已經有過動態決議,由於下面是goto retry
,因此這段代碼可能會執行屢次。
if (resolver && !triedResolver) { _class_resolveMethod(cls, sel, inst); triedResolver = YES; goto retry; }
若是知足條件而且是第一次進行動態方法決議,則進入if
語句中調用_class_resolveMethod
函數。動態方法決議有兩種,_class_resolveClassMethod
類方法決議和_class_resolveInstanceMethod
實例方法決議。
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend; bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
在這兩個動態方法決議的函數實現中,本質上都是經過objc_msgSend
函數,調用NSObject
中定義的resolveInstanceMethod:
和resolveClassMethod:
兩個方法。
能夠在這兩個方法中動態添加方法,添加方法實現後,會在下面執行goto retry
,而後再次進入方法查找的過程當中。從triedResolver
參數能夠看出,動態方法決議的機會只有一次,若是此次再沒有找到,則進入消息轉發流程。
imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst);
若是通過上面這些步驟,仍是沒有找到方法實現的話,則進入動態消息轉發中。在動態消息轉發中,還能夠對沒有實現的方法作一些彌補措施。
下面是經過objc_msgSend
函數發送一條消息後,所通過的調用堆棧,調用順序是從上到下的。
CacheLookup NORMAL, CALL __objc_msgSend_uncached MethodTableLookup NORMAL _class_lookupMethodAndLoadCache3 lookUpImpOrForward
在調用objc_msgSend
函數後,會有一系列複雜的判斷邏輯,總結以下。
SEL
是否須要忽略,例如Mac OS
中的垃圾處理機制啓動的話,則忽略retain
、release
等方法,並返回一個_objc_ignored_method
的IMP
,用來標記忽略。nil
,由於在OC中對nil
發消息是無效的,這是由於在調用時就經過判斷條件過濾掉了。cache_getImp
函數進行查找,若是找到緩存則直接返回IMP
。method list
,查找是否有對應的SEL
,若是有則獲取到Method
對象,並從Method
對象中獲取IMP
,並返回IMP
(這步查找結果是Method
對象)。SEL
,則去父類中查找。首先查找cache list
,若是緩存中沒有則查找method list
,並以此類推直到查找到NSObject
爲止。SEL
,則進入動態方法解析中。能夠在resolveInstanceMethod
和resolveClassMethod
兩個方法中動態添加實現。Crash
。整體能夠被分爲三部分:
objc_msgSend
函數後,內部的一些處理邏輯。IMP
的過程,會涉及到cache list
和method list
等。在cache list
中找不到方法的狀況下,會經過MethodTableLookup
宏定義從類的方法列表中,查找對應的方法。在MethodTableLookup
中本質上也是調用_class_lookupMethodAndLoadCache3
函數,只是在傳參時cache
字段傳NO
,表示不從cache list
中查找。
在cache3
函數中,是直接調用的lookUpImpOrForward
函數,這個函數內部實現很複雜,能夠看一下Runtime Analyze。在這個裏面直接搜lookUpImpOrForward
函數名便可,能夠詳細看一下內部實現邏輯。
簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github
上,下載Runtime PDF
合集。把全部Runtime
文章總計九篇,都寫在這個PDF
中,並且左側有目錄,方便閱讀。
下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!