OC底層原理十四:objc_msgSend(消息轉(zhuǎn)發(fā))

OC底層原理 學(xué)習(xí)大綱

當(dāng)我們發(fā)送的消息,在消息接受者以及它的整個(gè)繼承鏈緩存(Cache)方法列表(methodList)中都找不到時(shí),系統(tǒng)給我們留了崩潰前最后一次挽救機(jī)會(huì)

本節(jié)將探索方法找不到時(shí)最后的挽救機(jī)會(huì) - 消息轉(zhuǎn)發(fā)機(jī)制。

1. 前期準(zhǔn)備
2. 動(dòng)態(tài)方法決議
3. 消息轉(zhuǎn)發(fā)
4. 異常消息處理流程圖

1.前期準(zhǔn)備

打開objc4源文件,在main.m中加入測(cè)試代碼:

@interface HTPerson : NSObject
- (void)sayHello;
+ (void)say666;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson * person  = [HTPerson alloc];
        [person sayHello];
        
    }
    return 0;
}

sayHello方法不實(shí)現(xiàn)。模擬方法找不到的場(chǎng)景。

  • 熟悉上兩節(jié)快速查找慢速查找內(nèi)容,知道消息在匯編層cache中高速查找,找不到imp后,會(huì)進(jìn)入C/C++混編層,調(diào)用lookUpImpOrForward 函數(shù)。在查詢cls接收者和cls的整個(gè)繼承鏈對(duì)象cachemethodlist,都找不到對(duì)應(yīng)的imp時(shí)。我們會(huì)進(jìn)入動(dòng)態(tài)方法決議 resolveMethod_locked。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER; // behavior取反后,下一次進(jìn)入判斷時(shí),if條件就不成立了。 確保動(dòng)態(tài)決議只執(zhí)行一次
    return resolveMethod_locked(inst, sel, cls, behavior);
}

補(bǔ)充lookUpImpOrForward的入?yún)⒗斫?/p>

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
  • inst:真實(shí)持有sel方法的對(duì)象方法存放在類中,類方法存放在元類中
    對(duì)象方法: [person sayHello], instHTPerson
    類方法: [HTPerson say666], inst也是HTPerson元類

  • sel:方法名

  • cls:方法接受者的所屬類
    對(duì)象方法: [person sayHello], cls是HTPerson
    類方法: [HTPerson say666], cls也是HTPerson

  • behavior:行為參數(shù), 影響進(jìn)入動(dòng)態(tài)決議等判斷條件。

2. 動(dòng)態(tài)方法決議

  • 進(jìn)入resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
  
    // 對(duì)象方法
    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    // 類方法
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // 重新查詢一次
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

判斷是cls是元類還是本類:

  • 如果不是元類,就是對(duì)象方法,直接調(diào)用resolveInstanceMethod

  • 否則,就是類方法,先調(diào)用resolveClassMethod,再調(diào)用lookUpImpOrNil檢查是否找到imp,沒找到的話再調(diào)用resolveInstanceMethod。

    意思是,如果類方法中沿著元類繼承鏈找到NSObject元類了都沒找到,再找就是NSObjet本類了,NSObjet本類存的是對(duì)象方法,所以要用resolveInstanceMethod。

最后再調(diào)用lookUpImpOrForward重新查詢一次。

分析resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 1. 查找類對(duì)象的元類中是否有`resolveInstanceMethod`的imp。
    // (根元類中默認(rèn)實(shí)現(xiàn)了`resolveInstanceMethod`方法,所以永遠(yuǎn)不會(huì)return)
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        return;
    }
    
    // 2. 調(diào)用一次`resolveInstanceMethod`函數(shù)
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // 3. 再搜索一次sel的imp
    //(如果在上面resolveInstanceMethod函數(shù)實(shí)現(xiàn)了sel,我們就拿到imp了,成功將sel和imp寫入cls的緩存中)
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    
    // 做Log記錄
    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:

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
   // behavior = 0, LOOKUP_CACHE = 4, LOOKUP_NIL = 8
   return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

內(nèi)部繼續(xù)調(diào)用了lookUpImpOrForward函數(shù),不同的是,behavior變成了 0 | 4 | 8 = 12。 >這決定了進(jìn)入lookUpImpOrForward后:

  • fastpath(behavior & LOOKUP_CACHE) = 12 & 4 = 4,條件成立,會(huì)優(yōu)先cache_getImp讀取一次緩存
  • slowpath(behavior & LOOKUP_RESOLVER) = 12 & 2 = 0,不成立,不會(huì)進(jìn)入resolveMethod_locked動(dòng)態(tài)方法決議。

lookUpImpOrNil中的lookUpImpOrForward會(huì)循環(huán)遍歷cls繼承鏈所有類cachemethodList來(lái)尋找imp

所以resolveInstanceMethod函數(shù),就是系統(tǒng)給開發(fā)者的一次機(jī)會(huì)。

    1. 你可以在合適的地方加上resolveInstanceMethod函數(shù)。在函數(shù)內(nèi)部把sel實(shí)現(xiàn)
    1. 系統(tǒng)會(huì)發(fā)送消息,調(diào)用一次 resolveInstanceMethod函數(shù)
    1. 再次循環(huán)查找sel,如果在上面resolveInstanceMethod函數(shù)實(shí)現(xiàn)了sel,就可以拿到imp,并將selimp寫入cls的緩存中。

