在iOS
開發(fā)中,我們常常會(huì)調(diào)用各種方法,既包括對(duì)象方法也包括類方法牍汹,那我們方法調(diào)用內(nèi)部到底是如何實(shí)現(xiàn)的呢铐维?我們今天就來一起探索一下。
一慎菲、objc_msgSend
和objc_msgSendSuper
首先嫁蛇,創(chuàng)建工程,并新建一個(gè)LPPerson
類露该,并添加一個(gè)對(duì)象方法和一個(gè)類方法睬棚。并在main.m
中完成調(diào)用:
@interface LPPerson : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayHello;
+ (void)sayHi;
}
@implementation LPPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (void)sayHi{
NSLog(@"%s",__func__);
}
@end
@interface LPSon : LPPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LPPerson *person = [LPPerson alloc];
[person sayHello];
[LPPerson sayHi];
}
return 0;
}
然后我們使用clang
編譯器胆数,將main.m
編譯成main.cpp
看下其內(nèi)部結(jié)構(gòu)见秤。因?yàn)榇a很多,并且main
在最后撬碟,所以我們直接滑到最后即可:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LPPerson *person = ((LPPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LPPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LPPerson"), sel_registerName("sayHi"));
}
return 0;
}
可以看到撵摆,不管是對(duì)象方法還是類方法底靠,包括alloc方法他們都是調(diào)用了一個(gè)叫做objc_msgSend
的函數(shù)。它的字面意思就是消息發(fā)送特铝,在Objc
源碼中進(jìn)行全局查找:
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
我們看到有objc_msgSend
和objc_msgSendSuper
這兩個(gè)函數(shù)暑中,他們的都有兩個(gè)參數(shù):
- 第一個(gè)參數(shù):表示消息接收者
- 第二個(gè)參數(shù)
SEL
:表示需要執(zhí)行的方法
既然我們調(diào)用方法就是執(zhí)行了消息發(fā)送,那我們是不是可以直接調(diào)用objc_msgSend
或者objc_msgSendSuper
呢鲫剿?
我們實(shí)驗(yàn)一下:
-
1鳄逾、首先導(dǎo)入
#import <objc/message.h>
-
2、在
main.m
中添加以下代碼:
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LPPerson *person = [LPPerson alloc];
[person sayHello];
objc_msgSend(person,sel_registerName("sayHello"));
[LPPerson sayHi];
objc_msgSend(objc_getClass("LPPerson"),sel_registerName("sayHi"));
}
return 0;
}
-
3灵莲、但是發(fā)現(xiàn)報(bào)錯(cuò)了:
這是因?yàn)橄到y(tǒng)默認(rèn)開啟的方法檢查严衬,我們需要手動(dòng)關(guān)閉。在target
下選中當(dāng)前target
笆呆,選擇buildSetting
请琳,然后搜索msg
,將Enable Strict Checking of objc_msgSend Calls
設(shè)置為NO
即可:
現(xiàn)在直接運(yùn)行:
2020-09-22 16:13:47.379526+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380326+0800[44411:14029752] -[LPPerson sayHello]
2020-09-22 16:13:47.380457+0800[44411:14029752] +[LPPerson sayHi]
2020-09-22 16:13:47.380536+0800[44411:14029752] +[LPPerson sayHi]
結(jié)果證明赠幕,直接通過objc_msgSend
調(diào)用方法是可以的俄精,objc_msgSendSuper
也是一樣的,又興趣的同學(xué)可以自己試驗(yàn)一下榕堰。
總結(jié):方法調(diào)用的本質(zhì)就是消息發(fā)送竖慧,具體是調(diào)用
runtime中objc_msgSend
和objc_msgSendSuper
函數(shù)來實(shí)現(xiàn)的。
那么objc_msgSend
和objc_msgSendSuper
中又是如何查找方法sel
和imp
呢逆屡?接下里我們就來從源碼中一探究竟圾旨,因?yàn)?code>objc_msgSend和objc_msgSendSuper
內(nèi)部邏輯實(shí)際是一樣的,所以我們接下來主要分析objc_msgSend
原理魏蔗。
二砍的、objc_msgSend
原理
進(jìn)入源碼中,我們可以發(fā)現(xiàn)objc_msgSend
是使用匯編實(shí)現(xiàn)的莺治,這是因?yàn)閰R編主要的特性是:
速度快:匯編更容易被機(jī)器識(shí)別廓鞠。
方法參數(shù)的動(dòng)態(tài)性:匯編調(diào)用函數(shù)時(shí)傳遞的參數(shù)是不確定的帚稠,那么發(fā)送消息時(shí),直接調(diào)用一個(gè)函數(shù)就可以發(fā)送所有的消息:
而在iOS
中床佳,方法查找有兩種實(shí)現(xiàn)方式:
- 快速查找滋早,從
cache
中查找,也就是我們前面講到的cache_t中存儲(chǔ)的緩存 - 慢速查找砌们,從
methodList
中查找以及消息轉(zhuǎn)發(fā)杆麸,下一篇我們會(huì)講到
在Objc
源碼中搜索objc_msgSend
,前面提到了objc_msgSend
是基于匯編的浪感,所以我們直接以.s
結(jié)尾的文件角溃,然后找到ENTRY _objc_msgSend
即可:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
///P0是objc_msgSend的第一個(gè)參數(shù),即消息接受者篮撑,這里需要判斷消息接受者是否為空
cmp p0, #0 // nil check and tagged pointer check
///判斷是支持tagged_pointer
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
//再次判斷消息接受者是否為空
#else
b.eq LReturnZero
#endif
///獲取當(dāng)前消息接受者的isa
ldr p13, [x0] // p13 = isa
///獲取當(dāng)前消息接受者的class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
///緩存中尋找imp
CacheLookup NORMAL, _objc_msgSend
接下來减细,我們繼續(xù)查看CacheLookup
的源碼:
全局搜索CacheLookup
,同樣找.s
結(jié)尾的文件赢笨,如下圖所示:
然后進(jìn)入源碼中:
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
//第一步:通過內(nèi)存平移16字節(jié)獲取當(dāng)前的mask_buckets
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//第二步:獲取buckets 通過p11 & 0x0000ffffffffffff 得到后48位 buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//第三步:獲取hash 搜索下標(biāo):邏輯右移48位 得到mask未蝌;然后p1 & mask給p12 得到hash存儲(chǔ)的key
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
///此處不會(huì)執(zhí)行
#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
//第四步:p12是獲取到的下標(biāo),然后邏輯左移4位茧妒,再由p10(buckets)平移萧吠,得到對(duì)應(yīng)的bucket保存到p12中
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
///第五步:1、將p12屬性imp 和 sel分別賦值為p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
///第五步:2桐筏、判斷當(dāng)前bucket的sel和傳入的sel是否相等
1: cmp p9, p1 // if (bucket->sel != _cmd)
///第五步:3纸型、如果不相同,則跳入2f
b.ne 2f // scan more
///第五步:4梅忌、如果相同狰腌,命中緩存,直接返回imp
CacheHit $0 // call or return imp
///第五步:5牧氮、 沒有找到 進(jìn)入2f
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
///第五步:6琼腔、如果p12(在第四步獲取到的bucket) == p10(在第二步獲取到的buckets),說明p12指針已經(jīng)到了buckets的首地址了踱葛。
cmp p12, p10 // wrap if bucket == buckets
///第五步:7丹莲、如果相等 跳入3f
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
///第五步:8、再將p12的指針指到buckets的最后一個(gè)元素
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.
///第五步:9尸诽、然后在繼續(xù)查找甥材,直到找到或者再次 bucket 與 buckets再次相等,跳出循環(huán)性含。
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
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
上述流程大概分為5個(gè)步驟洲赵。接下來我們具體分析下:
-
第一步:獲取
mask_buckets
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
......
}
前面我們已經(jīng)分析過objc_class
,知道其內(nèi)部結(jié)構(gòu),所以我們?cè)谀玫疆?dāng)前類的首地址后板鬓,因?yàn)?code>isa和superclass
各占8個(gè)字節(jié)悲敷,所以我們?cè)谀玫疆?dāng)前類的首地址后究恤,我們平移16個(gè)字節(jié)俭令,即可獲取到cache
的地址。
-
第二步:獲取
buckets
同樣的部宿,我們知道在arm64
也就是真機(jī)中抄腔,cache
的首地址是_maskAndBuckets
,我們查看_maskAndBuckets
的源碼:
{
uintptr_t buckets = (uintptr_t)newBuckets;
uintptr_t mask = (uintptr_t)newMask;
ASSERT(buckets <= bucketsMask);
ASSERT(mask <= maxMask);
//maskShift 是 48
//將mask左移48位只留下16位理张,剩余的補(bǔ)0赫蛇,
_maskAndBuckets.store(((uintptr_t)newMask << maskShift) | (uintptr_t)newBuckets, std::memory_order_relaxed);
_occupied = 0;
}
通過源碼我們可以發(fā)現(xiàn),mask
有左右48位雾叭,所以·高16位 | 低48位 = mask | buckets
因此悟耘,我們將p11 & 0x0000ffffffffffff
獲取到低48位,即buckets
织狐。
-
第三步:獲取
hash
搜索下標(biāo)
在前面cache_t
我們有分析到暂幼,方法存儲(chǔ)到cache
中,是使用hash
算法存儲(chǔ)移迫,其中開始下標(biāo)則是 sel & mask
旺嬉。
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
所以我們要拿到下標(biāo),就需要分別拿到mask
和sel
:
mask
:上面有看到在_maskAndBuckets
中mask
左移48位厨埋,所以我們要取到mask
邪媳,只需要_maskAndBuckets
右移48位即可sel
:object_msgSend
中傳入的兩個(gè)參數(shù),第一個(gè)是消息接受者荡陷,即isa
雨效,也就是P0
。第二個(gè)就是sel
废赞,即P1
-
第四步:根據(jù)下標(biāo)找到對(duì)應(yīng)的
bucket
#if __arm64__
#if __LP64__
// true arm64
#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
...
搜索源碼找到PTRSHIFT
设易,發(fā)現(xiàn)它是一個(gè)宏定義,值是3蛹头。而我們知道顿肺,buckets
是一個(gè)數(shù)組,如果想得到數(shù)組中的元素 我們可以根據(jù)首地址進(jìn)行指針平移獲取到對(duì)應(yīng)下標(biāo)的值渣蜗。
將第三步獲取的P12
開始下標(biāo) 邏輯左移4位 或者 可以理解為 bucket
是有sel
和imp
兩個(gè)屬性組成屠尊,每個(gè)屬性都是8個(gè)字節(jié)的大小,所以bucket
的大小是16耕拷。
將buckets
指針平移上一步得到的值讼昆,然后將平移后的bucket
存到p12
中。
-
第五步:根據(jù)
bucket
中的sel
查找- 1骚烧、將
bucket
中的屬性屬性imp
和sel
分別賦值為p17
和p9
- 2浸赫、判斷當(dāng)前
bucket
的sel
和傳入的sel是否相等:如果相等返回對(duì)應(yīng)imp=>p17
;不相等進(jìn)入2f闰围。 - 3、此時(shí)是不相等既峡,
2f
部分羡榴,這是一個(gè)循環(huán)。由于匯編中的查找是向上查找运敢,所以p12-1
獲取到上一個(gè)bucket
指針校仑。如果當(dāng)前p12 bucket
與buckets
的首地址(第一個(gè)元素)相等,那么就直接跳入3f部分传惠。 - 4迄沫、此時(shí)是
p12 bucket
與buckets
的首地址(第一個(gè)元素)相等,3f部分卦方。 - 5羊瘩、
mask
是buckets
數(shù)組的個(gè)數(shù)減一,將mask
左移4位盼砍, - 6尘吗、將
buckets
首地址地址平移上一步的結(jié)果,就到了buckets
的最后一位衬廷,再將buckets
最后一位的指針地址賦值給p12
摇予, - 7、然后在繼續(xù)進(jìn)行比較
sel
吗跋,如果有相等就返回相應(yīng)的imp
侧戴,如果沒有相等則就繼續(xù)向上查詢。 - 8跌宛、 如果
p12
又一次指到的首地址酗宋,那么說明整個(gè)buckets中
不存在方法sel
,則退出循環(huán)疆拘,并返回
具體流程可以參考下圖:
- 1骚烧、將
覺得不錯(cuò)記得點(diǎn)贊哦蜕猫!聽說看完點(diǎn)贊的人逢考必過,逢獎(jiǎng)必中哎迄。?( ′???` )比心