本文較長,分爲如下幾個部分html
cache_t
什麼是runtimec++
蘋果官方說法git
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. (儘可能將決定放到運行的時候,而不是在編譯和連接過程)github
runtime是有個兩個版本的: legacy 、 modern 在Objective-C 1.0使用的是legacy,在2.0使用的是modern。這裏簡單介紹下區別:數組
如今通常來講runtime都是指modern緩存
要想學習Runtime,首先要了解它底層的一些經常使用數據結構,好比isa指針bash
在arm64架構以前,isa就是一個普通的指針,存儲着Class、Meta-Class對象的內存地址markdown
從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。數據結構
查看runtime源碼能夠看到關於isa結構。官方的源碼是不能編譯的。我本身編譯了一份能夠運行的源碼在github上。架構
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; struct { ISA_BITFIELD; // defined in isa.h }; }; 複製代碼
在runtime723版本之前,直接把結構體放在isa裏面了。750版本以後,抽成宏了,展開宏ISA_BITFIELD
在__arm64__
架構下 以下所示
下面的代碼對isa_t中的結構體進行了位域聲明,地址從nonpointer
起到extra_rc
結束,從低到高進行排列。位域也是對結構體內存佈局進行了一個聲明,經過下面的結構體成員變量能夠直接操做某個地址。位域總共佔8字節,全部的位域加在一塊兒正好是64位。
小提示:union
中bits
能夠操做整個內存區,而位域只能操做對應的位。
eg: 一個對象的地址是0x7faf1b580450
轉換成二進制11111111010111100011011010110000000010001010000
,而後根據不一樣位置,去匹配不一樣的含義
define ISA_BITFIELD \ uintptr_t nonpointer : 1; //指針是否優化過 \ uintptr_t has_assoc : 1; //是否有設置過關聯對象,若是沒有,釋放時會更快 \ uintptr_t has_cxx_dtor : 1; //是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快 \ uintptr_t shiftcls : 33; //存儲着Class、Meta-Class對象的內存地址信息 \ uintptr_t magic : 6; //用於在調試時分辨對象是否未完成初始化 \ uintptr_t weakly_referenced : 1; //是否有被弱引用指向過,若是沒有,釋放時會更快 \ uintptr_t deallocating : 1; //對象是否正在釋放 \ uintptr_t has_sidetable_rc : 1; //引用計數器是否過大沒法存儲在isa中 \ uintptr_t extra_rc : 19 //裏面存儲的值是引用計數器減1 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) 複製代碼
nonpointer
has_assoc
has_cxx_dtor
shiftcls
magic
weakly_referenced
deallocating
extra_rc
has_sidetable_rc
eg: 查看objc_runtime-new.mm
文件中有以下代碼。
void *objc_destructInstance(id obj) { if (obj) { //是否有C++的析構函數 bool cxx = obj->hasCxxDtor(); //是否有設置過關聯對象 bool assoc = obj->hasAssociatedObjects(); //有C++的析構函數,就去銷燬 if (cxx) object_cxxDestruct(obj); //有設置過關聯對象,就去移除管理對象 if (assoc) _object_remove_assocations(obj); obj->clearDeallocating(); } return obj; } 複製代碼
能夠看出,釋放時候,會先判斷是否有設置過關聯對象,若是沒有,釋放時會更快。 是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快。其餘的弱引用,nonpointer等,讀者可自行看源碼。
關Tagged Pointer技術,深刻研究的話,能夠參考唐巧博客深刻理解Tagged Pointer
用一幅圖來表示
查看源碼(只保留了主要代碼)
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; //方法緩存
class_data_bits_t bits; // 用於獲取具體的類的信息
}
複製代碼
也就是說結構體objc_class
裏面
struct objc_class : objc_object {
Class isa;
Class superclass;
cache_t cache; //方法緩存
class_data_bits_t bits; // 用於獲取具體的類的信息}
複製代碼
class_rw_t
根據bits能夠獲得class_rw_t
,class_rw_t
裏面的methods、properties、protocols是二維數組,是可讀可寫的,包含了類的初始內容、分類的內容
eg:方法列表methods中存放着不少一維數組method_list_t
,而每個method_list_t
中存放這method_t
,method_t
中是對應方法的imp指針,名字。類型等方法信息,在詳解iOS中分類Cateogry一文中,咱們知道,每一個分類編譯完成以後都會生成一個_category_t
,對應着method_list_t
。
#define FAST_DATA_MASK 0x00007ffffffffff8UL class_rw_t *data() { return bits.data(); } class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } 複製代碼
由代碼可知 bits & FAST_DATA_MASK
可得到class_rw_t
.
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods; //方法列表
property_array_t properties; //屬性列表
protocol_array_t protocols; // 協議列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
複製代碼
結構體method_array_t
class method_array_t : public list_array_tt<method_t, method_list_t> { typedef list_array_tt<method_t, method_list_t> Super; public: method_list_t **beginCategoryMethodLists() { return beginLists(); } method_list_t **endCategoryMethodLists(Class cls); method_array_t duplicate() { return Super::duplicate<method_array_t>(); } }; 複製代碼
method_t
method_t
是對方法、函數的封裝
// IMP表明函數的具體實現
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
複製代碼
SEL
char *
相似@selector()
和sel_registerName()
得到sel_getName()
和NSStringFromSelector()
轉成字符串typedef struct objc_selector *SEL;
複製代碼
types包含了函數返回值、參數編碼的字符串
v16@0:8
表明,返回值void類型,第一個參數是id類型,第二個參數是SEL類型。後面會詳細說明。struct method_t { SEL name; //函數名 const char *types; //編碼(返回值類型,參數類型) MethodListIMP imp; //指向函數的指針(函數地址) struct SortBySELAddress : public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; } }; }; 複製代碼
@interface TestObject : NSObject - (void)testMethod; @end @interface TestObject2 : NSObject - (void)testMethod; @end // TestObject實現文件 @implementation TestObject - (void)testMethod { NSLog(@"TestObject testMethod %p", @selector(testMethod)); } @end // TestObject2實現文件也同樣 @implementation TestObject - (void)testMethod { NSLog(@"TestObject testMethod %p", @selector(testMethod)); } // 結果: TestObject testMethod 0x100000f81 TestObject2 testMethod 0x100000f81 複製代碼
class_ro_t
class_ro_t
裏面的baseMethodList、baseProtocols、ivars、baseProperties是一維數組,是隻讀的,包含了類的初始內容
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize;//instance對象佔用的內存空間 #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; //類名 method_list_t * baseMethodList; //方法列表 protocol_list_t * baseProtocols; //協議列表 const ivar_list_t * ivars; //成員變量列表 const uint8_t * weakIvarLayout; property_list_t *baseProperties; method_list_t *baseMethods() const { return baseMethodList; } }; 複製代碼
前面說了,v16@0:8
表明,返回值void類型,第一個參數是id類型,第二個參數是SEL類型。這裏詳細說明
iOS中提供了一個叫作@encode的指令,能夠將具體的類型表示成字符串編碼,連接爲 Type Encodings
eg: 咱們有以下函數
void objc_msgSend(id receiver, SEL selector) { if (receiver == nil) return; // 查找緩存 ... } 複製代碼
就能夠用v16@0:8
表示
v@:
,這在後面講到消息轉發的時候會用到。再如:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. Method method = class_getClassMethod([self class], @selector(test)); const char *str = method_getTypeEncoding(method); Method method2 = class_getClassMethod([self class], @selector(testWithNum:)); const char *str2 = method_getTypeEncoding(method2); NSLog(@"test的類型 = %s ",str); NSLog(@"testWithNum: = %s ",str2); } // v16@0:8 +(void)test{ } // i20@0:8i16 +(int)testWithNum:(int)num{ return num; } 複製代碼
輸出結果爲:
RuntimeDemo[28247:303205] test的類型 = v16@0:8 RuntimeDemo[28247:303205] testWithNum: = i20@0:8i16
對於方法testWithNum
來講
@encode
cache_t
前面講了Class內部結構,其中有個方法緩存cache_t
,用散列表(哈希表)來緩存曾經調用過的方法,能夠提升方法的查找速度
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表的長度 -1
mask_t _occupied; //已經緩存的方法數量
}
複製代碼
散列表數組_buckets
中存放着bucket_t
,bucket_t
的結構以下
struct bucket_t {
MethodCacheIMP _imp; //函數的內存地址
cache_key_t _key; //SEL做爲Key
}
複製代碼
cache_t
查找原理在cache_t
中如何查找方法,其實對於其餘散列表也是通用的。
在文件objc-cache.mm
中找到bucket_t * cache_t::find(cache_key_t k, id receiver)
// 散列表中查找方法緩存 bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t begin = cache_hash(k, m); mask_t i = begin; do { if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) != begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); } 複製代碼
其中,根據key
和散列表長度減1 mask
計算出下標 key & mask
,取出的值若是key和當初傳進來的Key相同,就說明找到了。不然,就不是本身要找的方法,就有了hash衝突,把i的值加1,繼續計算。以下代碼
// 計算下標 static inline mask_t cache_hash(cache_key_t key, mask_t mask) { return (mask_t)(key & mask); } //hash衝突的時候 static inline mask_t cache_next(mask_t i, mask_t mask) { return (i+1) & mask; } 複製代碼
cache_t
的擴容當方法緩存太多的時候,超過了容量的3/4s時候,就須要擴容了。擴容是,把原來的容量增長爲2倍
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { ... 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 { // 來到這裏說明,超過了3/4,須要擴容 cache->expand(); } ... } 複製代碼
具體擴容代碼爲
enum { INIT_CACHE_SIZE_LOG2 = 2, INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2) }; // cache_t的擴容 void cache_t::expand() { cacheUpdateLock.assertLocked(); uint32_t oldCapacity = capacity(); // 擴容爲原來的2倍 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); } 複製代碼
咱們常常寫的OC方法調用,到底是怎麼個調用流程,怎麼找到方法的呢?
咱們以下代碼
Person *per = [[Person alloc]init]; [per test]; 複製代碼
執行指令
clang -rewrite-objc main.m -o main.cpp
生成cpp文件,對應上面的代碼爲
((void (*)(id, SEL))(void *)objc_msgSend)((id)per, sel_registerName("test")); 複製代碼
簡化爲
objc_msgSend)(per, sel_registerName("test")); 複製代碼
其中,per稱爲消息接收者(receiver), test稱爲消息名稱,也就是說,OC中方法的調用其實都是轉換爲objc_msgSend函數的調用
OC中的方法調用,其實都是轉換爲objc_msgSend
函數的調用
objc_msgSend的執行流程能夠分爲3大階段
消息發送
動態方法解析
消息轉發
運行時期,調用方法流程爲
實例對象中存放 isa 指針以及實例變量,有 isa 指針能夠找到實例對象所屬的類對象 (類也是對象,面向對象中一切都是對象),類中存放着實例方法列表,在這個方法列表中 SEL 做爲 key,IMP 做爲 value。 在編譯時期,根據方法名字會生成一個惟一標識,這個標識就是 SEL。IMP 其實就是函數指針 指向了最終的函數實現。整個 Runtime 的核心就是 objc_msgSend 函數,經過給類發送 SEL 以傳遞消息,找到匹配的 IMP 再獲取最終的實現
類中的 super_class
指針能夠追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,並自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法,找到後就運行整個方法。
用一張經典的圖來表示就是
類中的 super_class 指針能夠追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,並自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法,找到後就運行整個方法。
metaClass是元類,也有 isa 指針、super_class 指針。其中保存了類方法列表。
objc-msg-arm64.s
裏面都是彙編
objc-msg-arm64.s
ENTRY _objc_msgSend
b.le LNilOrTagged
CacheLookup NORMAL
.macro CacheLookup
.macro CheckMiss
STATIC_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
__class_lookupMethodAndLoadCache3
複製代碼
objc-runtime-new.mm
objc-runtime-new.mm
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
_class_resolveInstanceMethod
_objc_msgForward_impcache
複製代碼
一直跟到 __forwarding__
的時候,已經不開源的了。
objc-msg-arm64.s
STATIC_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
Core Foundation
__forwarding__(不開源
複製代碼
_objc_msgSend
先來看 objc-msg-arm64.s
主要代碼爲
//1.進入objcmsgSend ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame // x0 recevier // 消息接收者 消息名稱 cmp p0, #0 // nil check and tagged pointer check #if SUPPORT_TAGGED_POINTERS //2.isa 優化 b.le LNilOrTagged // (MSB tagged pointer looks negative) #else b.eq LReturnZero #endif ldr p13, [x0] // p13 = isa GetClassFromIsa_p16 p13 // p16 = class LGetIsaDone: // 3.isa優化完成 CacheLookup NORMAL //4.執行 CacheLookup NORMAL // calls imp or objc_msgSend_uncached #if SUPPORT_TAGGED_POINTERS LNilOrTagged: b.eq LReturnZero // nil check ...省略不少代碼 .macro MethodTableLookup // push frame SignLR stp fp, lr, [sp, #-16]! mov fp, sp ...省略不少代碼 // receiver and selector already in x0 and x1 mov x2, x16 bl __class_lookupMethodAndLoadCache3 //方法爲_class_lookupMethodAndLoadCache3調用的彙編語言 ...省略不少代碼 .endmacro STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band p16 is the class to search MethodTableLookup //查找IMP TailCallFunctionPointer x17 END_ENTRY __objc_msgSend_uncached STATIC_ENTRY __objc_msgLookup_uncached UNWIND __objc_msgLookup_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band p16 is the class to search MethodTableLookup ret END_ENTRY __objc_msgLookup_uncached STATIC_ENTRY _cache_getImp GetClassFromIsa_p16 p0 CacheLookup GETIMP ...省略不少代碼 複製代碼
從上面的代碼能夠看出方法查找 IMP 的工做交給了 OC 中的 _class_lookupMethodAndLoadCache3 函數,並將 IMP 返回(從 r11 挪到 rax)。最後在 objc_msgSend 中調用 IMP。
彙編代碼比較晦澀難懂,所以這裏將函數的實現反彙編成C語言的僞代碼:
/* objc_msgSend的C語言版本僞代碼實現. receiver: 是調用方法的對象 op: 是要調用的方法名稱字符串 */ id objc_msgSend(id receiver, SEL op, ...) { //1............................ 對象空值判斷。 //若是傳入的對象是nil則直接返回nil if (receiver == nil) return nil; //2............................ 獲取或者構造對象的isa數據。 void *isa = NULL; //若是對象的地址最高位爲0則代表是普通的OC對象,不然就是Tagged Pointer類型的對象 if ((receiver & 0x8000000000000000) == 0) { struct objc_object *ocobj = (struct objc_object*) receiver; isa = ocobj->isa; } else { //Tagged Pointer類型的對象中沒有直接保存isa數據,因此須要特殊處理來查找對應的isa數據。 //若是對象地址的最高4位爲0xF, 那麼表示是一個用戶自定義擴展的Tagged Pointer類型對象 if (((NSUInteger) receiver) >= 0xf000000000000000) { //自定義擴展的Tagged Pointer類型對象中的52-59位保存的是一個全局擴展Tagged Pointer類數組的索引值。 int classidx = (receiver & 0xFF0000000000000) >> 52 isa = objc_debug_taggedpointer_ext_classes[classidx]; } else { //系統自帶的Tagged Pointer類型對象中的60-63位保存的是一個全局Tagged Pointer類數組的索引值。 int classidx = ((NSUInteger) receiver) >> 60; isa = objc_debug_taggedpointer_classes[classidx]; } } //由於內存地址對齊的緣由和虛擬內存空間的約束緣由, //以及isa定義的緣由須要將isa與上0xffffffff8才能獲得對象所屬的Class對象。 struct objc_class *cls = (struct objc_class *)(isa & 0xffffffff8); //3............................ 遍歷緩存哈希桶並查找緩存中的方法實現。 IMP imp = NULL; //cmd與cache中的mask進行與計算獲得哈希桶中的索引,來查找方法是否已經放入緩存cache哈希桶中。 int index = cls->cache.mask & op; while (true) { //若是緩存哈希桶中命中了對應的方法實現,則保存到imp中並退出循環。 if (cls->cache.buckets[index].key == op) { imp = cls->cache.buckets[index].imp; break; } //方法實現並無被緩存,而且對應的桶的數據是空的就退出循環 if (cls->cache.buckets[index].key == NULL) { break; } //若是哈希桶中對應的項已經被佔用可是又不是要執行的方法,則經過開地址法來繼續尋找緩存該方法的桶。 if (index == 0) { index = cls->cache.mask; //從尾部尋找 } else { index--; //索引減1繼續尋找。 } } /*end while*/ //4............................ 執行方法實現或方法未命中緩存處理函數 if (imp != NULL) return imp(receiver, op, ...); //這裏的... 是指傳遞給objc_msgSend的OC方法中的參數。 else return objc_msgSend_uncached(receiver, op, cls, ...); } /* 方法未命中緩存處理函數:objc_msgSend_uncached的C語言版本僞代碼實現,這個函數也是用匯編語言編寫。 */ id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls) { //這個函數很簡單就是直接調用了_class_lookupMethodAndLoadCache3 來查找方法並緩存到struct objc_class中的cache中,最後再返回IMP類型。 IMP imp = _class_lookupMethodAndLoadCache3(receiver, op, cls); return imp(receiver, op, ....); } 複製代碼
前面跟到_class_lookupMethodAndLoadCache3
以後,後面就不是彙編了,是C語言的實現
class_rw_t
中查找方法(分爲二分查找和線性查找,兩種),若是找到了,就結束查找,緩存一份到本身緩存中,調用方法class_rw_t
中查找方法,若是找到了,就結束查找,緩存一份到本身緩存中,調用方法IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } 複製代碼
關鍵代碼在lookUpImpOrForward
裏面,下面的代碼,增長了註釋
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = nil; bool triedResolver = NO; //是否動態解析過的標記 runtimeLock.assertUnlocked(); if (cache) { imp = cache_getImp(cls, sel); if (imp) return imp; } runtimeLock.lock(); checkIsKnownClass(cls); if (!cls->isRealized()) { realizeClass(cls); } if (initialize && !cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock(); } retry: runtimeLock.assertLocked(); // 這裏先查緩存,雖然前面彙編裏面已經查過了。可是有可能動態添加,致使緩存有更新 imp = cache_getImp(cls, sel); //若是查到了,就直接跳轉到最後 if (imp) goto done; //來到這裏,說明緩存沒有 // Try this class's method lists. { // 查找方法列表 Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; //若是查到了,就直接跳轉到最後 goto done; } } // Try superclass caches and method lists. { //查找父類的緩存和方法列表 unsigned attempts = unreasonableClassCount(); // for循環層層向上找 for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { // Halt if there is a cycle in the superclass chain. if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. // 父類緩存 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; } } // Superclass method list. 查找父類方法列表 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.unlock(); _class_resolveMethod(cls, sel, inst); runtimeLock.lock(); // Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; //動態解析過,標記設爲YES // 回到查找緩存的地方開始查找,緩存中沒有加過,此次去查找,能夠再方法列表中查到 goto retry; } // No implementation found, and method resolver didn't help. // Use forwarding. // 來到這裏,說明進入第三階段,消息轉發階段 imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlock(); // 返回方法地址 return imp; } 複製代碼
前面的消息發送階段,沒有找到,就來到動態方法解析階段
頭文件中定義兩個方法
- (void)test; - (void)run; 複製代碼
只實現test
-(void)test{ NSLog(@"%s",__func__); } 複製代碼
調用的是時候
Person *per = [[Person alloc]init];
[per run];
複製代碼
由前面的消息發送階段知道,去查緩存,查方法列表,查父類等等,這些操做以後,都沒有找到這個方法的實現,若是後面不作處理,必然拋出異常
報錯方法找不到
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x100f436c0'
若是要處理的話,消息發送階段處理不了。那麼就來到第二階段,動態解析階段。這個階段的處理,從前面的源碼可知
// 動態方法解析 void _class_resolveMethod(Class cls, SEL sel, id inst) { if (! cls->isMetaClass()) { //若是不是元類對象 // try [cls resolveInstanceMethod:sel] _class_resolveInstanceMethod(cls, sel, inst); } else { // 是元類對象 // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); } } } 複製代碼
系統默認的resolveClassMethod
和resolveInstanceMethod
默認返回NO
+ (BOOL)resolveClassMethod:(SEL)sel { return NO; } + (BOOL)resolveInstanceMethod:(SEL)sel { return NO; } 複製代碼
咱們能夠在動態解析階段,重寫resolveInstanceMethod
並添加方法的實現
+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(run)) { // 獲取其餘方法 實例方法,或者類方法均可以 Method method = class_getInstanceMethod(self, @selector(test)); // 動態添加test方法的實現 class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); //等價於下面的 // class_addMethod(self, sel, method_getImplementation(method), "v@:"); // 返回YES表明有動態添加方法 其實這裏返回NO,也是能夠的,返回YES只是增長了一些打印 return NO; } return [super resolveInstanceMethod:sel]; } 複製代碼
上面的代碼中,由於-(void)test
無參無返回值,函數類型爲v@:
,因此,上面的method_getTypeEncoding(method)
能夠換成"v@:"
也是沒問題的。
這樣的話,就至關於,調用run的時候,實際上調用的是test。由源碼可知,動態解析完以後,回到查找緩存的地方開始查找,緩存中沒有加過,此次去查找,能夠再方法列表中查到。這樣就能夠正確執行了。輸出結果爲
objc-test[6681:75992] -[Person test]
直接運行源碼,以下圖
若是前面消息發送和動態解析階段,對方法都沒有處理,咱們還有最後一個階段,消息轉發階段來處理。從源碼的imp = (IMP)_objc_msgForward_impcache;
能夠看出,_objc_msgForward_impcache
的代碼是在彙編裏面
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
// 這裏進去以後,不開源了
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
... 還有不少代碼
複製代碼
跟到 ___forwarding___
以後就不開源了
objc-test[15568:163497] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x100f039f0' *** First throw call stack: ( 0 CoreFoundation 0x00007fff307de063 __exceptionPreprocess + 250 1 libobjc.A.dylib 0x000000010038ec9f objc_exception_throw + 47 2 CoreFoundation 0x00007fff308671bd -[NSObject(NSObject) __retain_OA] + 0 3 CoreFoundation 0x00007fff307844b4 ___forwarding___ + 1427 4 CoreFoundation 0x00007fff30783e98 _CF_forwarding_prep_0 + 120 5 objc-test 0x0000000100000e11 main + 97 6 libdyld.dylib 0x00007fff672e93f9 start + 1 7 ??? 0x0000000000000001 0x0 + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException 複製代碼
在上述調用棧中,發現了在 Core Foundation 中會調用___forwarding___
。根據資料也能夠了解到,在 objc_setForwardHandler
時會傳入 __CF_forwarding_prep_0
和 ___forwarding_prep_1___
兩個參數,而這兩個指針都會調用____forwarding___
。這個函數中,也交代了消息轉發的邏輯
接下來怎麼辦呢?能夠經過彙編調試,或逆向來進一步分析後續的實現。
站在前人的代碼上,能看的更遠 ---魯迅.尼古拉斯
___forwarding___
的實現國外有大神復原了___forwarding___
的實現,具體可參考Hmmm, What's that Selector?
須要注意的是,復原了___forwarding___
的實現是僞代碼。具體代碼我已經放在了github上。
僞代碼 // 兩個參數:前者爲被轉發消息的棧指針 IMP ,後者爲是否返回結構體 int __forwarding__(void *frameStackPointer, int isStret) { id receiver = *(id *)frameStackPointer; SEL sel = *(SEL *)(frameStackPointer + 8); const char *selName = sel_getName(sel); Class receiverClass = object_getClass(receiver); // 調用 forwardingTargetForSelector: // 進入 備援接收 主要步驟 if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { // 得到方法簽名 id forwardingTarget = [receiver forwardingTargetForSelector:sel]; // 判斷返回類型是否正確 if (forwardingTarget && forwardingTarget != receiver) { if (isStret == 1) { int ret; objc_msgSend_stret(&ret,forwardingTarget, sel, ...); return ret; } return objc_msgSend(forwardingTarget, sel, ...); } } // 殭屍對象 const char *className = class_getName(receiverClass); const char *zombiePrefix = "_NSZombie_"; size_t prefixLen = strlen(zombiePrefix); // 0xa if (strncmp(className, zombiePrefix, prefixLen) == 0) { CFLog(kCFLogLevelError, @"*** -[%s %s]: message sent to deallocated instance %p", className + prefixLen, selName, receiver); <breakpoint-interrupt> } // 調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation // 進入消息轉發系統 if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; // 判斷返回類型是否正確 if (methodSignature) { BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct; if (signatureIsStret != isStret) { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", selName, signatureIsStret ? "" : not, isStret ? "" : not); } if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { // 傳入消息的所有細節信息 NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; [receiver forwardInvocation:invocation]; void *returnValue = NULL; [invocation getReturnValue:&value]; return returnValue; } else { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message", receiver, className); return 0; } } } SEL *registeredSel = sel_getUid(selName); // selector 是否已經在 Runtime 註冊過 if (sel != registeredSel) { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", sel, selName, registeredSel); } // doesNotRecognizeSelector,主動拋出異常 // 代表未能獲得處理 else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { [receiver doesNotRecognizeSelector:sel]; } else { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort", receiver, className); } // The point of no return. kill(getpid(), 9); } 複製代碼
forwardingTargetForSelector
的返回值,若是有值,就向這個返回值發送消息。也就是objc_msgSend(返回值, SEL)
。methodSignatureForSelector
方法,若是有值,就調用forwardInvocation
,其中的參數是一個 NSInvocation 對象,並將消息所有屬性記錄下來。 NSInvocation 對象包括了選擇子、target 以及其餘參數。其中的實現僅僅是改變了 target 指向,使消息保證可以調用。假若發現本類沒法處理,則繼續想父類進行查找。直至 NSObject 。methodSignatureForSelector
方法返回nil,就調用doesNotRecognizeSelector:
方法上面都是源碼分析,那下面代碼驗證
在源碼中forwardingTargetForSelector
系統默認返回nil 。
+ (id)forwardingTargetForSelector:(SEL)sel { return nil; } - (id)forwardingTargetForSelector:(SEL)sel { return nil; } 複製代碼
咱們有類Person
只定義了方法- (void)run;
可是沒有實現,另外有類Car
,實現了方法- (void)run;
@interface Car : NSObject - (void)run; @end #import "Car.h" @implementation Car - (void)run{ NSLog(@"%s",__func__); } @end 複製代碼
在person中,重寫forwardingTargetForSelector
讓返回Car
對象
// 消息轉發 - (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(run)) { return [[Car alloc] init]; } return [super forwardingTargetForSelector:aSelector]; } 複製代碼
調用的時候
Person *per = [[Person alloc]init];
[per run];
複製代碼
輸出objc-test[16694:174917] -[Car run]
驗證了前面說的,forwardingTargetForSelector
返回值不爲空的話,就向這個返回值發送消息,也就是 objc_msgSend(返回值, SEL)
若是前面的forwardingTargetForSelector
返回爲空, 就會調用 methodSignatureForSelector
獲取方法簽名後再調用 forwardInvocation
// 方法簽名:返回值類型、參數類型 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(run)) { return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; } return [super methodSignatureForSelector:aSelector]; } // NSInvocation封裝了一個方法調用,包括:方法調用者、方法名、方法參數 // anInvocation.target 方法調用者 // anInvocation.selector 方法名 // [anInvocation getArgument:NULL atIndex:0] - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation invokeWithTarget:[[Car alloc] init]]; } 複製代碼
依然能夠調用到-[Car run]
注意點1
消息轉發的forwardingTargetForSelector
和methodSignatureForSelector
以及forwardInvocation
不只支持實例方法,還支持類方法。不過系統沒有提示,須要寫成實例方法,而後把前面的-
改爲+
便可。
注意點2 只能向運行時動態建立的類添加ivars,不能向已經存在的類添加ivars
這是由於在編譯時只讀結構體class_ro_t就會被肯定,在運行時是不可更改的。ro結構體中有一個字段是instanceSize,表示當前類在建立對象時須要多少空間,後面的建立都根據這個size分配類的內存。 若是對一個已經存在的類增長一個參數,改變了ivars的結構,這樣在訪問改變以前建立的對象時,就會出現問題。
更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。