探索底層原理,積累從點(diǎn)滴做起。大家好,我是Mars。
往期回顧
iOS底層原理探索—OC對象的本質(zhì)
iOS底層原理探索—class的本質(zhì)
iOS底層原理探索—KVO的本質(zhì)
iOS底層原理探索— KVC的本質(zhì)
iOS底層原理探索— Category的本質(zhì)(一)
iOS底層原理探索— Category的本質(zhì)(二)
iOS底層原理探索— 關(guān)聯(lián)對象的本質(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)的編程語言,它的動(dòng)態(tài)性是基于Runtime的API。Runtime在我們的實(shí)際開發(fā)中占據(jù)著重要的地位,在面試過程中也經(jīng)常遇到Runtime相關(guān)的面試題,我們在之前幾期的探索分析時(shí)也經(jīng)常會(huì)到Runtime的底層源碼中查看相關(guān)實(shí)現(xiàn)。Runtime對于iOS開發(fā)者的重要性不言而喻,想要學(xué)習(xí)和掌握Runtime的相關(guān)技術(shù),就要從Runtime底層的一些常用數(shù)據(jù)結(jié)構(gòu)入手。掌握了它的底層結(jié)構(gòu),我們學(xué)習(xí)起來也能達(dá)到事半功倍的效果。今天研究OC的消息機(jī)制。
消息機(jī)制
OC語言中方法調(diào)用通過消息機(jī)制來實(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ā)給可以處理消息的接受者來處理;
如果消息轉(zhuǎn)發(fā)也沒有實(shí)現(xiàn),就會(huì)報(bào)出經(jīng)典的錯(cuò)誤:unrecognzied selector sent to instance,方法找不到的錯(cuò)誤,無法識(shí)別消息。
接下來我們通過源碼分析消息機(jī)制的三個(gè)階段分別是如何實(shí)現(xiàn)的。
1、消息發(fā)送
在項(xiàng)目中方法調(diào)用的頻率很高,所以為了能夠提升效率,在底層代碼中objc_msgSend函數(shù)的實(shí)現(xiàn)是通過匯編語言編寫的,我們在源碼中找到objc-msg-arm64.s匯編文件,來具體分析一下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語言函數(shù)_class_lookupMethodAndLoadCache3(雙下劃線開頭變成單下劃線)。
以上分析我們用簡單的流程圖來總結(jié):

接下來我們進(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編語言中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;
// 查找方法列表, 傳入類對象和方法名
{
// 根據(jù)sel去類對象里面查找方法
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ā)送階段,下面我們用一張圖來總結(jié)一下整體流程:

2、動(dòng)態(tài)方法解析
當(dāng)在類和父類的方法緩存列表、方法列表中都找不到方法時(shí),就會(huì)進(jìn)入動(dòng)態(tài)方法解析階段。我們在消息發(fā)送階段源碼中看到,進(jìn)入動(dòng)態(tài)方法解析階段是通過函數(shù)_class_resolveMethod。
_class_resolveMethod函數(shù)

函數(shù)內(nèi)部會(huì)根據(jù)是元類還是類,并且類方法和對象方法的動(dòng)態(tài)方法解析是調(diào)用不同的函數(shù):
動(dòng)態(tài)解析對象方法時(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,重新對方法查找一遍。

我們可以利用動(dòng)態(tài)方法解析來動(dòng)態(tài)的添加方法。我們將
MPerson類中的test方法實(shí)現(xiàn)注釋掉,用other方法的實(shí)現(xiàn)來替代test方法實(shí)現(xiàn):
從圖中的可以看到,我們注釋掉
test方法實(shí)現(xiàn)后系統(tǒng)已經(jīng)報(bào)出了警告,下面我們測試一下代碼:
當(dāng)調(diào)用
MPerson的test方法時(shí),打印了[MPerson other]。動(dòng)態(tài)添加方法成功。
這里需要注意class_addMethod函數(shù)用來向具有給定名稱和實(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ì)來到第三階段:消息轉(zhuǎn)發(fā)階段。
由于OC中消息機(jī)制并不是開源的,這里就直接將消息轉(zhuǎn)發(fā)的原理告訴給大家了。
進(jìn)入消息轉(zhuǎn)發(fā)階段后,就會(huì)判斷是否指定了其它對象來執(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方法,用來返回一個(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)用對象即可。
如果methodSignatureForSelector返回的為nil,就會(huì)來到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í)請關(guān)注公眾號(hào)
iOS進(jìn)階
iOS進(jìn)階.jpg
