iOS 底層 - 一文讀懂OC方法查找與消息轉發

前言

方法查找 , 動態方法解析以及消息轉發已是面試常客了 , 也是咱們瞭解 Aspects 或者餓了麼的 AOP - Stinger 等等優秀的三方庫必不可少的基礎知識 .html

上篇文章 手把手帶你探索OC方法的本質 咱們講到 objc_msgSend 函數彙編查找方法緩存的流程 , 直到緩存沒有命中 , 由 JumpMiss 調用了 bl __class_lookupMethodAndLoadCache3 , 由此找到了 lookUpImpOrForward 回到了 c 函數中 , 開啓方法查找與消息轉發的流程 .git

  • 建議 :

開啓本篇文章探索前 , 請對 OC類對象/實例對象/元類 各自存儲着什麼內容 , 以及 isa 的走位有詳細瞭解 .github

一、方法查找流程

繼續 上篇文章 , 咱們 objc_msgSend 彙編查找緩存 , 緩存 miss 時來到 _class_lookupMethodAndLoadCache3 這個函數 ,面試

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    /* cls:若是是實例方法那就是類,若是是類方法那就是元類 sel:方法名 obj:方法調用者 */
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼

首先須要明白的是 :緩存

  • 當調用實例對象方法時 , 查找的將是類對象 .
  • 當調用類方法是 , 查找的將是元類對象 .
  • 注意 : 本文下面書內容中所指 "本類" 基於此前提 . 就是說當調用實例方法 , 本類就是指類對象 , 當調用類方法 , 本類就是指元類 .

lookUpImpOrForward

注意 : runtime 有兩個版本 , 找源碼的時候不要找到 objc-class-old.mm 中去了 .bash

1.1 - 概覽

源碼以下 :app

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) {
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();

    //若是須要從緩存裏面查找,那須要先從緩存裏面找
    // 第一次進入爲 false , 由於彙編快速查找流程沒找到進入.
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp; // 找到就直接返回 imp
    }
    // 加把鎖 
    runtimeLock.lock();
    checkIsKnownClass(cls);
    
    /** - 爲查找方法作準備條件,判斷類有沒有加載好 - 若是沒有加載好,那就先加載一下類信息,準備好父類、元類 - 只會加載一次. - 具體能夠參考 realizeClass 具體實現. */
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    
    // 當 sel == initialize, _class_initialize 將會調用 +initialize
    // 確保對象已經初始化
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
    
 retry:    
    /** 這部分代碼過長先省略 , 後面詳細分析...*/
 done:
    runtimeLock.unlock();
    return imp;
}
複製代碼

具體操做都加了註釋.ide

  • 先來看下調用普通實例方法時 , 該方法的參數狀況 .

第一次進入該函數是由 objc_msgSend 彙編快速查找完 cache , 而後 cache miss 纔會來到 , 所以傳入 cachefalse .函數

  • 源碼分析 :
    • 1️⃣ : 若是指定須要查找緩存 , 就先查一次緩存 , 找到直接 return, 找不到繼續下面步驟 ( 第一次進來爲不查找緩存 , 由於沒有緩存 ) .
    • 2️⃣ : 加鎖 , 確保類已經初始化 ,準備好父類 , 元類 .

1.2 - retry

1.2.1 - 查找本類

// 查找本類緩存
imp = cache_getImp(cls, sel);
if (imp) goto done;

// 查找本類方法列表 class_data_bits_t -> bits.
{
    Method meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
}
複製代碼
  • 源碼分析 :
    • 1️⃣ : 查找本類緩存 , 找到直接跳轉到 done, 找不到繼續下面步驟.
    • 2️⃣ : 查找本類方法列表 class_data_bits_t , 找到後進行緩存並打印 , 直接跳轉到 done , 找不到繼續下面步驟 .

1.2.2 - 循環查找父類

{
    unsigned attempts = unreasonableClassCount();
    for (Class curClass = cls->superclass;
         curClass != nil;
         curClass = curClass->superclass)
    {
        if (--attempts == 0) {
            _objc_fatal("Memory corruption in class list.");
        }
        
        // 查找父類緩存
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // 找到 -> 把方法緩存,跳轉到 done
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                break;
            }
        }
        
        // 查找父類方法列表
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
}
複製代碼
  • 源碼分析 :
    • 1️⃣ : 循環遍歷父類 , 查找方法緩存 , 找到直接跳轉到 done, 找不到繼續下面步驟.
    • 2️⃣ : 查找方法列表 , 找到把方法添加到緩存並打印 , 直接跳轉到 done , 找不到繼續遍歷 .
    • 3️⃣ : 直到遍歷結束 superclass 爲 nil , 繼續下面流程 .

