探索底層原理栅屏,積累從點(diǎn)滴做起。大家好,我是Mars手蝎。
往期回顧
iOS底層原理探索—OC對(duì)象的本質(zhì)
iOS底層原理探索—class的本質(zhì)
iOS底層原理探索—KVO的本質(zhì)
iOS底層原理探索— KVC的本質(zhì)
iOS底層原理探索— Category的本質(zhì)(一)
iOS底層原理探索— Category的本質(zhì)(二)
iOS底層原理探索— 關(guān)聯(lián)對(duì)象的本質(zhì)
iOS底層原理探索— block的本質(zhì)(一)
iOS底層原理探索— block的本質(zhì)(二)
iOS底層原理探索— Runtime之isa的本質(zhì)
iOS底層原理探索— Runtime之class的本質(zhì)
今天繼續(xù)帶領(lǐng)大家探索iOS之Runtime
的本質(zhì)显熏。
前言
OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語(yǔ)言雄嚣,它的動(dòng)態(tài)性是基于Runtime
的API
。Runtime
在我們的實(shí)際開發(fā)中占據(jù)著重要的地位喘蟆,在面試過程中也經(jīng)常遇到Runtime
相關(guān)的面試題缓升,我們?cè)谥皫灼诘奶剿鞣治鰰r(shí)也經(jīng)常會(huì)到Runtime
的底層源碼中查看相關(guān)實(shí)現(xiàn)。Runtime
對(duì)于iOS
開發(fā)者的重要性不言而喻蕴轨,想要學(xué)習(xí)和掌握Runtime
的相關(guān)技術(shù)港谊,就要從Runtime
底層的一些常用數(shù)據(jù)結(jié)構(gòu)入手。掌握了它的底層結(jié)構(gòu)橙弱,我們學(xué)習(xí)起來(lái)也能達(dá)到事半功倍的效果歧寺。今天研究OC
的消息機(jī)制
。
消息機(jī)制
OC
語(yǔ)言中方法調(diào)用通過消息機(jī)制
來(lái)實(shí)現(xiàn)棘脐,方法調(diào)用其實(shí)都是轉(zhuǎn)換為 objc_msgSend
函數(shù)調(diào)用斜筐。
OC
的消息機(jī)制
可以分為一下三個(gè)階段:
1、消息發(fā)送階段:從類及父類的方法緩存列表及方法列表查找方法蛀缝;
2顷链、動(dòng)態(tài)解析階段:如果消息發(fā)送階段沒有找到方法,則會(huì)進(jìn)入動(dòng)態(tài)解析階段内斯,負(fù)責(zé)動(dòng)態(tài)的添加方法實(shí)現(xiàn)蕴潦;
3像啼、消息轉(zhuǎn)發(fā)階段:如果也沒有實(shí)現(xiàn)動(dòng)態(tài)解析方法,則會(huì)進(jìn)行消息轉(zhuǎn)發(fā)階段潭苞,將消息轉(zhuǎn)發(fā)給可以處理消息的接受者來(lái)處理忽冻;
如果消息轉(zhuǎn)發(fā)也沒有實(shí)現(xiàn),就會(huì)報(bào)出經(jīng)典的錯(cuò)誤:unrecognzied selector sent to instance
此疹,方法找不到的錯(cuò)誤僧诚,無(wú)法識(shí)別消息。
接下來(lái)我們通過源碼分析消息機(jī)制
的三個(gè)階段分別是如何實(shí)現(xiàn)的蝗碎。
1湖笨、消息發(fā)送
在項(xiàng)目中方法調(diào)用的頻率很高,所以為了能夠提升效率蹦骑,在底層代碼中objc_msgSend
函數(shù)的實(shí)現(xiàn)是通過匯編語(yǔ)言編寫的慈省,我們?cè)谠创a中找到objc-msg-arm64.s
匯編文件,來(lái)具體分析一下objc_msgSend
函數(shù)的實(shí)現(xiàn)眠菇。
objc_msgSend
函數(shù)中首先判斷消息接收者receiver
是否為空边败。如果傳入的消息接受者為nil
則會(huì)執(zhí)行LNilOrTagged
,LNilOrTagged
內(nèi)部會(huì)執(zhí)行LReturnZero
捎废,而LReturnZero
內(nèi)部則直接return 0
笑窜。
如果傳入的消息接收者receiver
不為空則通過消息接收者receiver
的isa
指針找到消息接收者的class
,執(zhí)行CacheLookup
從方法緩存中取查找登疗。如果在方法緩存列表找到則執(zhí)行CacheHit
排截,調(diào)用方法或者返回函數(shù)地址;如果找到就執(zhí)行CheckMiss
辐益。CheckMiss
內(nèi)調(diào)用__objc_msgSend_uncached
断傲,方法沒有被緩存。
__objc_msgSend_uncached
內(nèi)會(huì)執(zhí)行MethodTableLookup
荷腊,去方法列表中查找艳悔。MethodTableLookup
內(nèi)部的核心代碼__class_lookupMethodAndLoadCache3
也就是c
語(yǔ)言函數(shù)_class_lookupMethodAndLoadCache3
(雙下劃線開頭變成單下劃線)。
以上分析我們用簡(jiǎn)單的流程圖來(lái)總結(jié):
接下來(lái)我們進(jìn)入
_class_lookupMethodAndLoadCache3
函數(shù)女仰,分析是如何從方法列表中查找方法猜年。
_class_lookupMethodAndLoadCache3函數(shù)
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
函數(shù)內(nèi)部調(diào)用lookUpImpOrForward
方法,傳入三個(gè)BOOL
類型的參數(shù)疾忍。
lookUpImpOrForward 函數(shù)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
//接收傳入的參數(shù)乔外, initialize = YES , cache = NO , resolver = YES
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 緩存查找, 因?yàn)閏ache傳入的為NO, 這里不會(huì)進(jìn)行緩存查找, 因?yàn)樵趨R編語(yǔ)言中CacheLookup已經(jīng)查找過
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
// 防止動(dòng)態(tài)添加方法败匹,緩存會(huì)變化巢株,再次查找緩存。
imp = cache_getImp(cls, sel);
// 如果找到imp方法地址, 直接調(diào)用done, 返回方法地址
if (imp) goto done;
// 查找方法列表, 傳入類對(duì)象和方法名
{
// 根據(jù)sel去類對(duì)象里面查找方法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果方法存在恢口,則緩存方法,
// 內(nèi)部調(diào)用的就是 cache_fill差购。
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
// 方法緩存之后, 取出函數(shù)地址imp并返回
imp = meth->imp;
goto done;
}
}
// 如果類方法列表中沒有找到, 則去父類的緩存中或方法列表中查找方法
{
unsigned attempts = unreasonableClassCount();
// 如果父類緩存列表及方法列表均找不到方法四瘫,則去父類的父類去查找。
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 查找父類的緩存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 在父類中找到方法, 在本類中緩存方法, 注意這里傳入的是cls, 將方法緩存在本類緩存列表中, 而非父類中
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 執(zhí)行done, 返回imp
goto done;
}
else {
// 跳出循環(huán), 停止搜索
break;
}
}
// 查找父類的方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 同樣拿到方法, 在本類進(jìn)行緩存
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
// 執(zhí)行done, 返回imp
goto done;
}
}
}
// ---------------- 消息發(fā)送階段完成,沒有找到方法實(shí)現(xiàn)欲逃,進(jìn)入動(dòng)態(tài)解析階段 ---------------------
//首先檢查是否已經(jīng)被標(biāo)記為動(dòng)態(tài)方法解析找蜜,如果沒有才會(huì)進(jìn)入動(dòng)態(tài)方法解析
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
//將triedResolver標(biāo)記為YES,下次就不會(huì)再進(jìn)入動(dòng)態(tài)方法解析
triedResolver = YES;
goto retry;
}
// ---------------- 動(dòng)態(tài)解析階段完成,進(jìn)入消息轉(zhuǎn)發(fā)階段 ---------------------
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
// 返回方法地址
return imp;
}
getMethodNoSuper_nolock 函數(shù)
方法列表中查找方法
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// cls->data() 得到的是 class_rw_t
// class_rw_t->methods 得到的是methods二維數(shù)組
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
// mlists 為 method_list_t
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
getMethodNoSuper_nolock
函數(shù)中通過遍歷方法列表拿到method_list_t
最終通過search_method_list
函數(shù)查找方法
search_method_list
函數(shù)
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
// 如果方法列表已經(jīng)排序好了稳析,則通過二分查找法查找方法洗做,以節(jié)省時(shí)間
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// 如果方法列表沒有排序好就遍歷查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
findMethodInSortedMethodList
函數(shù)內(nèi)二分查找實(shí)現(xiàn)原理
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// >>1 表示將變量n的各個(gè)二進(jìn)制位順序右移1位,最高位補(bǔ)二進(jìn)制0彰居。
// count >>= 1 如果count為偶數(shù)則值變?yōu)?count / 2)诚纸。如果count為奇數(shù)則值變?yōu)?count-1) / 2
for (count = list->count; count != 0; count >>= 1) {
// probe 指向數(shù)組中間的值
probe = base + (count >> 1);
// 取出中間method_t的name,也就是SEL
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// 取出 probe
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 返回方法
return (method_t *)probe;
}
// 如果keyValue > probeValue 則折半向后查詢
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
通過以上分析陈惰,我們了解了消息機(jī)制
中的第一階段消息發(fā)送階段畦徘,下面我們用一張圖來(lái)總結(jié)一下整體流程:
2、動(dòng)態(tài)方法解析
當(dāng)在類和父類的方法緩存列表抬闯、方法列表中都找不到方法時(shí)旧烧,就會(huì)進(jìn)入動(dòng)態(tài)方法解析階段。我們?cè)谙l(fā)送階段源碼中看到画髓,進(jìn)入動(dòng)態(tài)方法解析階段是通過函數(shù)_class_resolveMethod
。
_class_resolveMethod函數(shù)
函數(shù)內(nèi)部會(huì)根據(jù)是元類還是類平委,并且類方法和對(duì)象方法的動(dòng)態(tài)方法解析是調(diào)用不同的函數(shù):
動(dòng)態(tài)解析對(duì)象方法時(shí)奈虾,會(huì)調(diào)用
+(BOOL)resolveInstanceMethod:(SEL)sel
方法。動(dòng)態(tài)解析類方法時(shí)廉赔,會(huì)調(diào)用
+(BOOL)resolveClassMethod:(SEL)sel
方法肉微。
動(dòng)態(tài)解析方法之后,會(huì)將triedResolver = YES;
那么下次就不會(huì)在進(jìn)行動(dòng)態(tài)解析階段了蜡塌,之后會(huì)回到消息發(fā)送階段碉纳,重新執(zhí)行retry
,重新對(duì)方法查找一遍馏艾。
我們可以利用動(dòng)態(tài)方法解析來(lái)動(dòng)態(tài)的添加方法劳曹。我們將
MPerson
類中的test
方法實(shí)現(xiàn)注釋掉,用other
方法的實(shí)現(xiàn)來(lái)替代test
方法實(shí)現(xiàn):從圖中的可以看到琅摩,我們注釋掉
test
方法實(shí)現(xiàn)后系統(tǒng)已經(jīng)報(bào)出了警告铁孵,下面我們測(cè)試一下代碼:當(dāng)調(diào)用
MPerson
的test
方法時(shí),打印了[MPerson other]
房资。動(dòng)態(tài)添加方法成功蜕劝。
這里需要注意class_addMethod
函數(shù)用來(lái)向具有給定名稱和實(shí)現(xiàn)的類添加新方法,class_addMethod
將添加一個(gè)方法實(shí)現(xiàn)的覆蓋,但是不會(huì)替換已有的實(shí)現(xiàn)岖沛。也就是說如果上述代碼中已經(jīng)實(shí)現(xiàn)了-(void)test
方法暑始,則不會(huì)再動(dòng)態(tài)添加方法。
3婴削、消息轉(zhuǎn)發(fā)階段
如果上面兩個(gè)階段都失敗的話廊镜,就會(huì)來(lái)到第三階段:消息轉(zhuǎn)發(fā)階段。
由于OC
中消息機(jī)制并不是開源的馆蠕,這里就直接將消息轉(zhuǎn)發(fā)的原理告訴給大家了期升。
進(jìn)入消息轉(zhuǎn)發(fā)階段后,就會(huì)判斷是否指定了其它對(duì)象來(lái)執(zhí)行方法互躬。具體查看當(dāng)前類是否實(shí)現(xiàn)了forwardingTargetForSelector
函數(shù)播赁,如果返回值不為空,那么說明指定了轉(zhuǎn)發(fā)目標(biāo)吼渡,那么就會(huì)讓轉(zhuǎn)發(fā)目標(biāo)處理消息容为。
如果forwardingTargetForSelector
函數(shù)返回為nil
,沒有指定轉(zhuǎn)發(fā)目標(biāo)寺酪,就會(huì)調(diào)用methodSignatureForSelector
方法坎背,用來(lái)返回一個(gè)方法簽名,這也是跳轉(zhuǎn)方法的最后機(jī)會(huì)寄雀。
如果methodSignatureForSelector
方法返回正確的方法簽名就會(huì)調(diào)用forwardInvocation
方法得滤,forwardInvocation
方法內(nèi)提供一個(gè)NSInvocation
類型的參數(shù),NSInvocation
封裝了一個(gè)方法的調(diào)用盒犹,包括方法的調(diào)用者懂更,方法名,以及方法的參數(shù)急膀。在forwardInvocation
函數(shù)內(nèi)修改方法調(diào)用對(duì)象即可沮协。
如果methodSignatureForSelector
返回的為nil
,就會(huì)來(lái)到doseNotRecognizeSelector:
方法內(nèi)部卓嫂,程序crash
報(bào)出經(jīng)典的錯(cuò)誤unrecognized selector sent to instance
慷暂。
至此,OC
的消息機(jī)制
的分析就告一段落晨雳,OC
中的方法調(diào)用其實(shí)都是轉(zhuǎn)成了objc_msgSend
函數(shù)的調(diào)用行瑞,給方法調(diào)用者(receiver)發(fā)送一條消息(selector方法名)。方法調(diào)用過程包括三個(gè)階段:消息發(fā)送悍募、動(dòng)態(tài)方法解析蘑辑、消息轉(zhuǎn)發(fā)。
更多技術(shù)知識(shí)請(qǐng)關(guān)注公眾號(hào)
iOS進(jìn)階