引入
眾所周知,Objective-C動態性的根源在方法的調用是通過message來實現的,一次發生message的過程就是一次方法的調用過程。發送message只需要指定對象和SEL,Runtime的objc_msgSend會根據在信息在對象isa指針指向的Class中尋找該SEL對應的IMP,從而完成方法的調用。這樣每次方法的調用必然會有方法的查找過程,如果頻繁調用,或者Class的方法列表過大,很容易導致性能瓶頸,但OC似乎并沒有這個問題,這得益于蘋果的優化機制,其中包括純匯編的objc_msgSend實現(不用匯編參數暫存困難,當然也不是沒有辦法,總有一些歪招可以解決,但為了兼顧效率自然匯編更加合適),方法查找cache,TaggedPointer等等技術才帶來OC極高的效率。接下來我們從objc_msgSend為引,來解讀整個過程。
Objective-C動態化的核心objc_msgSend
先說幾句閑話,如果大學期間學習過匯編課程的同學就知道,相同的邏輯,c語言寫出的函數匯編成.s文件和直接匯編的的文件,體積差異是很大的,幾倍到幾十倍的差距,由此可見兩者的效率差距也十分巨大,這也是蘋果為什么非要用匯編去干這個事情。而且蘋果匯編只實現了cache方法的查找過程,并未匯編實現class所有的方法查找,因為前者調用非常頻繁,后者卻不是,其在運行效率和開發效率(可靠性)作了一個平衡。
進入正題,我們知道對于任意的OC方法的調用,比如[obj aMethod];
都會被翻譯成objc_msgSend(obj, sel/*@selector(aMethod)*/);
,由此進入objc_msgSend執行,而該方法實現是匯編完成的,這對解讀造成了一定的困擾,所以我們不得不迎難而上,搞定整個方法的邏輯過程。
我這里下載的objc_706的源碼,這里我只解讀objc-msg-arm64.s文件,其他處理器架構的除了實現細節出入,其查找邏輯的類似。
文件的最開始聲明了12個私有的_objc_entryPoints,其中包括我們關注的.quad _objc_msgSend
在文件中搜索"_objc_msgSend",會找到以下匯編代碼,這就是其實現的一部分,接下來我們將一步步解讀它。
.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
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
數據結構定義
咋一看,只能了解其定義了一些數據存儲的空間,里面存儲的應該是指針,而且這些指針3bit對齊,似乎跟OC的objc_object指針很類似,然后通過.fill偽指令,將所有的數據單元填0。至于這些數據怎么使用我們并不了解,沒關系,我們繼續往下看。
找到了ENTRY _objc_msgSend
,顧名思義這就是真正的函數入口了。我們搜索ENTRY,其定義如下:
.macro ENTRY /* name */
.text
.align 5
.globl $0
$0:
.endmacro
這里定義了一個匯編宏,表示text段,定義一個global的_objc_msgSend,"$0"同時生產一個函數入口標簽。
UNWIND _objc_msgSend, NoFrame
則定義了一些段存儲數據對象,簡單來說就是類似于結構體數據對象,具體意義我也不是很了解。
MESSENGER_START
定義了一些方法調用開始的調試數據,具體對應到objc-gdb.h中的
#define OBJC_MESSENGER_START 1
#define OBJC_MESSENGER_END_FAST 2
#define OBJC_MESSENGER_END_SLOW 3
#define OBJC_MESSENGER_END_NIL 4
struct objc_messenger_breakpoint {
uintptr_t address;
uintptr_t kind;
};
函數邏輯主體
邏輯部分有很多子邏輯,我們一步一步解讀
tagged pointer處理
cmp x0, #0
,從注釋可以了解是在和"0"比較,比較的結果會有三種,大于小于等于。
這里邏輯是
b.le LNilOrTagged
,即如果小于等于就跳轉到標簽:LNilOrTagged。(因為nil==0,tagged指針最高位是1(符號位),所以肯定小于0)。跳轉到LNilOrTagged后,執行
b.eq LReturnZero
繼續檢查比較結果,相等則跳轉標簽:LReturnZero在LReturnZero中,將x1置為0,浮點寄存器d1,d2,d3,d4全部置為0。這是因為objc_msgSend并不知道,該函數調用期望返回的是什么數據類型,可能是浮點,整型,指針,甚至可能結構體,所以其將常用的返回值的寄存器,全部清0。但對于復雜的結構體,objc_msgSend就無能為力了(因為其不知道這些數據的大小),它只能將返回結果放入x8寄存器,由另外代碼去清0,而這部分代碼則編譯器在編譯的時候根據相關數據類型生成。
-
如果
b.eq LReturnZero
不成立,則表明該數據是個tagged pointer,需要進一步處理才能做調用。mov x10, #0xf000000000000000; cmp x0, x10
,這兩句就很明顯了。比較其最高的4bit,這應該是個標記位。我們去搜索其相關的定義#define _OBJC_TAG_INDEX_MASK 0x7 // array slot includes the tag bit itself #define _OBJC_TAG_SLOT_COUNT 16 #define _OBJC_TAG_SLOT_MASK 0xf #define _OBJC_TAG_EXT_INDEX_MASK 0xff // array slot has no extra bits #define _OBJC_TAG_EXT_SLOT_COUNT 256 #define _OBJC_TAG_EXT_SLOT_MASK 0xff #if OBJC_MSB_TAGGED_POINTERS # define _OBJC_TAG_MASK (1ULL<<63) # define _OBJC_TAG_INDEX_SHIFT 60 # define _OBJC_TAG_SLOT_SHIFT 60 # define _OBJC_TAG_PAYLOAD_LSHIFT 4 # define _OBJC_TAG_PAYLOAD_RSHIFT 4 # define _OBJC_TAG_EXT_MASK (0xfULL<<60) # define _OBJC_TAG_EXT_INDEX_SHIFT 52 # define _OBJC_TAG_EXT_SLOT_SHIFT 52 # define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 12 # define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12 #else ...//其他 #endif
其中
#0xf000000000000000
就是_OBJC_TAG_EXT_MASK (0xfULL<<60)
。當然肯定不是一下就找到這里了,是通過isTaggedPointer(),找到_objc_isTaggedPointer()
,發現tagged指針相關的操作這里都有,其中_objc_makeTaggedPointer
的功能是實現原始數據封裝成tagged指針。
這里我們畫了一個簡圖,是arm64下的存儲結構(其他CPU下并不一樣,比如X86_64,data存在前部,tag存后部),如果是個擴展的tagged指針,其中0-51位是數據部分,52-59這8個bit是擴展tagged部分,60到62的3bit是tagged,63是tagged指針標記。如果是一個tagged指針0-59是數據,60-62是tagged index,63是標記位。所以tagged指針記錄了data+index。
常見的tagged指針有
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
其中tag是一個index,表示_objc_debug_taggedpointer_classes
偏移量,而ext_tag是_objc_debug_taggedpointer_ext_classes
index。
在objc-object.h文件中有以下聲明,所以這兩個數據是匯編語言定義,但C++也在聲明和使用。
#if SUPPORT_TAGGED_POINTERS
extern "C" {
extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT*2];
extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
#define objc_tag_classes objc_debug_taggedpointer_classes
#define objc_tag_ext_classes objc_debug_taggedpointer_ext_classes
#endif
?
接著看
. b.hs LExtTag
,如果比較結果是大于等于,則表示這是個擴展的tagged,跳轉到標簽:LExtTag
- 接來兩句是加載
_objc_debug_taggedpointer_ext_classes
. ubfx x11, x0, #52, #8
的意思是x11= (x0 & 0x0ff0000000000000)>>52,即取第52-59bit的數據。
. ldr x16, [x10, x11, LSL #3]
,x16=x10+(x11<<3),左移三位是因為_objc_debug_taggedpointer_ext_classes
是8個byte為單位來偏移的,后面做匯編逆向源碼的時候會有類似的例子來說明。
如果第五步不成立,則執行按照正常的tagged指針處理,加載
_objc_debug_taggedpointer_classes
,取出第60-63bit,左移三位,取出真正的isa。跳轉LGetIsaDone,其使用了匯編宏CacheLookup,參數NORMAL,其用于搜索緩存。
到這里,匯編的第一部分就解讀完成了,其主要就是解析當前指針得到對應的class以備后續處理。為了更好的理解,這里我貼出逆向出來的源碼,以便于理解。
id objc_msgSend_c(id obj, SEL sel,...) {
id localObj = obj;
int64_t obj_i = (int64_t)obj;
//這一部分處理tagged pointer的isa指針
if (obj_i == 0) return nil;
if (obj_i < 0) {
//tagged pointer
uintptr_t obj_ui = (uintptr_t)obj_i;
if (obj_ui >= _OBJC_TAG_EXT_MASK) {
uint16_t index = (obj_ui << _OBJC_TAG_PAYLOAD_LSHIFT) >> (_OBJC_TAG_EXT_INDEX_SHIFT + _OBJC_TAG_PAYLOAD_LSHIFT);
localObj = objc_tag_ext_classes[index];
} else {
uint16_t index = obj_ui >> _OBJC_TAG_INDEX_SHIFT;
localObj = objc_tag_classes[index];
}
}
...
}
核心代碼——緩存查找
這里先給出CacheLookup匯編源碼如下:
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
這段匯編注釋很詳細,很多給出了對應的c代碼,所以很容易了解其大概做了什么邏輯,但要看懂具體細節,并逆向出源碼,對于不常玩匯編的人來說,還是有那么一點困難。
-
之前操作,已經在x1和x16中存入處理好的相關數據,x0=obj,x1=SEL,x16=isa。立即數#CACHE=16,OC對象的內存布局中,前面分別是isa和superclass指針,x16+16就是cache的地址。cache結構體數據布局如下。
typedef uint32_t mask_t; struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; }
所以加載x16+16后,
x10=_buckets,x11=_mask和_occupied
小端機,
_mask
在x11的低位,即w11。x1存的是SEL的地址,將其低位w1取出(ARM64下,這里的指針的低32bit是真實地址,高32bit一般是1)。這里取出w1與w11做與運算,放入寄存器x12。-
add x12, x10, x12, LSL #4
,這句的意思是x12=x10+(x12<<4)。x10是_buckets的首地址。把這句逆向成c代碼如下:
Class cls = localObj->ISA();
cache_t cache = cls->cache;
uintptr_t sel_i = (uintptr_t)sel;
bucket_t *bucket = (bucket_t *)((uintptr_t)cache.buckets() + ((sel_i & cache.mask()) << 4));
我們再看看bucket_t的定義
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
IMP _imp;
}
所以x12<<4,放大了16倍,也就是一個bucket_t的大小。所以我們可以將上句代碼簡化如下:
bucket_t *bucket = &(cache.buckets()[sel_i & cache.mask()])
ldp x9, x17, [x12]
,加載bucket的數據到x9和x17。cmp x9, x1
,比較x9與x1,也就是bucket->_key與SEL。b.ne 2f
如果不相等跳轉到標簽2-
接上面一步,如果相等則
CacheHit $0
,表示命中緩存,CacheHit是一個宏,“$0”是第一個參數,就是之前的NORMAL。這是將執行br x17
,跳轉到寄存器x17的地址,也即是bucket->_imp。貼一下這部分代碼吧。#define NORMAL 0 #define GETIMP 1 #define LOOKUP 2 .macro CacheHit .if $0 == NORMAL MESSENGER_END_FAST br x17 // call imp .elseif $0 == GETIMP mov x0, x17 // return imp ret .elseif $0 == LOOKUP ret // return imp via x17 .else .abort oops .endif .endmacro .macro CheckMiss // miss if bucket->sel == 0 .if $0 == GETIMP cbz x9, LGetImpMiss .elseif $0 == NORMAL cbz x9, __objc_msgSend_uncached .elseif $0 == LOOKUP cbz x9, __objc_msgLookup_uncached .else .abort oops .endif .endmacro .macro JumpMiss .if $0 == GETIMP b LGetImpMiss .elseif $0 == NORMAL b __objc_msgSend_uncached .elseif $0 == LOOKUP b __objc_msgLookup_uncached .else .abort oops .endif .endmacro
如果第一次沒有找到該緩存那么就調用宏
CheckMiss $0
,也就是執行cbz x9, __objc_msgSend_uncached
,嘛意思呢,就是x9和0比較,如果相等則跳轉__objc_msgSend_uncached
,其內部實現主要是調用__class_lookupMethodAndLoadCache3
,這個是c代碼實現的,后面再說。cmp x12, x10
,這里是比較cache.buckets()和當前指向的bucket比較,看是否是一樣。b.eq 3f
相等跳轉到標簽3,否則順序執行下一指令。ldp x9, x17, [x12, #-16]!
就是將bucket自減,取下一條數據,跳轉到標簽1循環執行接步驟9,x12=x12+w11<<4,并進入下一部分代碼。可以發現與之前的代碼幾乎一致,除了末尾的JumpMiss,也就是出口。
雖然解讀了代碼,但具體是在做什么邏輯,可能還是不太明白,需要說明一下。
首先_buckets是一個簡單的hash表,就是數據結構課上講的那種最基本hash表,hash值計算公式就是最簡單的hash=sel地址%mask,其中mask就是存儲空間的大小,初始大小是4,如果不夠用時(使用空間大于總空間的3/4)則增長一倍。根據sel地址計算出的hash值作為偏移量存儲IMP。
有了這個基礎,再回顧上面的代碼邏輯。
如果當前sel的地址與存儲的bucket->sel一樣,那就表示已經有緩存了,直接調用即可。否則檢查bucket->sel是否為0,如果為0則表明肯定還沒有建立緩存,則直接調用c代碼建立緩存。如果不等于0,則表示此處被其他的sel占用了,這時候就需要通過逐項搜索檢查是否已經緩存(因為已計算了index,所以搜索距離會大幅減少),同時檢查bucket是不是已經移動到最開始,如果不是則移動指針查找下一個bucket,否則將bucket直接跳轉到最末尾繼續查找。還是畫個圖吧,這樣就清晰了。
需要注意的是這里用了一個小技巧,bucket的查找是反向的,這樣可以不需要知道bucket具體大小,就可以判斷是否已經查找完前部,然后跳轉到后部。
了解了上面的邏輯,可以逆向出的C代碼如下:
id objc_msgSend_c(id obj, SEL sel) {
id localObj = obj;
int64_t obj_i = (int64_t)obj;
if (obj_i == 0) return nil;
if (obj_i < 0) {
//tagged pointer
uintptr_t obj_ui = (uintptr_t)obj_i;
if (obj_ui >= _OBJC_TAG_EXT_MASK) {
uint16_t index = (obj_ui << _OBJC_TAG_PAYLOAD_LSHIFT) >> (_OBJC_TAG_EXT_INDEX_SHIFT + _OBJC_TAG_PAYLOAD_LSHIFT);
localObj = objc_tag_ext_classes[index];
} else {
uint16_t index = obj_ui >> _OBJC_TAG_INDEX_SHIFT;
localObj = objc_tag_classes[index];
}
}
Class cls = localObj->ISA();
cache_t cache = cls->cache;
uintptr_t sel_i = (uintptr_t)sel;
bucket_t *bucket = &(cache.buckets()[sel_i & cache.mask()]);
do {
if (bucket->key() == sel_i) {
return (id)bucket->imp();
}
if (bucket->key() == 0) {
//調用匯編方法__objc_msgSend_uncached();
//其直接調用了c方法__class_lookupMethodAndLoadCache3
}
} while((cache.buckets() == bucket) ?
bucket = &(cache.buckets()[cache.mask()])
: --bucket);
return nil;
}
可以看出objc_msgSend只用匯編寫了很少的代碼,只包含tagged指針處理和方法緩存查找,但是其帶來的效率提高卻是巨大的,非常符合28原則,80%情況下調用了20%代碼,蘋果就是在這20%的代碼上盡可能的提高效率,帶來明顯的收益。
緩存的建立
以上就是是緩存的查找邏輯,那么究竟是否正確,我們需要找到緩存的建立邏輯相互印證,才能得出結論。
進入之前說到的__objc_msgSend_uncached
,其就兩句MethodTableLookup; br x17
,而前一句里面則直接跳轉bl __class_lookupMethodAndLoadCache3
,其緩存加載的主線調用邏輯如下(其他邏輯暫時先不關注)
lookUpImpOrForward -> log_and_fill_cache->cache_fill -> cache_fill_nolock
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// 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();
}
// 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.
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();
bucket->set(key, imp);
}
- key就是sel的地址。
- 調用cache->find查找緩存,如果沒有找到,則添加新緩存,調用incrementOccupied將occupied++;
- 只要調用了本函數,不管有沒有找到,都把原緩存覆蓋掉。
那么find是怎么完成的呢?相關代碼如下
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
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);
}
static inline mask_t cache_hash(cache_key_t key, mask_t mask) {
return (mask_t)(key & mask);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
代碼邏輯還是很好理解的,其查找循環邏輯和之前逆向的邏輯是等效的,不一樣的是循環退出的邏輯,但兩者本來功能就不一樣。兩相印證,可以確認逆向代碼應該是正確的。
接下來聊聊在類的方法列表中查找方法實現。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
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.
curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
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 = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
// 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;
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.unlockRead();
return imp;
}
- 這里將根據cache開關,覺得是否查找緩存中的實現。但下面卻有直接查找的調用,這可能是蘋果的一個小失誤,但不會有什么副作用,也不會有bug。其中cache_getImp是匯編實現的,直接使用了之前objc_msgSend中的CacheLookup宏,只不過參數是GETIMP,所以其只查找imp不調用,找不到也沒有關系。
- 調用getMethodNoSuper_nolock,顧名思義,在當前類的查找該方法。這里需要說明的是,類中的方法列表是一個二維數組,其中第一維存著各Category方法列表或Class方法列表的指針,第二維才是具體的方法列表。其中Class方法列表的指針只有1個或0個。如果找到對應的方法就加載到緩存中。
- 如果前面都沒找到,那么就進入循環依次去父類查找。首先查找父類的緩存,如果找到并檢查是否是
_objc_msgForward_impcache
message轉發IMP,因為后面邏輯顯示,該方法實現也會被加載到緩存中。如果不是,則表明找到了對應的方法,記錄到緩存,否則就退出循環。如果緩存沒有則跟2中一樣在該類的方法列表中查找。 - 如果沒有最終都沒有找到IMP,則調用
_class_resolveMethod
看能否響應該消息。 - 如果第四步都沒有響應,則返回
_objc_msgForward_impcache
,并記錄緩存。
__objc_msgForward_impcache由匯編實現,其代碼如下
STATIC_ENTRY __objc_msgForward_impcache
MESSENGER_START
nop
MESSENGER_END_SLOW
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr x17, [x17, __objc_forward_handler@PAGEOFF]
br x17
END_ENTRY __objc_msgForward
其調用了__objc_forward_handler()
,查源碼可知void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
,而這個默認的實現內部沒有任何實質性的功能。但有以下代碼可以在其他地方可以調用該函數該改變這個默認的實現,
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
可搜索runtime源碼并無調用痕跡,線索在此就斷掉了。
不過我們可以下個斷點,看被誰調用了。
我們發現其在dyld加載image時被ImageLoaderMachO::doImageInit
調用了,到dyld的源碼查找該函數,發現其循環調用了Image下注冊所有load_command
對應的Initializer函數。也就是說__CFInitialize
是由其他Image文件提供的。我們知道CF是CoreFaundation簡寫,我們到CoreFaundation的源碼中搜索發現確實有__CFInitialize
,但是卻沒有對objc_setForwardHandler調用,全局搜索也沒有。
不過在上圖斷點的調用中我們發現objc_setForwardHandler有getenv
和_CFStringGetUserDefaultEncoding
,而__CFInitialize
源碼中確實也有這兩句,應該是蘋果在開放CoreFaundation的時候由于某些原因刪除了相關的代碼。可以通過Mac下的系統的CoreFoundation庫查找__forwarding__
實現體(注意不是.tbd,tbd只包含描述,不包括實質內容,模擬器的dylib文件Mac下找。iOS就麻煩點,有越獄機就容易了,可惜我手上沒有越獄手機),通過ida就很容易發現有該函數實現體,不過在自動逆向的時候出了問題。
我嘗試人肉逆向該函數,如果僅僅只是需要了解大致轉發邏輯流程,相對容易,而且已經有人做了(參考鏈接Hmmm, What's that Selector? ),我和匯編代碼對照了一下,基本上是正確的,但很多細節被拋棄了,當然主要是這些細節破解確實比較麻煩,難以了解其背后C代碼的邏輯意義。目前我嘗試在破解這些細節,但結果不是特別滿意,所以也就沒有貼逆向的代碼,如果之后有比較好的進展再給出源碼。
總結
雖然分析說明的過程比較復雜,但是消息處理流程比較容易理解的。objc_msgSend匯編部分僅僅完成很少的緩存查找功能,如果找不到就會調用C方法去對象的方法二維數組中找,找不到再查父類的緩存(這也是匯編實現的)和父類的方法數組,一直找到根類,如果此過程中找到對應的方法則調用并添加緩存,如果沒有找到,則表明該繼承體系都沒有直接實現該方法,這時runtime會調用對象的方法決議去嘗試解決。如果不行則由CoreFoundation框架提供的__forwarding__
來轉發到其他對象處理,若還不能處理則拋出異常。