引入
眾所周知,Objective-C動態(tài)性的根源在方法的調(diào)用是通過message來實現(xiàn)的涝婉,一次發(fā)生message的過程就是一次方法的調(diào)用過程劲件。發(fā)送message只需要指定對象和SEL未巫,Runtime的objc_msgSend會根據(jù)在信息在對象isa指針指向的Class中尋找該SEL對應的IMP,從而完成方法的調(diào)用蔗草。這樣每次方法的調(diào)用必然會有方法的查找過程暇榴,如果頻繁調(diào)用,或者Class的方法列表過大蕉世,很容易導致性能瓶頸蔼紧,但OC似乎并沒有這個問題,這得益于蘋果的優(yōu)化機制狠轻,其中包括純匯編的objc_msgSend實現(xiàn)(不用匯編參數(shù)暫存困難奸例,當然也不是沒有辦法,總有一些歪招可以解決向楼,但為了兼顧效率自然匯編更加合適)查吊,方法查找cache,TaggedPointer等等技術才帶來OC極高的效率湖蜕。接下來我們從objc_msgSend為引逻卖,來解讀整個過程。
Objective-C動態(tài)化的核心objc_msgSend
先說幾句閑話昭抒,如果大學期間學習過匯編課程的同學就知道评也,相同的邏輯,c語言寫出的函數(shù)匯編成.s文件和直接匯編的的文件灭返,體積差異是很大的盗迟,幾倍到幾十倍的差距,由此可見兩者的效率差距也十分巨大熙含,這也是蘋果為什么非要用匯編去干這個事情罚缕。而且蘋果匯編只實現(xiàn)了cache方法的查找過程,并未匯編實現(xiàn)class所有的方法查找怎静,因為前者調(diào)用非常頻繁邮弹,后者卻不是黔衡,其在運行效率和開發(fā)效率(可靠性)作了一個平衡。
進入正題腌乡,我們知道對于任意的OC方法的調(diào)用盟劫,比如[obj aMethod];
都會被翻譯成objc_msgSend(obj, sel/*@selector(aMethod)*/);
,由此進入objc_msgSend執(zhí)行导饲,而該方法實現(xiàn)是匯編完成的捞高,這對解讀造成了一定的困擾妥粟,所以我們不得不迎難而上变过,搞定整個方法的邏輯過程男韧。
我這里下載的objc_706的源碼,這里我只解讀objc-msg-arm64.s文件袋毙,其他處理器架構(gòu)的除了實現(xiàn)細節(jié)出入,其查找邏輯的類似冗尤。
文件的最開始聲明了12個私有的_objc_entryPoints听盖,其中包括我們關注的.quad _objc_msgSend
在文件中搜索"_objc_msgSend",會找到以下匯編代碼裂七,這就是其實現(xiàn)的一部分皆看,接下來我們將一步步解讀它。
.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
數(shù)據(jù)結(jié)構(gòu)定義
咋一看背零,只能了解其定義了一些數(shù)據(jù)存儲的空間腰吟,里面存儲的應該是指針,而且這些指針3bit對齊徙瓶,似乎跟OC的objc_object指針很類似毛雇,然后通過.fill偽指令,將所有的數(shù)據(jù)單元填0侦镇。至于這些數(shù)據(jù)怎么使用我們并不了解灵疮,沒關系,我們繼續(xù)往下看壳繁。
找到了ENTRY _objc_msgSend
震捣,顧名思義這就是真正的函數(shù)入口了。我們搜索ENTRY闹炉,其定義如下:
.macro ENTRY /* name */
.text
.align 5
.globl $0
$0:
.endmacro
這里定義了一個匯編宏伍派,表示text段,定義一個global的_objc_msgSend剩胁,"$0"同時生產(chǎn)一個函數(shù)入口標簽诉植。
UNWIND _objc_msgSend, NoFrame
則定義了一些段存儲數(shù)據(jù)對象,簡單來說就是類似于結(jié)構(gòu)體數(shù)據(jù)對象昵观,具體意義我也不是很了解晾腔。
MESSENGER_START
定義了一些方法調(diào)用開始的調(diào)試數(shù)據(jù)舌稀,具體對應到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;
};
函數(shù)邏輯主體
邏輯部分有很多子邏輯,我們一步一步解讀
tagged pointer處理
cmp x0, #0
灼擂,從注釋可以了解是在和"0"比較壁查,比較的結(jié)果會有三種,大于小于等于剔应。
這里邏輯是
b.le LNilOrTagged
睡腿,即如果小于等于就跳轉(zhuǎn)到標簽:LNilOrTagged。(因為nil==0峻贮,tagged指針最高位是1(符號位)席怪,所以肯定小于0)。跳轉(zhuǎn)到LNilOrTagged后纤控,執(zhí)行
b.eq LReturnZero
繼續(xù)檢查比較結(jié)果挂捻,相等則跳轉(zhuǎn)標簽:LReturnZero在LReturnZero中,將x1置為0船万,浮點寄存器d1,d2,d3,d4全部置為0刻撒。這是因為objc_msgSend并不知道,該函數(shù)調(diào)用期望返回的是什么數(shù)據(jù)類型耿导,可能是浮點声怔,整型,指針舱呻,甚至可能結(jié)構(gòu)體醋火,所以其將常用的返回值的寄存器,全部清0狮荔。但對于復雜的結(jié)構(gòu)體胎撇,objc_msgSend就無能為力了(因為其不知道這些數(shù)據(jù)的大小)殖氏,它只能將返回結(jié)果放入x8寄存器晚树,由另外代碼去清0,而這部分代碼則編譯器在編譯的時候根據(jù)相關數(shù)據(jù)類型生成雅采。
-
如果
b.eq LReturnZero
不成立爵憎,則表明該數(shù)據(jù)是個tagged pointer,需要進一步處理才能做調(diào)用婚瓜。mov x10, #0xf000000000000000; cmp x0, x10
宝鼓,這兩句就很明顯了。比較其最高的4bit巴刻,這應該是個標記位愚铡。我們?nèi)ニ阉髌湎嚓P的定義#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()
碍舍,發(fā)現(xiàn)tagged指針相關的操作這里都有,其中_objc_makeTaggedPointer
的功能是實現(xiàn)原始數(shù)據(jù)封裝成tagged指針邑雅。
這里我們畫了一個簡圖片橡,是arm64下的存儲結(jié)構(gòu)(其他CPU下并不一樣,比如X86_64淮野,data存在前部捧书,tag存后部),如果是個擴展的tagged指針骤星,其中0-51位是數(shù)據(jù)部分经瓷,52-59這8個bit是擴展tagged部分,60到62的3bit是tagged妈踊,63是tagged指針標記了嚎。如果是一個tagged指針0-59是數(shù)據(jù)泪漂,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文件中有以下聲明慎式,所以這兩個數(shù)據(jù)是匯編語言定義,但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
瘪吏,如果比較結(jié)果是大于等于,則表示這是個擴展的tagged蜗巧,跳轉(zhuǎn)到標簽:LExtTag
- 接來兩句是加載
_objc_debug_taggedpointer_ext_classes
. ubfx x11, x0, #52, #8
的意思是x11= (x0 & 0x0ff0000000000000)>>52掌眠,即取第52-59bit的數(shù)據(jù)。
. ldr x16, [x10, x11, LSL #3]
幕屹,x16=x10+(x11<<3)蓝丙,左移三位是因為_objc_debug_taggedpointer_ext_classes
是8個byte為單位來偏移的,后面做匯編逆向源碼的時候會有類似的例子來說明望拖。
如果第五步不成立渺尘,則執(zhí)行按照正常的tagged指針處理,加載
_objc_debug_taggedpointer_classes
说敏,取出第60-63bit鸥跟,左移三位,取出真正的isa盔沫。跳轉(zhuǎn)LGetIsaDone医咨,其使用了匯編宏CacheLookup蚂夕,參數(shù)NORMAL,其用于搜索緩存腋逆。
到這里婿牍,匯編的第一部分就解讀完成了,其主要就是解析當前指針得到對應的class以備后續(xù)處理惩歉。為了更好的理解等脂,這里我貼出逆向出來的源碼,以便于理解撑蚌。
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代碼,所以很容易了解其大概做了什么邏輯争涌,但要看懂具體細節(jié)粉楚,并逆向出源碼,對于不常玩匯編的人來說亮垫,還是有那么一點困難模软。
-
之前操作,已經(jīng)在x1和x16中存入處理好的相關數(shù)據(jù)饮潦,x0=obj燃异,x1=SEL,x16=isa继蜡。立即數(shù)#CACHE=16回俐,OC對象的內(nèi)存布局中,前面分別是isa和superclass指針稀并,x16+16就是cache的地址仅颇。cache結(jié)構(gòu)體數(shù)據(jù)布局如下。
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的數(shù)據(jù)到x9和x17。cmp x9, x1
夺鲜,比較x9與x1皆尔,也就是bucket->_key與SEL。b.ne 2f
如果不相等跳轉(zhuǎn)到標簽2-
接上面一步币励,如果相等則
CacheHit $0
慷蠕,表示命中緩存,CacheHit是一個宏食呻,“$0”是第一個參數(shù)流炕,就是之前的NORMAL。這是將執(zhí)行br x17
仅胞,跳轉(zhuǎn)到寄存器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
如果第一次沒有找到該緩存那么就調(diào)用宏
CheckMiss $0
影兽,也就是執(zhí)行cbz x9, __objc_msgSend_uncached
揭斧,嘛意思呢莱革,就是x9和0比較,如果相等則跳轉(zhuǎn)__objc_msgSend_uncached
讹开,其內(nèi)部實現(xiàn)主要是調(diào)用__class_lookupMethodAndLoadCache3
盅视,這個是c代碼實現(xiàn)的,后面再說旦万。cmp x12, x10
闹击,這里是比較cache.buckets()和當前指向的bucket比較,看是否是一樣成艘。b.eq 3f
相等跳轉(zhuǎn)到標簽3赏半,否則順序執(zhí)行下一指令。ldp x9, x17, [x12, #-16]!
就是將bucket自減淆两,取下一條數(shù)據(jù)断箫,跳轉(zhuǎn)到標簽1循環(huán)執(zhí)行接步驟9,x12=x12+w11<<4秋冰,并進入下一部分代碼仲义。可以發(fā)現(xiàn)與之前的代碼幾乎一致,除了末尾的JumpMiss埃撵,也就是出口赵颅。
雖然解讀了代碼,但具體是在做什么邏輯暂刘,可能還是不太明白饺谬,需要說明一下。
首先_buckets是一個簡單的hash表谣拣,就是數(shù)據(jù)結(jié)構(gòu)課上講的那種最基本hash表商蕴,hash值計算公式就是最簡單的hash=sel地址%mask,其中mask就是存儲空間的大小芝发,初始大小是4绪商,如果不夠用時(使用空間大于總空間的3/4)則增長一倍。根據(jù)sel地址計算出的hash值作為偏移量存儲IMP辅鲸。
有了這個基礎格郁,再回顧上面的代碼邏輯。
如果當前sel的地址與存儲的bucket->sel一樣独悴,那就表示已經(jīng)有緩存了例书,直接調(diào)用即可。否則檢查bucket->sel是否為0刻炒,如果為0則表明肯定還沒有建立緩存决采,則直接調(diào)用c代碼建立緩存。如果不等于0坟奥,則表示此處被其他的sel占用了树瞭,這時候就需要通過逐項搜索檢查是否已經(jīng)緩存(因為已計算了index,所以搜索距離會大幅減少)爱谁,同時檢查bucket是不是已經(jīng)移動到最開始晒喷,如果不是則移動指針查找下一個bucket,否則將bucket直接跳轉(zhuǎn)到最末尾繼續(xù)查找访敌。還是畫個圖吧凉敲,這樣就清晰了。
需要注意的是這里用了一個小技巧寺旺,bucket的查找是反向的爷抓,這樣可以不需要知道bucket具體大小,就可以判斷是否已經(jīng)查找完前部阻塑,然后跳轉(zhuǎn)到后部蓝撇。
了解了上面的邏輯,可以逆向出的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) {
//調(diào)用匯編方法__objc_msgSend_uncached();
//其直接調(diào)用了c方法__class_lookupMethodAndLoadCache3
}
} while((cache.buckets() == bucket) ?
bucket = &(cache.buckets()[cache.mask()])
: --bucket);
return nil;
}
可以看出objc_msgSend只用匯編寫了很少的代碼叮姑,只包含tagged指針處理和方法緩存查找唉地,但是其帶來的效率提高卻是巨大的据悔,非常符合28原則,80%情況下調(diào)用了20%代碼耘沼,蘋果就是在這20%的代碼上盡可能的提高效率极颓,帶來明顯的收益。
緩存的建立
以上就是是緩存的查找邏輯群嗤,那么究竟是否正確菠隆,我們需要找到緩存的建立邏輯相互印證,才能得出結(jié)論狂秘。
進入之前說到的__objc_msgSend_uncached
骇径,其就兩句MethodTableLookup; br x17
,而前一句里面則直接跳轉(zhuǎn)bl __class_lookupMethodAndLoadCache3
者春,其緩存加載的主線調(diào)用邏輯如下(其他邏輯暫時先不關注)
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的地址破衔。
- 調(diào)用cache->find查找緩存,如果沒有找到钱烟,則添加新緩存晰筛,調(diào)用incrementOccupied將occupied++;
- 只要調(diào)用了本函數(shù)拴袭,不管有沒有找到读第,都把原緩存覆蓋掉。
那么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;
}
代碼邏輯還是很好理解的怜瞒,其查找循環(huán)邏輯和之前逆向的邏輯是等效的,不一樣的是循環(huán)退出的邏輯般哼,但兩者本來功能就不一樣吴汪。兩相印證,可以確認逆向代碼應該是正確的逝她。
接下來聊聊在類的方法列表中查找方法實現(xiàn)浇坐。
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;
}
- 這里將根據(jù)cache開關,覺得是否查找緩存中的實現(xiàn)黔宛。但下面卻有直接查找的調(diào)用,這可能是蘋果的一個小失誤擒贸,但不會有什么副作用臀晃,也不會有bug。其中cache_getImp是匯編實現(xiàn)的介劫,直接使用了之前objc_msgSend中的CacheLookup宏徽惋,只不過參數(shù)是GETIMP,所以其只查找imp不調(diào)用座韵,找不到也沒有關系险绘。
- 調(diào)用getMethodNoSuper_nolock踢京,顧名思義,在當前類的查找該方法宦棺。這里需要說明的是瓣距,類中的方法列表是一個二維數(shù)組,其中第一維存著各Category方法列表或Class方法列表的指針代咸,第二維才是具體的方法列表蹈丸。其中Class方法列表的指針只有1個或0個。如果找到對應的方法就加載到緩存中呐芥。
- 如果前面都沒找到逻杖,那么就進入循環(huán)依次去父類查找。首先查找父類的緩存思瘟,如果找到并檢查是否是
_objc_msgForward_impcache
message轉(zhuǎn)發(fā)IMP荸百,因為后面邏輯顯示,該方法實現(xiàn)也會被加載到緩存中滨攻。如果不是管搪,則表明找到了對應的方法,記錄到緩存铡买,否則就退出循環(huán)更鲁。如果緩存沒有則跟2中一樣在該類的方法列表中查找。 - 如果沒有最終都沒有找到IMP奇钞,則調(diào)用
_class_resolveMethod
看能否響應該消息澡为。 - 如果第四步都沒有響應,則返回
_objc_msgForward_impcache
景埃,并記錄緩存媒至。
__objc_msgForward_impcache由匯編實現(xiàn),其代碼如下
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
其調(diào)用了__objc_forward_handler()
谷徙,查源碼可知void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
拒啰,而這個默認的實現(xiàn)內(nèi)部沒有任何實質(zhì)性的功能。但有以下代碼可以在其他地方可以調(diào)用該函數(shù)該改變這個默認的實現(xiàn)完慧,
void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
_objc_forward_handler = fwd;
#if SUPPORT_STRET
_objc_forward_stret_handler = fwd_stret;
#endif
}
可搜索runtime源碼并無調(diào)用痕跡谋旦,線索在此就斷掉了。
不過我們可以下個斷點屈尼,看被誰調(diào)用了册着。
我們發(fā)現(xiàn)其在dyld加載image時被ImageLoaderMachO::doImageInit
調(diào)用了,到dyld的源碼查找該函數(shù)脾歧,發(fā)現(xiàn)其循環(huán)調(diào)用了Image下注冊所有load_command
對應的Initializer函數(shù)甲捏。也就是說__CFInitialize
是由其他Image文件提供的。我們知道CF是CoreFaundation簡寫鞭执,我們到CoreFaundation的源碼中搜索發(fā)現(xiàn)確實有__CFInitialize
司顿,但是卻沒有對objc_setForwardHandler調(diào)用芒粹,全局搜索也沒有。
不過在上圖斷點的調(diào)用中我們發(fā)現(xiàn)objc_setForwardHandler有getenv
和_CFStringGetUserDefaultEncoding
大溜,而__CFInitialize
源碼中確實也有這兩句化漆,應該是蘋果在開放CoreFaundation的時候由于某些原因刪除了相關的代碼×蕴幔可以通過Mac下的系統(tǒng)的CoreFoundation庫查找__forwarding__
實現(xiàn)體(注意不是.tbd获三,tbd只包含描述,不包括實質(zhì)內(nèi)容锨苏,模擬器的dylib文件Mac下找疙教。iOS就麻煩點,有越獄機就容易了伞租,可惜我手上沒有越獄手機)贞谓,通過ida就很容易發(fā)現(xiàn)有該函數(shù)實現(xiàn)體,不過在自動逆向的時候出了問題葵诈。
我嘗試人肉逆向該函數(shù)裸弦,如果僅僅只是需要了解大致轉(zhuǎn)發(fā)邏輯流程,相對容易作喘,而且已經(jīng)有人做了(參考鏈接Hmmm, What's that Selector? )理疙,我和匯編代碼對照了一下,基本上是正確的泞坦,但很多細節(jié)被拋棄了窖贤,當然主要是這些細節(jié)破解確實比較麻煩,難以了解其背后C代碼的邏輯意義贰锁。目前我嘗試在破解這些細節(jié)赃梧,但結(jié)果不是特別滿意,所以也就沒有貼逆向的代碼豌熄,如果之后有比較好的進展再給出源碼授嘀。
總結(jié)
雖然分析說明的過程比較復雜,但是消息處理流程比較容易理解的锣险。objc_msgSend匯編部分僅僅完成很少的緩存查找功能蹄皱,如果找不到就會調(diào)用C方法去對象的方法二維數(shù)組中找,找不到再查父類的緩存(這也是匯編實現(xiàn)的)和父類的方法數(shù)組囱持,一直找到根類夯接,如果此過程中找到對應的方法則調(diào)用并添加緩存,如果沒有找到纷妆,則表明該繼承體系都沒有直接實現(xiàn)該方法,這時runtime會調(diào)用對象的方法決議去嘗試解決晴弃。如果不行則由CoreFoundation框架提供的__forwarding__
來轉(zhuǎn)發(fā)到其他對象處理掩幢,若還不能處理則拋出異常逊拍。