runtime
我們都知道大部分語言是編譯時決議的,而Object-C
是在運(yùn)行時決議方淤,這來源于強(qiáng)大的runtime
引几。通過runtime
可以動態(tài)對類各方面進(jìn)行配置,還有就是消息傳遞爸舒。消息傳遞其實就是通過objc_msgSend
按照sel
找到函數(shù)imp
的過程。
objc_msgSend
新建一個工程稿蹲,在main.m
文件夾內(nèi)創(chuàng)建一個LGPerson
類。在main
函數(shù)內(nèi)部調(diào)用[p study]
鹊奖,[p happy]
苛聘。
@interface LGPerson : NSObject
- (void)study;
- (void)happy;
+ (void)eat;
@end
@implementation LGPerson
- (void)study {
NSLog(@"%s",__func__);
}
- (void)happy {
NSLog(@"%s",__func__);
}
+ (void)eat {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
[p study];
[p happy];
}
return NSApplicationMain(argc, argv);
}
通過clang探索objc_msgSend
使用clang
編譯,打開該工程目錄輸入clang -rewrite-objc main.m
忠聚,然后打開該工程目下main.cpp
文件设哗,我們可以看到[p study]
,[p happy]
被編譯成了objc_msgSend()函數(shù)
两蟀。第一個參數(shù)是接受者网梢,第二個參數(shù)是方法名,系統(tǒng)會根據(jù)這2個參數(shù)找到方法具體實現(xiàn)赂毯。
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("study"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));
我們把 [p study]
直接改為編譯后的方法((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"study"))
,直接運(yùn)行驗證战虏,我們可以看到study
方法是可以執(zhí)行拣宰。
我們在study
方法加個參數(shù)str
,看看clang
編譯文件烦感,打開該工程目錄輸入clang -rewrite-objc main.m
巡社,然后打開該工程目下main.cpp
文件。
我們可以看到如果有參數(shù)的話手趣,系統(tǒng)編譯會自動加上相應(yīng)的參數(shù)晌该。
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_mx_2ljwkcpn0kg_4m1bgs1507f00000gn_T_main_5b862f_mi_3);
objc_msgSend的種類
我們查看main.cpp
文件的頂部。我們發(fā)現(xiàn)有5種objc_msgSend
绿渣。
-
objc_msgSend
: 發(fā)消息給本類 -
objc_msgSendSuper
: 發(fā)消息給父類 -
objc_msgSend_stret
: 發(fā)消息返回值是一個結(jié)構(gòu)體 -
objc_msgSendSuper_stret
: 發(fā)消息給父類朝群,返回值是一個結(jié)構(gòu)體 -
objc_msgSend_fpret
: 發(fā)消息返回值是一個浮點(diǎn)型的
objc_msgSend的種類
objc_msgSendSuper
首先我們來個經(jīng)典的面試題,新建一個LGPerson
中符,還是申明一個study
方法姜胖,再新建一個LGTeacher
繼承自LGPerson
,在LGTeacher
的init方法里面打印[self class]
和[super class]
舟茶。
@interface LGPerson : NSObject
-(void)study;
@end
@interface LGTeacher : LGPerson
@end
@implementation LGTeacher
-(instancetype)init {
if (self = [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
}
return self;
}
@end
初始化一個LGTeacher
的實例對象谭期,我們通常會認(rèn)為第一個打印是LGTeacher
,第二的打印位LGPerson
吧凉∷沓觯看看是不是啊,運(yùn)行阀捅。
我們可以看到運(yùn)行結(jié)果2個打印都是
LGTeacher
胀瞪。這是為什么呢?使用clang
編譯饲鄙,打開該工程目錄輸入clang -rewrite-objc LGTeacher.m
凄诞,打開LGTeacher.cpp
文件,找到init
函數(shù),然后我們可以看到了[super class]
其實就是調(diào)用了objc_msgSendSuper
函數(shù)忍级。那我看蘋果官方文檔是怎么解釋
objc_msgSendSuper
這個函數(shù)的帆谍,首先打開Xcode
菜單欄的help
然后點(diǎn)擊Developer Documentation
,選擇Objective-C
轴咱,然后點(diǎn)擊搜索框搜索objc_msgSendSuper
汛蝙。查看objc_msgSendSuper官方文檔。
Parameters
super
A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.
op
A pointer of type SEL. Pass the selector of the method that will handle the message.
...
A variable argument list containing the arguments to the method.Return Value
The return value of the method identified by op.Discussion
When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.參數(shù)
父類
指向 objc_super 數(shù)據(jù)結(jié)構(gòu)的指針朴肺。 傳遞標(biāo)識消息發(fā)送到的上下文的值窖剑,包括要接收消息的類的實例和開始搜索方法實現(xiàn)的超類。
操作
SEL 類型的指針戈稿。 傳遞將處理消息的方法的選擇器西土。
...
包含方法參數(shù)的變量參數(shù)列表。返回值
op 標(biāo)識的方法的返回值鞍盗。-
討論
當(dāng)遇到方法調(diào)用時需了,編譯器生成對函數(shù) objc_msgSend跳昼、objc_msgSend_stret、objc_msgSendSuper 或 objc_msgSendSuper_stret 之一的調(diào)用援所。 發(fā)送到對象超類的消息(使用 super 關(guān)鍵字)使用 objc_msgSendSuper 發(fā)送庐舟; 其他消息使用 objc_msgSend 發(fā)送。 將數(shù)據(jù)結(jié)構(gòu)作為返回值的方法使用 objc_msgSendSuper_stret 和 objc_msgSend_stret 發(fā)送住拭。
我們查看蘋果官方文件可以查看到(使用 super 關(guān)鍵字)使用 objc_msgSendSuper 發(fā)送
挪略,所以說我們前文[super class]
是實際是調(diào)用objc_msgSendSuper
函數(shù)的,接受者是類的實例滔岳。
我們可以看到main.app
內(nèi)部[super class]
,其實接受者還是self
一個LGTeacher
的實例對象杠娱,所以說消息的接受者還是LGTeacher
的實例對象,所以[super class]
輸出為LGTeacher
谱煤。
main.app
重寫objc_msgSendSuper
我們在子類LGTeacher
重寫父類LGPerson
的study
方法摊求,然后不進(jìn)行調(diào)用,然后我們重寫objc_msgSendSuper
刘离。查看objc_super
結(jié)構(gòu)體我們可以發(fā)現(xiàn)它需要傳一個receiver
(接受者)和super_class
(父類)室叉。receiver
還是LGTeacher
,super_class
是LGPerson.class
硫惕。
struct objc_super lg_objc_super;
lg_objc_super.receiver = self;
lg_objc_super.super_class = LGPerson.class;
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&lg_objc_super,@selector(study));
運(yùn)行我們可以發(fā)現(xiàn)是可以調(diào)用LGPerson的study方法茧痕。
那我們把
super_class
改為NSObject.class
試試;
lg_objc_super.super_class = NSObject.class;
運(yùn)行,我們可以看到是找不到這個study
方法的恼除,因為使用objc_msgSendSuper
時候它會直接從它的super_class
直接找相應(yīng)的方法實現(xiàn)踪旷,我們設(shè)置的是NSObject.class
,NSObject
沒有實現(xiàn)study
方法豁辉,所以就直接就找不到study
方法的實現(xiàn)了令野。
方法的快速查找
我們是結(jié)合objc4-838進(jìn)行探索的,我們?nèi)炙阉?code>objc_msgSend徽级,可以看到有很多气破,我們找到真機(jī)arm64
架構(gòu)下的,objc_msgSend
是由匯編寫的餐抢,為什么會采用匯編呢堵幽,是因為匯編效率性能高,可以節(jié)約不少的時間弹澎。
我們新建個工程,連上真機(jī)努咐,在
[t study]
處打上斷點(diǎn)苦蒿,然后再加上objc_msgSend
符號斷點(diǎn)。運(yùn)行至斷點(diǎn)處打開objc_msgSend
符號斷點(diǎn)渗稍。我們就可以看到匯編了佩迟。- objc_msgSend匯編解析
//進(jìn)入objc_msgSend流程
ENTRY _objc_msgSend
//流程開始团滥,無需frame
UNWIND _objc_msgSend, NoFrame
//判斷p0(消息接受者)是否存在,不存在則重新開始執(zhí)行objc_msgSend
cmp p0, #0 // nil check and tagged pointer check
//如果支持小對象類型报强。返回小對象或空
#if SUPPORT_TAGGED_POINTERS
//b是進(jìn)行跳轉(zhuǎn)灸姊,b.le是小于判斷,也就是小于的時候LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//等于秉溉,如果不支持小對象力惯,就LReturnZero
b.eq LReturnZero
#endif
//通過p13取isa
ldr p13, [x0] // p13 = isa
//通過isa取class并保存到p16寄存器中
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//LGetIsaDone是一個入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//進(jìn)入到緩存查找或者沒有緩存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空處理,直接退出
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
我們可以看到是和objc4-838中objc_msgSend
函數(shù)匯編是一樣的召嘶。
- 首先進(jìn)入
objc_msgSend
流程父晶,cmp p0
,判斷p0
是否存在,我們讀取寄存器看看,可以看到x0
是一個LGTeacher
的實例對象,x1
是study
方法弄跌。
讀取寄存器x0 x1
2.ldr p13, [x0]
通過p13
取isa
指針地址甲喝,我們可以打印x13
地址為0x000021a1047b96e1
,然后我們通過x0
打印對象的isa
铛只,可以看到x13
就是對象的isa
指針埠胖。
isa
3.GetClassFromIsa_p16 p13, 1, x0
:通過isa&掩碼(0xffffffff8)
獲取class
并保存到p16寄存器中,我們打印x16
可以看到x16
是類對象地址淳玩。
image.png
4.CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
開始找方法直撤,取出x16
的class
移到x15
,通過x16
找cache
凯肋。
5.ldr x11, [x16, #0x10]
:x16
通過內(nèi)存平移可以得到x11
,x11
就是之前cache詳解內(nèi)的第一個8字節(jié)成員_bucketsAndMaybeMask
谊惭。
6.
and x10, x11, #0xfffffffffffe
:x11&0xfffffffffffe
就得到buckets的
首地址x10
。然后在cache
進(jìn)行方法查找侮东。如果找到了會進(jìn)行CacheHit(緩存命中)
圈盔,找不到的話會調(diào)用_objc_msgSend_uncached
函數(shù)。總結(jié):
當(dāng)調(diào)用objc_msgSend(receiver, sel)
時:
- 看
receiver
是否存在 - 通過
receiver
的isa
指針獲取類對象 - 通過類對象內(nèi)存平移獲取
cache
- 通過
cache
找到buckets
- 根據(jù)
buckets
找相應(yīng)的sel
- 如果有相應(yīng)的
sel
走CacheHit
函數(shù) - 如果沒有相應(yīng)的
sel
會調(diào)用_objc_msgSend_uncached
函數(shù)
方法的慢速查找
方法在cache
內(nèi)找不到就是調(diào)用_objc_msgSend_uncached
函數(shù)悄雅,我們在_objc_msgSend_uncached
函數(shù)內(nèi)部可以看到它會調(diào)用MethodTableLookup
函數(shù)驱敲,MethodTableLookup
函數(shù)會調(diào)用_lookUpImpOrForward
。_lookUpImpOrForward
函數(shù)在匯編內(nèi)是看不到宽闲,那我們直接在源碼內(nèi)搜索lookUpImpOrForward
众眨。
我們可以看到
lookUpImpOrForward
函數(shù)內(nèi)部首先會判斷cache
內(nèi)部有沒有方法,因為在多線程環(huán)境下現(xiàn)在cache
可能有該方法容诬。如果本類沒有的話會調(diào)用
getMethodNoSuper_nolock
函數(shù)娩梨,然后依次調(diào)用search_method_list_inline
、findMethodInSortedMethodList
函數(shù)览徒。我們可以看到findMethodInSortedMethodList
是采用二分查找來獲取方法的狈定。當(dāng)我們在本類里面找到方法時,它會進(jìn)行
goto done
,然后會進(jìn)入log_and_fill_cache
函數(shù)纽什,可以看到log_and_fill_cache
函數(shù)內(nèi)部調(diào)用了cache.insert()
方法措嵌,也就是加入緩存里面了。需要注意的是哪個類調(diào)用就會加入哪個類的cache里面芦缰,也就是如果子類調(diào)用父類的方法企巢,之后該方法會緩存到子類的cache
。如果本類也沒有方法让蕾,通過
curClass = curClass->getSuperclass()
浪规,把curClass
轉(zhuǎn)換成父類,然后找父類的cache
涕俗,cache
如果沒有在通過二分查找找方法列表罗丰。如果再沒有把curClass
轉(zhuǎn)換成父類的父類依次查找。總結(jié):
消息的慢速查找流程:
- 調(diào)用
lookUpImpOrForward
- 看本類的
cache
里面有沒有該方法 - 如果沒有看本類的
methodList
有沒有該方法(二分查找) - 如果沒有看父類的
cache
里面有沒有該方法 - 如果沒有看父類的
methodList
有沒有該方法(二分查找) - 然后逐級向上查找
- 當(dāng)父類是
nil
的時候也就是查找到NSObject
的時候再姑,都沒有的話就會進(jìn)入消息轉(zhuǎn)發(fā)流程萌抵。