在OC中,方法本質(zhì)上又是什么枯夜?我們調(diào)用一個方法的時候究竟發(fā)生了什么挺智?
方法的本質(zhì)
我們新建一個項目祷愉,在main.m中實現(xiàn)入下代碼。
int main(int argc, const char * argv[]) {
@autoreleasepool {
JKPerson *person = [[JKPerson alloc] init];
[person saySomething];
}
return 0;
}
通過clang來編譯這個main.m文件赦颇。
clang -rewrite-objc main.m
執(zhí)行完這條命令后我們會發(fā)現(xiàn)二鳄,在當前mian.m所在的文件目錄下生成了一個新的main.cpp文件。
在main.cpp文件的最底部媒怯,我們發(fā)現(xiàn)我們main.m中main函數(shù)中的代碼被編譯成了如下形式订讼。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
JKPerson *person = ((JKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((JKPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JKPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
}
return 0;
}
這段對象調(diào)用方法的代碼
[person saySomething];
被編譯成了如下形式
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
也就是說我們的OC方法其本質(zhì)上就是通過調(diào)用objc_msgSend函數(shù)來發(fā)送消息。
接下來我們來看看在objc_msgSend中究竟做了什么事情扇苞。
objc_msgSend
我們通過給objc_msgSend下符號斷點得知objc_msgSend函數(shù)在我們的libobjc.A.dylib中欺殿。
接下來我們在libobjc.A.dylib中來查看我們的objc_msgSend源碼寄纵。
我們以objc-msg-arm64.s為研究對象。
我們發(fā)現(xiàn)objc_msgSend使用匯編來實現(xiàn)的脖苏,為什么要用匯編來實現(xiàn)呢程拭?有以下幾點原因
匯編更加容易被機器識別,效率更高棍潘。
C語言中不可以通過一個函數(shù)來保留未知的參數(shù)并且跳轉(zhuǎn)到任意的函數(shù)指針恃鞋。C語言沒有滿足這些事情的必要特性。
在objc_msgSend中摘取其中關(guān)鍵代碼如下
ENTRY _objc_msgSend
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL
END_ENTRY _objc_msgSend
我們可以看到在獲取到Isa之后我們開啟了方法的緩存查找流程
LGetIsaDone:
CacheLookup NORMAL
查找緩存
我們摘取CacheLookup關(guān)鍵代碼如下亦歉。
.macro CacheLookup
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: // 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.
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
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
.endmacro
我們可以看出其中有兩個比較重要的恤浪,一個是CacheHit命中緩存,這個時候緩存命中了之后直接返回imp
另一個是CheckMiss,我們來看看CheckMiss做了什么
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
我們跟隨__objc_msgSend_uncached和__objc_msgLookup_uncached流程繼續(xù)往下看
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
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
我們可以看到他們都調(diào)用了一個叫MethodTableLookup的東西资锰。
摘取其中關(guān)鍵代碼如下。
.macro MethodTableLookup
bl __class_lookupMethodAndLoadCache3
.endmacro
流程圖:
至此我們關(guān)于objc_msgSend的匯編部分結(jié)束了阶祭,接下來將進入C/C++的查找流程。我們將在下篇文章中介紹objc_msgSend的慢速查找流程直秆。