1.3 動態方法解析

retry 部分通過 本類 , 父類方法緩存和方法列表查找都沒找到時來到此處. 代碼以下:源碼分析

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;
}
複製代碼

咱們能夠看到,在通過 _class_resolveMethod 後,會進行一遍 retry 操做,從新進行一遍方法的查找流程,而且只有一次動態方法解析的機會 .

官方給出註釋以下 :

Don't cache the result ; we don't hold the lock so it may have changed already. Re-do the search from scratch instead .

咱們先來看下 _class_resolveMethod 的實現 , 再來看這個問題 .

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);
        }
    }
}
複製代碼

源碼流程分析 :

  • 若是本類爲元類 : 說明方法爲類方法 . 調用 _class_resolveInstanceMethod

  • 若是本類不是元類 : 說明方法爲實例方法 . 調用 _class_resolveClassMethod

    • 調用 lookUpImpOrNil 若是沒找到 , 調用 _class_resolveInstanceMethod

1.3.1 _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*/)) 
    {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    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 {
            _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));
        }
    }
}
複製代碼

源碼流程分析 :

  • 判斷是否實現 SEL_resolveInstanceMethod 方法,即 +(BOOL)resolveInstanceMethod:(SEL)sel , 提示 , NSObject 已經實現這個方法 , 默認返回爲 NO
  • 向本類發送 SEL_resolveInstanceMethod 消息 , 即調用這個方法 .
  • 調用解析器方法 ( SEL_resolveInstanceMethod ) 完成後 , 從新檢查有沒有這個 selimp .
  • 該方法執行完 , 迴歸到 lookUpImpOrForward 中 , 進行 retry , triedResolver 設置爲 YES .

講到這裏 , 是否是就對咱們剛剛所說的 : 在通過 _class_resolveMethod 後,會進行一遍 retry 操做,從新進行一遍方法的查找流程,而且只有一次動態方法解析的機會 豁然開朗了 .

1.3.2 _class_resolveClassMethod

這個方法和 1.3.1 - _class_resolveInstanceMethod 基本同樣 , 只不過發消息的對象變成元類 , 以下圖. 這裏就不重複贅述了 .

1.4 消息轉發入口

retry 最後一步中源碼以下 :

// No implementation found, and method resolver didn't help. 
// Use forwarding.

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製代碼

lookUpImpOrForward 方法中,通過對本類 , 父類的緩存,方法列表查找和動態方法解析後,若是以上步驟都沒有進行處理,那麼就會進入,消息處理的最後一步,即消息轉發流程,這也是蘋果預留的補救方法崩潰最後一步。

1.5 done

找到 imp , 返回.

done:
    runtimeLock.unlock();
    return imp;
複製代碼

問題

爲何當調用類方法時 , 對元類進行了動態方法解析也沒有找到 imp 時 , 還要再對類進行實例方法的解析呢 ? 以下圖 :

答 :

  • 其實這是因爲 isa 走位的緣由 , 根源類的父類是 NSObject ,

  • 類方法和實例方法的區別是存儲地不一樣 , 類方法是在元類中 , 實例方法是在類對象中 , 都是存儲在方法列表裏 , 並沒有 + / - 之分 .

  • 所以當調用類方法時 , 繼承鏈走到根源類並無查找到時 , 是須要繼續走其父類 NSObject 查一次動態方法解析器的 , 而 NSObject 做爲類對象 , 是須要經過實例方法去查找的 .

總結 :

也就是說 , 當在本類和父類中方法緩存以及方法列表都沒有找到 imp 時 , 蘋果給了咱們一次動態方法解析的機會 , 也就是在 resolveInstanceMethod 中本身給這個方法添加 imp , return true 便可 .

二、消息轉發

在上面流程中 , 都沒有找到函數實現地址 , 那麼就進入了消息轉發流程 , 調用入口以下 :

_objc_msgForward_impcacheobjc_msgSend 同樣 , 是由彙編實現的 , 所以全局搜索 , 直接來到 入口處.

