iOS底層系列14 -- 消息流程的動態方法決議與轉發

iOS底層系列12 -- 消息流程的快速查找iOS底層系列13 -- 消息流程的慢速查找這兩篇文章中分別介紹了objc_msgSend快速查找與方法列表的慢查找,如果都沒有找到方法實現就會進入動態方法決議和消息轉發。

  • 動態方法決議:慢速查找流程未找到方法實現時,會執行一次動態方法決議;
  • 消息轉發:如果動態方法決議仍然沒有找到方法實現時,則進行消息轉發;
  • 如果動態方法決議消息轉發都沒有做任何操作,就會出現崩潰報錯,即unrecognized selector sent to instance xxxx

動態方法決議

  • 在慢速查找中沒有找到方法實現,會嘗試進行一次動態方法決議,源碼實現如下:
static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
  • 判斷當前類如果不是元類,執行實例方法的動態方法決議resolveInstanceMethod
  • 當前類是元類,執行類方法的動態方法決議resolveClassMethod,如果在元類中沒有找到或者為空,則在元類的實例方法的動態方法決議resolveInstanceMethod中查找,主要是因為類方法在元類中是實例方法,所以還需要查找元類中實例方法的動態方法決議
  • 實例方法的動態方法決議resolveInstanceMethod源代碼實現如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

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

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    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));
        }
    }
}
  • 在發送resolveInstanceMethod消息前,首先查找cls類中是否有該方法的實現,即通過lookUpImpOrNil方法又會進入lookUpImpOrForward慢速查找流程,但這次查找的是resolveInstanceMethod方法
    • 如果沒有實現,則直接返回;
    • 如果有實現,則執行resolveInstanceMethod方法;
  • 實例方法的動態方法決議代碼測試:
@interface YYPerson : NSObject

- (void)walk;

+ (void)speak;

@end
#import "YYPerson.h"
#import <objc/runtime.h>

@implementation YYPerson

- (void)walk_resolve{
    NSLog(@"walk_resolve");
}

//給當前類動態添加一個方法和方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"walk"]) {
        NSLog(@"walk -- ");
        IMP imp = class_getMethodImplementation(self, @selector(walk_resolve));
        Method method = class_getInstanceMethod(self,@selector(walk_resolve));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self,sel,imp,type);
    }
    return [super resolveInstanceMethod:sel];
}
@end
  • 在resolveInstanceMethod方法內部打下斷點,當應用停在斷點,控制臺輸入bt命令,打印出函數調用堆棧如下所示:
Snip20210301_112.png
  • 類方法的動態方法決議代碼測試:
#import "YYPerson.h"
#import <objc/runtime.h>

@implementation YYPerson

+ (void)speak_resolve{
    NSLog(@"speak_resolve");
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"speak"]) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("YYPerson"), @selector(speak_resolve));
        Method method  = class_getInstanceMethod(objc_getMetaClass("YYPerson"), @selector(speak_resolve));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("YYPerson"),sel,imp,type);
    }
    return [super resolveClassMethod:sel];
}

@end
  • 斷點同上設置,函數調用堆棧如下:
Snip20210301_113.png
  • 若動態方法決議沒有手動去實現,就會進入消息轉發的流程;

消息轉發

  • 消息轉發的處理主要分為兩個部分:
    • 快速轉發:當慢速查找,以及動態方法決議均沒有找到實現時,進行消息轉發,首先是進行快速消息轉發,即執行forwardingTargetForSelector方法;
      • 如果返回消息接收者,在消息接收者中還是沒有找到,則進入另一個方法的查找流程;
      • 如果返回nil,則進入慢速消息轉發;
    • 慢速轉發:執行到methodSignatureForSelector方法;
      • 如果返回的方法簽名為nil,則直接崩潰報錯;
      • 如果返回的方法簽名不為nil,走到forwardInvocation方法中,對invocation事務進行處理,如果不處理也會造成崩潰報錯;
  • 消息快速轉發的代碼測試如下:
#import <Foundation/Foundation.h>

@interface YYStudent : NSObject

- (void)walk;

@end
#import "YYStudent.h"

@implementation YYStudent

- (void)walk{
    NSLog(@"%s",__func__);
}

@end
#import "YYPerson.h"
#import <objc/runtime.h>
#import "YYStudent.h"

