Objective-C 中的消息與消息轉發

你們都知道OC是一門動態語言,其動態性由底層的runtime庫來支撐實現。OC全部的方法都是經過runtime來發送消息,當咱們探討消息發送,其實也就是在探討OC方法的調用過程。git

[receiver message];

這是咱們很熟悉的一個OC方法的調用,你們都知道這個方法最終會被編譯器轉換爲消息發送函數github

objc_msgSend(receiver, @selector(message));

首先聲明我們這篇文章不去講解具體的class的數據結構一類的細節問題,咱們主要關注的是這個過程。objective-c

很遺憾,objc_msgSend的實現是用匯編寫的,我並不能看懂。可是從runtime的源碼中我發現了一個關鍵的方法:緩存

//查找方法的實現
extern IMP lookUpImpOrForward(Class, SEL, id obj, bool initialize, bool cache, bool resolver);

咱們要執行一個方法,其實最重要的就是找到這個方法的實現。下面咱們來看一下這個lookUpImpOrForward函數的源碼。數據結構

lookUpImpOrForward

經過源碼中的註釋能夠看出來,主要的流程分爲如下幾個階段:併發

無鎖狀態下從緩存中查找方法的實現
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
    imp = cache_getImp(cls, sel);
    if (imp) return imp;
}
類的實現和初始化
// 判斷類是否已經實現
if (!cls->isRealized()) {
    realizeClass(cls);
}
    
