objc_msgSend
是OC中調(diào)用最為頻繁的方法,所有OC方法的調(diào)用都離不開這個(gè)它选侨。蘋果已經(jīng)將其開源(https://opensource.apple.com/source/objc4/objc4-750/runtime/Messengers.subproj/)傻寂,這是使用匯編語(yǔ)言編寫的,其好處就是能提升函數(shù)的執(zhí)行速度。本文選用它的arm64為匯編代碼(objc-msg-arm64.s)進(jìn)行分析烁兰。
函數(shù)入口
首先,找到ENTRY _objc_msgSend
這一行徊都,它是objc_msgSend的函數(shù)入口沪斟,下面逐行進(jìn)行分析:
cmp p0, #0 將傳入的第一個(gè)參數(shù)與0判斷
這里的p0實(shí)際上就是x0,其定義在arm64-asm.h里面暇矫。
b.le LNilOrTagged // (MSB tagged pointer looks negative)
如果p0<0(即最高位為1),該對(duì)象是tagged pointer币喧,實(shí)際上是一個(gè)為了節(jié)省空間而使用的特殊指針,關(guān)于它的詳細(xì)描述可以看這篇文章袱耽,而當(dāng)p0為0的時(shí)候杀餐,即代表傳入對(duì)象為nil,函數(shù)應(yīng)該立即返回朱巨,總之史翘,都先要跳到LNilOrTagged
進(jìn)行特殊處理。。
如果p0>0琼讽,則代碼會(huì)繼續(xù)執(zhí)行下去:
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
然后將x0指向內(nèi)存中的值(isa)賦值給p13必峰,然后通過(guò)GetClassFromIsa_p16
的宏后,p16得到了class的地址钻蹬。GetClassFromIsa_p16
的實(shí)現(xiàn)如下(剔除了SUPPORT_INDEXED_ISA的部分,因?yàn)樗轻槍?duì)watch的):
.macro GetClassFromIsa_p16 /* src */
and p16, $0, #ISA_MASK
#endif
ISA_MASK是定義在isa.h的宏吼蚁,其值為0x0000000ffffffff8ULL。
可以看出class的地址是isa指針跟ISA_MASK與運(yùn)算得來(lái)的问欠,其中的關(guān)系可以參考這篇文章肝匆,這里就不展開講了。
LGetIsaDone:
CacheLookup NORMAL
接下來(lái)顺献,就是查緩存的流程旗国,在講這個(gè)之前,先把其它分支條件過(guò)一遍注整。
LNilOrTagged
LNilOrTagged的實(shí)現(xiàn)723版本和750版本的不太一樣能曾,不過(guò)原理是一樣的,先看下723版本的:
LNilOrTagged:
1 b.eq LReturnZero // nil check
// tagged
2 mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
3 adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
4 ubfx x11, x0, #60, #4
5 ldr x16, [x10, x11, LSL #3]
6 b LGetIsaDone
- 首先肿轨,如果p0 = 0寿冕,則跳到
LReturnZero
返回。接下來(lái)就是處理tagged pointer的邏輯椒袍。 - tagged pointer有兩種蚂斤,一種是系統(tǒng)的,其isa的前4位為標(biāo)志位槐沼,最高位位1曙蒸。另一種是開發(fā)者擴(kuò)展的,其isa的前8位是標(biāo)志位岗钩,前4位都是1纽窟。因此,如果p0比0xf00....要大(這里是無(wú)符號(hào)比較)兼吓,就跳到
LExtTag
進(jìn)行擴(kuò)展的處理臂港,否則執(zhí)行系統(tǒng)tagged pointer的邏輯。 - 取出
_objc_debug_taggedpointer_classes
的地址加載到x10中 - 獲取x0的高4位保存到x11中(高4位也是isa指針在_objc_debug_taggedpointer_classes中的索引)
- 以x11作為索引视搏,算出對(duì)應(yīng)isa指針的內(nèi)存地址存到x16中
- 取出class地址之后執(zhí)行
LGetIsaDone
對(duì)于750的版本审孽,邏輯是這樣的:
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
750的版本并沒(méi)有區(qū)分tagged pointer是系統(tǒng)的還是擴(kuò)展的,直接就將其當(dāng)成系統(tǒng)的處理取出class地址浑娜,在這之后佑力,又將___NSUnrecognizedTaggedPointer
的地址賦給x10,如果取出的這個(gè)class地址跟NSUnrecognizedTaggedPointer相等筋遭,就代表這是一個(gè)擴(kuò)展指針(因?yàn)槿绻菙U(kuò)展指針的話打颤,最高4位必須是1暴拄,通過(guò)前面的運(yùn)算之后x16存的地址只能是一個(gè)確定的值。也可以由此推斷出___NSUnrecognizedTaggedPointer
在_objc_debug_taggedpointer_classes
中的索引是0x1111编饺。
我看不出750的實(shí)現(xiàn)方式優(yōu)越在哪個(gè)地方乖篷,看起來(lái)都是9條匯編代碼,希望有大神來(lái)解釋一下透且。
LExtTag
求擴(kuò)展的tagged pointer的class地址和系統(tǒng)的tagged pointer是類似的撕蔼,其代碼如下:
1 adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
2 ubfx x11, x0, #52, #8
3 ldr x16, [x10, x11, LSL #3]
4 b LGetIsaDone
- 取出
_objc_debug_taggedpointer_ext_classes
的地址加載到x10中。 - 去x0(isa指針)的高8位放到x11
- 通過(guò)索引求出class的地址并將其放到x16
- 執(zhí)行
LGetIsaDone
LReturnZero
如果p0=0秽誊,則說(shuō)明傳入的類為nil鲸沮,這個(gè)時(shí)候應(yīng)該執(zhí)行返回nil的邏輯
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
在arm64位中,函數(shù)整型的返回值會(huì)存在x0,x1中养距,而浮點(diǎn)數(shù)的返回值存在v1-v3中,由于不知道函數(shù)的調(diào)用者需要什么類型日熬,因此會(huì)將上述寄存器都清空棍厌,x0已經(jīng)是0了,因此不需要清空竖席。
CacheLookup
不管是哪個(gè)分支條件耘纱,來(lái)到CacheLookup這個(gè)宏之后,p16都已經(jīng)得到類的地址了毕荐,接下來(lái)就是查找緩存的過(guò)程
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
將class地址的CACHE偏移量的內(nèi)存賦值給p10和p11束析,對(duì)于CACHE的定義可以在本文件中找到:
/* Selected field offsets in class structure */
#define SUPERCLASS __SIZEOF_POINTER__
#define CACHE (2 * __SIZEOF_POINTER__)
其中SIZEOF_POINTER是8個(gè)字節(jié),因此這里偏移了16個(gè)字節(jié)憎亚,在objc-runtime-new.h中员寇,我們可以找到objc_class的實(shí)現(xiàn)如下(注意不是runtime.h里面的objc_class,后者已經(jīng)廢棄掉了):
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
對(duì)于其父結(jié)構(gòu)體objc_object,其定義在objc.h
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
這樣第美,我們可以推斷出isa指針的偏移量是0蝶锋,superClass的偏移量是8,cache的偏移量是16什往。
對(duì)于cache_t的結(jié)構(gòu)體扳缕,定義如下:
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
其中bucket_t占了8字節(jié),而mark_t占了4個(gè)字節(jié),因此ldr p10, p11, [x16, #CACHE]
的結(jié)果是p10存了_buckets别威,p11高32位存_occupied躯舔,低32位存_mask(因?yàn)閍rm64默認(rèn)是小端)
_buckets就是存緩存函數(shù)地址的地方,實(shí)際上是一個(gè)哈希表省古,_mask總是2的n次冪-1粥庄,也就是0x00....1111,通過(guò)它和函數(shù)方法可以求出函數(shù)在哈希表中的索引豺妓。
and w12, w1, w11 // x12 = _cmd & mask
通過(guò)上面的運(yùn)算飒赃,就可以得到函數(shù)方法在哈希表的索引利花,實(shí)際上就相當(dāng)于_cmd%哈希表的大小,可以看出载佳,也就是說(shuō)哈希表的構(gòu)造方法是除留余數(shù)法炒事。
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
PTRSHIFT 的定義在arm64-asm.h,也就是3。p10是_buckets的首地址蔫慧,因?yàn)閎ucket_t的大小為16個(gè)字節(jié)挠乳,所以需要將索引乘以16,也就是左移3位。計(jì)算完之后姑躲,p12里面就是對(duì)應(yīng)的bucket的指針了睡扬。
ldp p17, p9, [x12] // {imp, sel} = *bucket
將bucket加載到p17和p9,bucket_t的結(jié)構(gòu)如下:
struct bucket_t {
MethodCacheIMP _imp;
cache_key_t _key;
...
通過(guò)這一運(yùn)算,p17存放了_imp黍析,p9存放了_key,而_key實(shí)際上就是sel卖怜。
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
找到sel后,就讓傳進(jìn)來(lái)的sel和找到的sel作對(duì)比阐枣,如果一樣马靠,則跳到CacheHit執(zhí)行函數(shù),如果沒(méi)找到蔼两,可能是出現(xiàn)哈希沖突了甩鳄,開始繼續(xù)查找找的邏輯。
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b
CheckMiss的作用是找出來(lái)的sel是否為nil,是的話就跳出匯編用C語(yǔ)言的方式找额划,其實(shí)現(xiàn)一會(huì)再講妙啃,如果p12和p10相等,即找到的bucket是buckets的首地址俊戳,那就跳到3(跳到最后一個(gè)bucket繼續(xù)查找)如果不是揖赴,則倒序查找跳到前一個(gè)bucket,跳回1繼續(xù)查找抑胎。
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
能來(lái)到3储笑,說(shuō)明找到的bucket是buckets的第一個(gè),這個(gè)時(shí)候圆恤,跳到最后一個(gè)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
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
接下來(lái)執(zhí)行的1,2跟上面的1突倍,2一樣,不一樣的是盆昙,如果再次碰到第一個(gè)bucket羽历,就跳出匯編。
CacheHit的宏如下:
.macro CacheHit
TailCallCachedImp x17, x12 // authenticate and call imp
.endmacro
對(duì)于TailCallCachedImp淡喜,它定義在arm64-asm.h
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp
brab $0, $1
.endmacro
這個(gè)時(shí)候秕磷,x12存了IMP的地址,x17存了保存的IMP炼团,但是brab是什么命令我沒(méi)查到澎嚣,大意應(yīng)該就是調(diào)用了這個(gè)緩存的函數(shù)疏尿。
總結(jié)一下CacheLookup這個(gè)流程,如果緩存高級(jí)語(yǔ)言的寫法易桃,那應(yīng)該就是:
bucket_t bucket = class->cache->buctet[sel]
if (sel == bucket->_key){
bucket]->_imp()
} else{
//執(zhí)行C語(yǔ)言的邏輯
}
緩存找不到的case
不管是JumpMiss還是CheckMiss的時(shí)候sel為空(也就是沒(méi)有找到緩存的sel)最后都會(huì)來(lái)到
_class_lookupMethodAndLoadCache3這個(gè)C函數(shù)這個(gè)函數(shù)定義在objc-runtime-new.mm褥琐,代碼如下:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
這個(gè)方法返回查找過(guò)后IMP指針,供匯編代碼調(diào)用晤郑,這里就不貼出返回之后的邏輯了敌呈。而lookUpImpOrForward是一個(gè)查找方法的函數(shù)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
//因?yàn)閭魅氲腸ache為NO,所以不會(huì)執(zhí)行(匯編已經(jīng)執(zhí)行過(guò)一遍了)
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
//檢查類是否是已知的類造寝,如果是NSClassFromString()方法得到的磕洪,那有可能是未知的
checkIsKnownClass(cls);
// 判斷類是否已經(jīng)實(shí)現(xiàn),如果沒(méi)有先將其實(shí)現(xiàn)
if (!cls->isRealized()) {
realizeClass(cls);
}
//檢查類是否被初始化诫龙,如果沒(méi)有析显,則將其初始化
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//嘗試在緩存里找
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 在本類的方法列表中查找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// 在父類中查找,也是先找緩存,再找方法列表签赃,如果找到谷异,則將該方法緩存到該類中
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
//判斷這是不是消息轉(zhuǎn)發(fā)的方法
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
//如果是消息轉(zhuǎn)發(fā),先不調(diào)用
//先調(diào)用resolveInstanceMethod
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;
}
}
}
// 如果沒(méi)找到實(shí)現(xiàn)姊舵,則調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel
//和+ (BOOL)resolveInstanceMethod:(SEL)sel方法晰绎,重新試一次
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
//如果還是找不到寓落,走消息轉(zhuǎn)發(fā)
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
這個(gè)方法其實(shí)就是在類中查找方法的整套流程實(shí)現(xiàn)括丁。這個(gè)過(guò)程是線程安全的。找到的方法都會(huì)調(diào)用cache_fill存到緩存里面伶选。一旦方法被緩存起來(lái)史飞,下次調(diào)用的時(shí)候則只需要執(zhí)行匯編的代碼就可以找到方法。大大地提高代碼執(zhí)行的效率仰税。