2.1 _objc_msgForward_impcache

代碼以下 :

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
複製代碼

還記得上篇文章提到的 彙編函數入口和結束符吧 ENTRY , END_ENTRY .

  • 這段函數很明顯 只調用了 __objc_msgForward .

  • 搜索 __objc_msgForward , 就在下面 , 一樣是彙編函數 .

ENTRY __objc_msgForward

adrp	x17, __objc_forward_handler@PAGE
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward
複製代碼

彙編代碼解析 :

  • _objc_forward_handlerimp 放到 x17 寄存器中 .
  • x17 寄存器中將低 32 位的數據放到 p17 裏 .
  • 調用 x17 存儲的函數 imp .

也就是說咱們須要找 _objc_forward_handler .

2.2 _objc_forward_handler

全局搜索 , 在 objc-runtime.mm 中找到以下 :

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製代碼

再搜索 objc_defaultForwardHandler , 找不到啥時候賦值的了 , 跟不進去了 ..

怎麼辦呢 ?

別急 , 既然前輩們都已經探索出來了 , 必然是有地方能夠尋找點蛛絲馬跡的 . 回顧一下本篇文章方法查找流程裏 , 咱們忽視掉一些細節 .

在整個流程裏 , 都是有一個打印的. 那麼打印到哪兒了呢 ? 點進去看下 .

2.3 打印查找

2.3.1 log_and_fill_cache

static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
複製代碼

其中 SUPPORT_MESSAGE_LOGGING 爲 :

# define SUPPORT_MESSAGE_LOGGING 1
複製代碼

也就是說 objcMsgLogEnabled 只要爲 true , 就能夠打印了 , 那麼怎麼把它設置爲 true 呢 ?

2.3.2 instrumentObjcMessageSends

cmd + 點擊 objcMsgLogEnabled . 來到 objc-class.mm . 搜索 objcMsgLogEnabled , 咱們看到有以下函數 .

void instrumentObjcMessageSends(BOOL flag) {
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
複製代碼

這裏咱們找到 當調用這個函數 , 參數會被賦值給 objcMsgLogEnabled .

所以來到咱們的代碼中 .

修改代碼以下 :

#import "LBStudent.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LBStudent *student = [[LBStudent alloc] init];
        
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);
        
    }
    return 0;
}
複製代碼

運行工程 . 來到 /private/tmp 目錄下 , 打開最新的一份 msgSends 文件 :

打開以下 :

2.4 快速轉發流程

這其中 resolveInstanceMethod 動態方法解析咱們已經分析過了 , 來到 forwardingTargetForSelector , 搜索一下發現其只有在 NSObject 中默認實現了返回爲 nil , 並沒有其餘的方法實現了 .

2.4.1 查找源碼

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
複製代碼

2.4.2 官方文檔

那麼這時該怎麼辦 ? 不急 , 來找官方文檔 .

小提示 :

主要看 Discussion

2.4.3 方法解析

文檔說明了該方法的前世此生 , 總結以下 :

  • 1️⃣ : 該方法的目的 , 說白了就是你搞不定的方法 , 就交給別人處理 . 可是不能返回 self ,不然會一直找不到 , 陷入死循環 .
  • 2️⃣ : 若是不實現或者返回 nil ,會走到 forwardInvocation: 方法進行處理 .
  • 3️⃣ : 被轉發消息的接受者參數和返回值等須要和原方法相同 .

也就是說咱們能夠本身新定義一個類 , 或者選一個現有的類 , 其實現了這個方法 , 就交由其去處理.

2.4.4 方法使用

寫法以下 :