系統(tǒng)大哥用大白話跟你說(shuō):老弟,我檢查到你的方法沒有實(shí)現(xiàn),我現(xiàn)在給你一次機(jī)會(huì),你識(shí)趣的話,就在我調(diào)用 resolveInstanceMethod函數(shù)之前,在這函數(shù)內(nèi)部把你的sel實(shí)現(xiàn)了。等到我調(diào)用完后,你還是沒實(shí)現(xiàn)的話,我就了你。

案例:

  • 授人以漁
  1. 分析imp實(shí)現(xiàn)位置。
  • 如:[person sayHello]對(duì)象方法,對(duì)象方法存放本類中。[HTPerson say666]類方法,類方法是存放在元類中。
  1. imp寫哪里?
  • NSObject是所有類的父類,NSObject中有resolveInstanceMethod類方法。所以我們可以在繼承鏈的某個(gè)合適的類中重寫resolveInstanceMethod方法。在這個(gè)方法內(nèi)部,實(shí)現(xiàn)imp
  • 授人以魚
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface BaseObject: NSObject
@end

@implementation BaseObject

- (void)handleErrorImp {
    NSLog(@"我?guī)湍銚踝”罎⒘耍蠄?bào)數(shù)據(jù)給后臺(tái)還是咋操作,你自己想");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel ==  @selector(sayHello)) {
        NSLog(@"???????? %@ 沒實(shí)現(xiàn)!!????????", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(self, @selector(handleErrorImp));
        Method method = class_getInstanceMethod(self, @selector(handleErrorImp));
        const char * type = method_getTypeEncoding(method);
        // 將imp加入當(dāng)前類。
        return class_addMethod(self, sel, imp, type);
        
    }
    
    return [super resolveInstanceMethod: sel];
}
@end

@interface HTPerson : BaseObject
- (void)sayHello;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson * person  = [HTPerson alloc];
        [person sayHello];
        
    }
    return 0;
}
  • HTPerson繼承自我們自定義的基類BaseObject,HTPerson中的sayHello沒實(shí)現(xiàn),我們?cè)诨愔屑尤霐r截。將臨時(shí)impsel寫入基類函數(shù)列表中。成功防止崩潰。

具體操作時(shí),我們并不知道sel是哪個(gè),按理說(shuō)進(jìn)入resolveInstanceMethod的所有sel都是找不到imp的。我們可以統(tǒng)一攔截進(jìn)行數(shù)據(jù)上報(bào),然后做防崩潰處理。
(比如個(gè)人中心某個(gè)深層頁(yè)面函數(shù)未實(shí)現(xiàn),我們直接統(tǒng)一返回個(gè)人中心防止崩潰,同時(shí)將這次找不到的記錄上傳給后臺(tái),讓相應(yīng)的程序員哥哥下個(gè)版本修復(fù)它)

實(shí)際開發(fā)中,你不確定是否有人在繼承鏈上游就使用了resolveInstanceMethod黑魔法。這樣你的resolveInstanceMethod被重寫 ?? 畢竟牛逼的人很多。

這是打印結(jié)果:


image.png
  • 厲害的人很多,大家都知道用resolveInstanceMethod。那有沒有更厲害一點(diǎn)的呢?
    ------------------------------ 來(lái)了,老弟 ?? ------------------------------

3. 消息轉(zhuǎn)發(fā)

當(dāng)我們?cè)?code>動(dòng)態(tài)方法決議中不做任何處理,在崩潰前,系統(tǒng)還有2個(gè)隱藏挽救點(diǎn)。快速轉(zhuǎn)發(fā)forwardingTargetForSelector慢速轉(zhuǎn)發(fā)methodSignatureForSelector

在正式介紹這2個(gè)方法前,我們應(yīng)該知道如何發(fā)現(xiàn)他們的。

方法1: 日志查看

lookUpImpOrForwardlog_and_fill_cache函數(shù)中:

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

我們看到在cache_fill寫入操作前,會(huì)檢查SUPPORT_MESSAGE_LOGGING是否支持消息記錄

進(jìn)入logMessageSend函數(shù)內(nèi)部,發(fā)現(xiàn)日志存放路徑/tmp文件夾:

image.png

我們打開任意一個(gè)文件夾,按Command + Shift + G,輸入/tmp文件夾查看。發(fā)現(xiàn)并沒有msgSends名字的文件。

  • 因?yàn)?code>系統(tǒng)默認(rèn)是關(guān)閉日志功能,objcMsgLogEnabled默認(rèn)為false。
image.png

文件內(nèi)搜索objcMsgLogEnabled,發(fā)現(xiàn)賦值操作是在instrumentObjcMessageSends中。

image.png

所以我們?cè)?code>崩潰前,手動(dòng)調(diào)用instrumentObjcMessageSends進(jìn)行日志的開啟關(guān)閉

  • 測(cè)試代碼:

