上篇文章我們對cache
原理進行了分析,在摸清cache
是如何將方法信息存放進去后,我們來研究研究怎么取出所存儲的方法信息,那么本文將從objc_msgSend
入手,探究如何快速獲取
到方法信息。
編譯時 & 運行時(Runtime)
-
編譯時
:顧名思義就是正在編譯
的時候,就是編譯器幫你把源代碼翻譯
成機器能識別的代碼。(當然只是一般意義上這么說,實際上可能只是翻譯某個中間狀態
的語言
)做一些詞法分析
,語法分析
及幫你檢查代碼錯誤
的過程。可理解為一個靜態
過程。 -
運行時
:就是代碼在內存中跑起來
的整個過程。因為編譯過的代碼只是保存在磁盤
上,并沒有裝入內存
中,而且運行時所做的一些檢查
和判斷工作
與編譯時不一樣。可理解為一個動態
過程。
上圖可以看出,代碼層與Runtime
底層庫之間有一個編譯層
,在我們代碼運行時候,我們所進行的操作,比如調用方法
,給屬性賦值
等操作時,上層代碼與在經過編譯后是不一樣,就像之前碰到過的alloc
方法,編譯器處理過就變為objc_alloc
,及isKindOfClass
方法變成了objc_opt_isKindOfClass
。
我們可以利用上層代碼使用到底層Runtime:
-
[person SomeMethod]
:對象方法調用 -
isKindOfClass
:Frameworks
&Service
-
class_getInstaceSize
:Runtime API
例子探究方法本質
創建一個類LGPerson
,添加兩個方法sayHello
和sayNB
:
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
只實現sayNB
方法,暫不實現sayHello
方法。在main.m
文件的 main
方法中初始化person
對象并調用sayHello
及sayNB
方法:
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];
NSLog(@"Hello, World!");
}
return 0;
}
此時,只編譯
項目的話,發現能編譯成功
,接著運行
的話,項目就會崩潰
,并且報錯未在LGPerson
類中找到sayHello
方法實現。那么我們就能證明編譯時代碼不運行
起來,也就不會檢查是否方法有實現了,運行時會去檢查是否方法已經實現了,也就是說在運行時找不到方法實現的話程序會崩潰
。
objc_msgSend初探
接著我們使用clang
編譯main.cpp
文件,通過查看main
函數中方法調用的實現:
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
可以看出,不管是類方法還是實例方法,方法調用的本質就是objc_msgSend
方法的調用。為驗證這個,我們可以直接調用objc_msgSend方法,傳入正確參數即可:
LGPerson *person = [LGPerson alloc];
objc_msgSend(person,sel_registerName("sayNB"));
[person sayNB];
1.引入頭文件
#import <objc/message.h>
2.target
-->Build Setting
將enable strict checking of obc_msgSend calls
由YES
改為NO
,不然會報錯。
打印結果如下發現直接調用方法和利用objc_msgSend方法調用結果一致:
方法isa走位再次探究
再添加一個LGPerson
的父類LGTeacher
類,并且讓LGTeacher
類實現sayHello
方法:
@interface LGTeacher : NSObject
- (void)sayHello;
@end
@implementation LGTeacher
- (void)sayHello{
NSLog(@"666");
}
@end
@interface LGPerson : LGTeacher
- (void)sayHello;
- (void)sayNB;
@end
@implementation LGPerson
- (void)sayNB{
NSLog(@"666");
}
@end
此時編譯并運行程序,發現程序不會奔潰,并能正常
打印輸出結果。我們可以猜測對象方法在此類
中找不到
就會去父類
里面查找的猜想。
我們也可直接利用objc_msgSendSuper
方法來調用父類方法:
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
[person sayHello];
struct objc_super lgsuper;
lgsuper.receiver = person;
lgsuper.super_class = [LGTeacher class];
objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));
發現直接person
調用sayHello
方法和利用objc_msgSendSuper
方法,都能正常獲取到方法的實現,正常輸出打印結果。
objc_msgSend方法快速查找流程分析
在objc4-781
源碼可執行工程中全局搜索objc_msgSend
,在匯編文件
objc_msg_arm64.s
中找到objc_msgSend
的入口ENTRY
:
ENTRY _objc_msgSend
//--- 無窗口
UNWIND _objc_msgSend, NoFrame
//--- 判斷當前p0和空對比,判斷是否為nil,p0為當前方法第一個參數即消息接受者receiver
cmp p0, #0 // nil check and tagged pointer check
//---支持taggedpointer(小對象類型)
#if SUPPORT_TAGGED_POINTERS
//--- le 小于 進入 LNilOrTagged 流程
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//--- eq 等于0,直接返回 空
b.eq LReturnZero
#endif
//--- p0即receiver 到這里說明存在
//--- 根據對象拿出isa, 即從寄存器x0指向的地址 取出isa,存入 p13 寄存器
ldr p13, [x0] // p13 = isa
//--- 在拿到isa后再去獲取當前class信息,利用 isa(p13) & ISA_MASK,拿出相應的isa中的shiftcls信息,即獲得calss信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//--- 緩存查找,也就是所謂的sel-imp快速查找過程
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//--- 等于空 返回 空
b.eq LReturnZero // nil check
// tagged
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]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// 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
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
- 首先判斷當前消息接收者
receiver
是否存在,如果不存在就直接返回空
。 - 判斷是否支持
Tagged_Pointers
,支持的話跳轉到LNilOrTagged
:- 如果等于
0
,則返回空
- 如果不為
0
,則對小對象的isa
處理,并進入LGetIsaDone
步驟
- 如果等于
- 如果不是
小對象
,且receiver
存在,則獲取到當前對象的isa
,然后通過GetClassFromIsa_p16
獲取到當前類信息
:
arm64匯編根據isa獲取類信息
在獲取isa
流程完畢后,接著就進入CacheLookup
,進行查找過程:
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
//--- #define CACHE (2 * __SIZEOF_POINTER__) 即2 * 8 = 16
//--- p11 = mask|buckets -- 從x16(isa)中平移 16 字節,取出cache 存入p11寄存器 -- 因為isa(8字節) superClass(8字節) cache(16字節) (真機maskAndBuckets mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 將p11 & 0x0000ffffffffffff 獲取到buckets 信息 高位mask處 抹0
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//--- p11(cache) 右移48位,前面48補0得到mask信息,然后p1(_cmd-sel)& 得到的mask信息,即獲取到sel-imp 的下標index 存入p12
//--- cache存入sel-imp時候,也是按哈希算法 sel & mask下標存入的。
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
//--- 非64位真機
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//--- p10 buckets數組首地址,p12 下標(_cmd & mask)<< 4,相當于 下標*16,再加上buckets首地址,獲取到存放當前_cmd的bucket(不確定的,不要做對比確認的),存入 p12寄存器
//--- bucket(16) = sel(8) + imp(8),一個bucket占用的大小為16,首地址偏移 獲取 當前的bucket
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT 等于 3
//--- 從x12中獲取出當前bucket對應的sel 及 imp,分別存入 p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--- 比較 sel 與 p1(傳入的sel)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不想等,即沒找到,請跳至 下面 2f
b.ne 2f // scan more
//--- 如果相等,即cacheHit 緩存命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果當前的bucket里沒有存放過sel\imp信息,就退出循環,因為 經過 hash算法 sel & mask 得到的下標位置 沒有存放任何sel\imp信息的話,也就是說此位置沒被別的占用,說明就沒有緩存次方法sel\imp信息,退出循環
CheckMiss $0 // miss if bucket->sel == 0
//--- 判斷p12(當前下標對應的bucket首地址)是否等于p10(buckets數組第一個bucket的首地址)
cmp p12, p10 // wrap if bucket == buckets
//--- 如果相等,跳轉到第三步 3f 定位到最后一個bucket地址
b.eq 3f
//--- 如果bucket != buckets
//--- 通過對此位置每次進行BUCKET_SIZE大小遞減,即向前查找的方式,每次將對應的sel和imp存入p17 和 p9
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 獲取到最新的bucket對應的sel和imp,然后回到 第一步,繼續與參數_cmd比較
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人為設置查找位置為最后一個bucket
//--- mask = capacity - 1。
//--- p11(mask)右移44位,獲取到最后一個bucket的位置
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
//--- 此時查找位置為最后一個bucket ,比較sel與p1(_cmd)
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
//--- 比較此時bucket是否回到了第一個位置
//--- 判斷是否是第一個bucket位置,還沒找到,說明里面buckets里面根本就沒找到 跳到下面 3f JumpMiss
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
//--- 向前查找規則,找到每一個bucket,然后回到第一步,繼續進行sel 與 _cmd 對比
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 找不到,只能去方法列表查找了
JumpMiss $0
.endmacro
此過程首先根據
isa
信息,經過地址偏移16
獲取到cache
信息,因為isa
和superClass
分別為8字節
大小,真機環境下獲取到的cache
信息中為mask|buckets
即 前面cache源碼分析文章
看到的_maskAndBuckets
數據。然后分別利用_maskAndBuckets
數據分別經過獲取到buckets
和mask
, 利用哈希算法_cmd & mask
獲取下標
。然后根據buckets
首地址偏移下標 * 16
(bucketsize = sel + imp = 16
),p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
,其中PTRSHIFT = 3
,(_cmd & mask) << (1+PTRSHIFT)
相當于下標左移 4
,即乘以 16
,為 相對于buckets
首地址的偏移量
。所以最后加上buckets
首地址的話,就獲得了當前_cmd
對應的bucket
。接下來將當前獲取到的
bucket
中的sel
與_cmd
對比,如果相等
,說明找到了,直接返回對應的imp
,如果不想等
,進入下一步2f
首先判斷如果此時的
bucket
中的sel = 0
,即不存在sel、imp
信息,則說明根本就沒有對此方法進行過緩存,結束
搜索流程如果當前
bucket
中sel
不等于_cmd
,則說明此位置被占用
了,所以找別的位置,先判斷當前bucket
是否是buckets
中的第一個bucket
, 如果是的話,進入3f
流程,將查找位置移到buckets
中最后一個
,然后以BUCKET_SIZE(16)
大小遞減,遵循向前查找
的規則,遍歷所有bucket
,每獲取到一個bucket
,進行sel
與_cmd
比較。如果當前
bucket
不是第一個bucket
的話,遵循向前查找
的規則,比較每一個bucket
的sel
與_cmd
,如果遍歷到第一個
還是沒找到,那么繼續將查找位置移到最后一個bucket
位置,繼續向前查找
,最后第二次
到第一個bucket
時候,就說明所有的bucket
都不符合,所以結束
查找流程。