// 是否初始化
if (initialize  &&  !cls->isInitialized()) {
    runtimeLock.unlock();
    _class_initialize (_class_getNonMetaClass(cls, inst));
    runtimeLock.lock();

在執行這兩段代碼以前還有兩行代碼,一個是runtimeLock.lock();,註釋裏面解釋是經過加鎖在遇到併發實現類的時候保護實現的過程。框架

另外一個是checkIsKnownClass(cls);。看名字是檢測這個類是否是咱們已知的類,什麼狀況下可能出現咱們未知的類呢?我猜想是經過NSClassFromString()來獲取類的時候,若是class的string寫錯了,就會生成一個未知的類,這時候確定找不到方法的實現,直接就crash掉行了。ide

咱們的Class在底層實際上是一個名爲objc_class的結構體,你們能夠去源碼中看一下,這裏咱們不細說。執行realizeClass方法其實等於對類的第一次初始化,包括配置類的讀寫空間(class_rw_t)而且返回類的正確的結構體,就至關於搭好了這個類的框架。_class_initialize則是讓類執行咱們熟悉的+initialize方法。函數

加鎖,查找
加鎖

下面就要開始真正的IMP查找了,查找以前有一個runtimeLock.assertLocked();加鎖。命令行

註釋中給出瞭解釋,runtimeLock在查找方法的時候加鎖,是爲了保持method-lookup(查找方法)和 cache-fill(緩存填充)這兩種方法的原子性,

// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.

這幾行註釋我沒讀明白啥意思,感受是說若是不加鎖,有添加category的狀況時,會致使緩存被沖洗掉。

在類中查找

經過代碼能夠看出,首先依然是從緩存中查找,而後在當前的類中查找,最後經過一個循環來從各級父類中查找。

這部分代碼咱們就不展現出來了,你們本身能夠看源碼,若是找到了這個方法的實現,咱們看到會調用log_and_fill_cache這樣一個方法,其實就是把此次查找緩存起來,方便下次使用。

若是沒有找到就進入到下一個環節。

動態決議

這個名稱你們應該是耳熟的,咱們都知道消息轉發機制,也就是說在咱們調用了一個未實現的方法時,並不會直接crash掉,而後報unrecognized selector sent to instance錯誤。咱們的系統會給咱們兩次機會,第一次就是動態決議。

if (resolver  &&  !triedResolver) {
    runtimeLock.unlock();
    _class_resolveMethod(cls, sel, inst);
    runtimeLock.lock();
    // Don't cache the result; we don't hold the lock so it may have 
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
}

在OC層面最直接的表現形式就是看咱們是否實現了+ (BOOL)resolveClassMethod:(SEL)sel或者+ (BOOL)resolveInstanceMethod:(SEL)sel方法(對應類方法和實例方法)。

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {//不是元類
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

根據cls是否爲元類來調用_class_resolveInstanceMethod_class_resolveClassMethod

/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    // 這個imp或者是一個正經的IMP指針,或者是一個彙編的入口
    //_objc_msgForward_impcache 是一個彙編的入口  (看名字應該是消息轉發相關的)
    // 本方法是不進行消息轉發的 ~  因此若是獲取到的IMP是這個入口,就直接return nil
    
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

經過lookUpImpOrNil的源碼咱們知道,其實它封裝了lookUpImpOrForward,不進行lookUpImpOrForward中最後的消息轉發這一步,並且在這個if中,參數resolver傳入了NO,也就是動態決議這一步也不進行,只進行了方法在本類和各級父類中的查找,若是找不到,則跟非元類同樣執行_class_resolveInstanceMethod

else中爲何要再執行這一段if代碼,我不是很理解。

具體的動態決議實現的代碼,咱們看其中一個就行來看,用_class_resolveInstanceMethod當例子吧

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        // 沒有找到SEL_resolveInstanceMethod(resolveInstanceMethod)方法
        return;
    }
    // 下面應該是類型轉換的代碼而已吧?
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 執行實現的+resolveInstanceMethod方法
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //經過+resolveInstanceMethod動態添加了方法,在進行一次查找
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

一開始先經過調用lookUpImpOrNil來查找是否已經實現了+resolveInstanceMethod,沒有實現就直接返回了。

若是實現了,經過objc_msgSend來執行實現的+resolveInstanceMethod方法。

咱們在+resolveInstanceMethod方法中都是動態的添加方法,因此在執行完以後在進行一次查找。

消息轉發

若是在動態決議以後,依然沒有找到方法,那咱們還有最後一次機會,那就是消息轉發。

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

很遺憾_objc_msgForward_impcache彙編實現看不懂。不過咱們仍是能夠從另外一個方向來研究消息轉發到底作了什麼。

先補充一個騷操做,這也是我這兩天剛學到的,就是打印runtime的代碼執行日誌,這麼說可能不太貼切,反正就是能看到咱們執行一個方法的過程當中調用了哪些方法。

想要開始打印的地方加上下面代碼
extern void instrumentObjcMessageSends(BOOL);
instrumentObjcMessageSends(YES);

想要關閉的地方加上下面代碼
extern void instrumentObjcMessageSends(BOOL);
instrumentObjcMessageSends(NO);

固然還有別的方式,你們能夠去查一下。

首先咱們新建一個工程,類型就選擇macOS->Common Line Tool。
由於我用iOS的工程無論用。

main函數中這樣寫

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Sark.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        extern void instrumentObjcMessageSends(BOOL);
        instrumentObjcMessageSends(YES);
        
        Sark * test = [[Sark alloc] init];
        [test performSelector:@selector(xxx)];
        
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

建立SEL選擇子的時候咱們故意穿進去一個xxx,就是爲了讓類找不到方法,而後走消息轉發的的這個過程。

執行一下代碼,運行時發送的全部消息都會打印到/private/tmp/msgSend-xxxx文件裏了。(這是電腦系統的路徑)

若是找起來不方即可以直接使用下面的命令行打開該文件。

open /private/tmp/

從該路徑的msgSend-xxx文件中咱們找到了這麼一部分代碼

+ Sark NSObject initialize
+ Sark NSObject alloc
- Sark NSObject init
- Sark NSObject performSelector:
+ Sark NSObject resolveInstanceMethod:
+ Sark NSObject resolveInstanceMethod:
- Sark NSObject forwardingTargetForSelector:
- Sark NSObject forwardingTargetForSelector:
- Sark NSObject methodSignatureForSelector:
- Sark NSObject methodSignatureForSelector:
- Sark NSObject class
- Sark NSObject doesNotRecognizeSelector:
- Sark NSObject doesNotRecognizeSelector:
- Sark NSObject class

咱們能夠看到在執行完了resolveInstanceMethod以後又執行forwardingTargetForSelector:methodSignatureForSelector:,最後才由於找不到方法執行doesNotRecognizeSelector:

如今咱們能夠大概瞭解在消息轉發的過程當中執行了哪些方法了,通過查閱資料咱們得出:

所有的消息轉發過程作了以下幾件事:(包括動態決議+消息轉發)

  1. 調用resolveInstanceMethod:方法,容許用戶在此時爲該Class動態添加實現。若是有實現了,則調用並返回。若是仍沒實現,繼續下面的動做。
  2. 調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。若是獲取到,則直接轉發給它。若是返回了nil,繼續下面的動做。
  3. 調用methodSignatureForSelector:方法,嘗試得到一個方法簽名。若是獲取不到,則直接調用doesNotRecognizeSelector拋出異常。
  4. 調用forwardInvocation:方法,將地3步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這裏面了。

上面這4個方法均是模板方法,開發者能夠override,由runtime來調用。

具體如何重寫使用這幾個方法,你們能夠本身查一下。


參考資料

https://blog.ibireme.com/2013...

https://github.com/draveness/...

相關文章
相關標籤/搜索