注意,這次不是在源碼環(huán)境。而是在任意一個(gè)正常項(xiàng)目中進(jìn)行測(cè)試。

// extern: 聲明當(dāng)前變量或函數(shù)在別的文件中定義了。不要報(bào)錯(cuò)。
extern void instrumentObjcMessageSends(BOOL flag);

@interface HTPerson : NSObject
- (void)sayHello;
@end

@implementation HTPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson * person  = [HTPerson alloc];
        instrumentObjcMessageSends(YES); // 打開記錄
        [person sayHello];
        instrumentObjcMessageSends(NO); // 關(guān)閉記錄
        
    }
    return 0;
}

運(yùn)行程序,crash后,在/tmp文件夾中看不到msgSends-92567文件

image.png

打開msgSends-92567文件,發(fā)現(xiàn)最上面的日志記錄為:

+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:

- HTPerson NSObject forwardingTargetForSelector:
- HTPerson NSObject forwardingTargetForSelector:

- HTPerson NSObject methodSignatureForSelector:
- HTPerson NSObject methodSignatureForSelector:

- HTPerson NSObject class

+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:

- HTPerson NSObject doesNotRecognizeSelector:
- HTPerson NSObject doesNotRecognizeSelector:

- HTPerson NSObject class

這是崩潰前調(diào)用的函數(shù)記錄。
我們發(fā)現(xiàn)調(diào)用者是HTPerson,我們直接在HTPerson中加入類似的方法進(jìn)行實(shí)現(xiàn)。

  • HTPerosn實(shí)現(xiàn)中打印這四個(gè)函數(shù)
@implementation HTPerosn

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

-(void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
    return [super doesNotRecognizeSelector:aSelector];
}

@end
image.png
  • 打印的順序和日志一樣,那這四個(gè)函數(shù)有什么用呢?

我們知道,resolveInstanceMethod動(dòng)態(tài)方法決議系統(tǒng)開發(fā)者留的最后一次機(jī)會(huì)。

  • 系統(tǒng)希望開發(fā)者resolveInstanceMethod中將未實(shí)現(xiàn)的sel完成實(shí)現(xiàn)。并且他會(huì)在給完機(jī)會(huì)后重新執(zhí)行一次lookUpImpOrForward。

  • 打印日志中有2次resolveInstanceMethod,如果我們沒有在resolveInstanceMethod中修改。 那么我們可以在第二次resolveInstanceMethod前的2個(gè)方法進(jìn)行操作。

所以我們可以在forwardingTargetForSelectormethodSignatureForSelector方法里做補(bǔ)救。

大家都知道改resolveInstanceMethod,那就讓你們隨意發(fā)揮。大佬在你們resolveInstanceMethod后面阻擊一切漏網(wǎng)之魚。??

現(xiàn)在,我們來(lái)了解大佬的法器

法器一:快速轉(zhuǎn)發(fā)forwardingTargetForSelector

  • 官方文檔:


    forwardingTargetForSelector

意思是:aSelector是沒實(shí)現(xiàn)的選擇器,我找不到實(shí)現(xiàn)的對(duì)象

  • 既然你找不到實(shí)現(xiàn)的對(duì)象,那我就給你個(gè)實(shí)現(xiàn)這個(gè)方法的對(duì)象

案例:

image.png

在第一次resolveInstanceMethod之后,成功替換實(shí)現(xiàn)方法,并攔截崩潰。

法器二:慢速轉(zhuǎn)發(fā)methodSignatureForSelector

forwardingTargetForSelector
image.png

意思是:返回一個(gè)NSMethodSignature函數(shù)簽名對(duì)象,Discussion中說(shuō)了需要實(shí)現(xiàn)協(xié)議。搭配forwardinvocation函數(shù)使用。

image.png

點(diǎn)擊進(jìn)入NSInvocation查看格式:

image.png

我們打印invocation看看

image.png
  • invocation就是一個(gè)漂流瓶,只要methodSignatureForSelector返回不為nil,且實(shí)現(xiàn)了forwardinvocation函數(shù),就一定不會(huì)崩潰。系統(tǒng)會(huì)把這個(gè)invocatio當(dāng)做一個(gè)漂流瓶,拋棄它了。 誰(shuí)想處理誰(shuí)處理,沒人處理我也不管了

  • invocation可以修改target接收對(duì)象和替換selector??刹僮餍钥臻g更大。

  • invocation需要調(diào)用invoke對(duì)象方法執(zhí)行([invocation invoke])

  • 必要時(shí)可以保存invocation,修改完參數(shù)再調(diào)用invoke執(zhí)行。

例如:

  • HTStudent實(shí)現(xiàn)一個(gè)sayNB,然后修改anInvocationtargetselector。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
   NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
   return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation {
   anInvocation.target = [HTStudent alloc];
   anInvocation.selector = @selector(sayNB);
   [anInvocation invoke];
}

4. 異常消息處理流程圖

異常消息處理流程圖

最后,奉上OC底層原理:objc_msgSend全流程圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容