// 消息轉發流程
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(saySomething)) {
        return [LBTeacher new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

在運行 , 就很愉快的打印出 LBTeachersaySomething 方法了 .

這個轉發給單一方法實現對象的過程 , 咱們也稱之爲 快速消息轉發流程 .

2.5 慢速轉發流程

當快速消息轉發流程也並無實現 , 或者返回爲 nil , 就來到了 慢速消息轉發流程 .

一樣 , 根據咱們的打印來尋求線索 .

2.5.1 查找源碼

找到 methodSignatureForSelector .

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}
複製代碼

查看源碼一樣是沒有發現 . 老辦法 , 找官方文檔 .

2.5.2 官方文檔

2.5.3 方法解析

其實熟悉方法和函數的同窗應該很清楚 , 這個就是咱們的方法簽名 .

  • 該方法是讓咱們根據方法選擇器 SEL 生成一個 NSMethodSignature 方法簽名並返回 .
  • 這個方法簽名裏面其實就是封裝了返回值類型,參數類型等信息。

2.5.4 方法使用

寫法以下 :

// 方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(saySomething)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
複製代碼

2.5.5 小提示

方法簽名encode表

Code Meaning
c A char
i An int
s A short
l A long
l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

很明顯 , 只給蘋果一個方法簽名是確定不行的 , 必需要有方法實現啊 , 別急 , 剛剛咱們在 forwardingTargetForSelector 的官方文檔中看到 , 若是返回 nil 或者不實現 , 則會進入 forwardInvocation: 的流程 .

2.5.6 forwardInvocation

一樣查找官方文檔後 , 方法解析以下 :

  • 1️⃣ : forwardInvocationmethodSignatureForSelector 必須是同時存在的 .

  • 2️⃣ : 該方法能夠自由指派多個對象來接收這個消息 .

  • 3️⃣ : 將消息發送到該對象。保存結果 ,運行時系統將提取此結果並將其傳遞給原始發消息人。

該方法能夠指定多個轉發者 , 並且因爲 NSInvocation 的封裝 , 能夠自由調配 target , 參數等等 , 自由度較高 , 但與此同時花費也將更高 , 官方文檔稱其爲 more expensive forwardInvocation: machinery . 使用方法以下 :

// 消息轉發
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    NSLog(@"%@",anInvocation);
    
    SEL aSelector = [anInvocation selector];
    
    if ([self respondsToSelector:aSelector]){
        [anInvocation invoke];
    }else if ([[LBTeacher new] respondsToSelector:aSelector]){
        [anInvocation invokeWithTarget:[LBTeacher new]];
    }
    else
       [super forwardInvocation:anInvocation];
}
複製代碼

至此 , 整個方法查找 , 動態方法解析 , 消息轉發的完整流程咱們已經講完了 , 整個流程走完 , 若是仍然沒有找到方法實現 , 那麼蘋果表示 我給過你太多機會了 , 我也沒轍了.

三、面試題

最後 留下兩道面試題 , 以幫助你們檢驗本身是否理解了 方法的本質 , 以及 方法查找和消息轉發的知識 .

3.1 isKindOfClass 與 isMemberOfClass

#import "LBPerson.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     
        BOOL re3 = [(id)[LBPerson class] isKindOfClass:[LBPerson class]];       
        BOOL re4 = [(id)[LBPerson class] isMemberOfClass:[LBPerson class]];     
        NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

        BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];   
        BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     
        BOOL re7 = [(id)[LBPerson alloc] isKindOfClass:[LBPerson class]];       
        BOOL re8 = [(id)[LBPerson alloc] isMemberOfClass:[LBPerson class]];     
        NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
    }
    return 0;
}
複製代碼

LBPerson 爲一個普通的 OC 類 .

問打印結果.

3.2 方法查找面試題

3.2.1 題目 1 :

LBStudent 繼承自 LBPerson , LBPerson 中有一個方法以下 :

//聲明
- (void)sayHi;

//實現
- (void)sayHi{
    NSLog(@"hi");
}
複製代碼

[LBStudent performSelector:@selector(sayHi)];

調用結果是什麼 .

3.2.2 題目 2 :

LBPerson 繼承自 NSObject , LBStudent 繼承於 LBPerson , 項目中有一個 NSObject 的分類以下 :

// NSObject+LB.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (LB)
- (void)sayHi;
@end
NS_ASSUME_NONNULL_END

// NSObject+LB.m
#import "NSObject+LG.h"
#import <AppKit/AppKit.h>

@implementation NSObject (LB)
- (void)sayHi{
    NSLog(@"%s",__func__);
}
@end

複製代碼

問 :

[LBStudent performSelector:@selector(sayHi)];
複製代碼

打印結果是什麼 .

若是將分類裏 類方法改成實例方法 , 打印結果是什麼 ?

你們能夠在評論區留下本身的答案 , 下期揭曉 .

四、發散思惟

你們能夠由此來開拓思惟 , 想想若是讓你來作一個防止崩潰的三方庫 , 你會如何思考 , 如何設計 ?

相關文章
相關標籤/搜索