@implementation YYPerson

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(walk)) {
        return [YYStudent new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end
  • YYPerson類沒有實現walk實例方法,實現forwardingTargetForSelector函數可將消息轉發給YYStudent實例對象(實現了walk方法);

  • forwardingTargetForSelector函數內部斷點調試如下:

Snip20210302_114.png
  • 可以看出消息的快速轉發調用了CoreFoundation框架;
  • 若當消息的快速轉發沒有進行處理,就會進入消息的慢速轉發流程,測試代碼如下:
#import "YYPerson.h"
#import <objc/runtime.h>
#import "YYStudent.h"

@implementation YYPerson

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

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    SEL sel = anInvocation.selector;
    YYStudent *student = [[YYStudent alloc]init];
    if ([student respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:student];
    }else{
        [anInvocation doesNotRecognizeSelector:sel];
    }
}

@end
  • methodSignatureForSelector為需要慢速轉發的消息提供方法簽名;
  • forwardInvocation系統會為需要轉發的消息創建一個NSInvocation事務對象,我們可以對NSInvocation事務進行處理,如果不處理也不會崩潰報錯;

總結

綜合 iOS底層系列12 -- 消息流程的快速查找iOS底層系列13 -- 消息流程的慢速查找以及本篇,objc_msgSend發送消息的整體流程就分析完成了,現作出如下總結:

  • 快速查找流程:首先在類的緩存cache中查找指定方法的實現;
  • 慢速查找流程:如果緩存中沒有找到,則在類的方法列表中查找(二分法),如果還是沒找到,則根據類/元類的繼承鏈在父類的緩存和方法列表中查找,一直遞歸到nil;
  • 動態方法決議:如果慢速查找還是沒有找到時,第一次補救機會就是嘗試一次動態方法決議,即實現resolveInstanceMethod/resolveClassMethod 方法;
  • 消息轉發:如果動態方法決議沒有處理,則進行消息轉發,消息轉發中有兩次補救機會:快速轉發+慢速轉發
  • 如果消息轉發也沒有處理,則程序直接報錯崩潰unrecognized selector sent to instance

super的本質

  • 定義類YYPerson,實現一個run方法;
  • 定義一個子類YYStudent,繼承自YYPerson;
  • 測試代碼如下:
#import "YYStudent.h"

@implementation YYStudent

- (instancetype)init{
    self = [super init];
    if (self) {
        NSLog(@"[self class] = %@",[self class]);
        NSLog(@"[self superclass] = %@",[self superclass]);
        
        NSLog(@"[super class] = %@",[super class]);
        NSLog(@"[super superclass] = %@",[super superclass]);
    }
    return self;
}

- (void)run{
    [super run];
    NSLog(@"%s",__func__);
}
@end
  • 終端輸入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YYStudent.m,看到YYStudent類的run方法的C++底層實現如下:
static void _I_YYStudent_run(YYStudent * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("YYStudent"))}, sel_registerName("run"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_YYStudent_1b9e06_mi_4,__func__);
}
  • 看到[super run],轉成了objc_msgSendSuper(struct objc_super,selector)
  • 其中第一個參數是一個objc_super類型的結構體,內部有兩個參數分別為:selfself的父類,也就是YYStudent的實例對象與YYPerson類;
  • [super message],底層轉成objc_msgsendSuper({self,父類對象},@selector(message)),消息的接受者依然是當前實例對象,只不過消息的查找越過了當前類,直接去其父類YYPerson中去查找;
#import <Foundation/Foundation.h>
#import "YYStudent.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YYStudent *student = [[YYStudent alloc]init];
    }
    return 0;
}
  • 控制臺打印結果:
Snip20210630_37.png
  • 看到 [self class]與[super class]打印結果相同;
  • 首先[self class] --> objc_msgSend(self,@selector(class))
  • 然后[super class] --> objc_msgSendSuper({self,YYPerson},@selector(class)
  • class方法實現是在NSObject基類里面的,其實現如下:
- (Class)class{
     object_getClass(self);
}
  • 也就是說class方法返回的結果,取決于消息的接受者self;
  • 所以 [self class]與[super class] 消息的接受者都是self,即YYStudent類的實例對象,所以最終的調用結果是相同的,都返回YYStudent類;
  • superClass的方法實現是在NSObject基類里面,其實現如下:
- (Class) superClass{
     object_get SuperClass(object_getClass(self));
}
  • 所以[self superClass]與[super superClass] 返回的都是YYPerson類;

objc_msgsend(instance,@selector)底層實現

  • 底層實現邏輯如下圖所示:
objc_msgSend.png
  • 主要分為三個階段:
    • 第一個階段:在緩存Cache_t中查找,是用匯編語言實現的;
    • 第二個階段:在類class結構體內部的class_rwe_t中的方法列表查找,是通過C語言實現的;
    • 第三個階段:動態方法決議與消息的轉發;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容