Runtime原理及應用

runtime介紹

  • Objective-C 擴展了 C 語言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向?qū)ο蠛蛣討B(tài)機制的基石。Runtime源碼下載
  • 理解 Objective-C 的 Runtime 機制可以幫我們更好的了解這個語言,適當?shù)臅r候還能對語言進行擴展,從系統(tǒng)層面解決項目中的一些設計或技術問題。了解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。

高級編程語言想要成為可執(zhí)行文件需先編譯為匯編語言再匯編為機器語言,機器語言也是計算機能夠識別的唯一語言,但是OC并不能直接編譯為匯編語言,而是要先轉寫為純C語言再進行編譯和匯編操作,從OCC語言的過渡就是runtime來實現(xiàn)的。然而我們使用OC進行面向?qū)ο箝_發(fā),而C語言更多的是面向過程開發(fā),這就需要將面向?qū)ο蟮念愞D變?yōu)槊嫦蜻^程的結構體。

Runtime消息傳遞

一個對象的方法像這樣[obj foo],編譯器轉成消息發(fā)送objc_msgSend(obj, foo),Runtime時執(zhí)行的流程是這樣的:

  1. 首先,通過objisa指針找到它的 class;
  2. classmethod listfoo ;
  3. 如果 class 中沒到foo,繼續(xù)往它的superclass 中找 ;
  4. 一旦找到 foo 這個函數(shù),就去執(zhí)行它的實現(xiàn)IMP 。

但這種實現(xiàn)有個問題,效率低。但一個class 往往只有 20% 的函數(shù)會被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的 80% 。每個消息都需要遍歷一次objc_method_list 并不合理。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來,那可以大大提高函數(shù)查詢的效率。這也就是objc_class 中另一個重要成員objc_cache 做的事情 - 再找到foo 之后,把foomethod_name 作為keymethod_imp 作為value 給存起來。當再次收到foo 消息的時候,可以直接在cache 里找到,避免去遍歷objc_method_list。從前面的源代碼可以看到objc_cache 是存在objc_class 結構體中的。

objec_msgSend的方法定義如下:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

那消息傳遞是怎么實現(xiàn)的呢?我們看看對象(object),類(class),方法(method)這幾個的結構體:

//對象
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
//類
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
//方法
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}
  1. 系統(tǒng)首先找到消息的接收對象,然后通過對象的isa找到它的類。
  2. 在它的類中查找method_list,是否有selector方法。
  3. 沒有則查找父類的method_list。
  4. 找到對應的method,執(zhí)行它的IMP
  5. 轉發(fā)IMPreturn值。

點擊查看消息傳遞用到的一些概念:

  • 類對象(objc_class)
  • 實例(objc_object)
  • 元類(Meta Class)
  • Method(objc_method)
  • SEL(objc_selector)
  • IMP
  • 類緩存(objc_cache)
  • Category(objc_category)

Runtime消息轉發(fā)

前文介紹了進行一次發(fā)送消息會在相關的類對象中搜索方法列表,如果找不到則會沿著繼承樹向上一直搜索知道繼承樹根部(通常為NSObject),如果還是找不到并且消息轉發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報unrecognized selector錯。那么消息轉發(fā)到底是什么呢?接下來將會逐一介紹最后的三次機會。

  • 動態(tài)方法解析
  • 備用接收者
  • 完整消息轉發(fā)

動態(tài)方法解析

首先,Objective-C運行時會調(diào)用 +resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數(shù)實現(xiàn)。如果你添加了函數(shù)并返回YES, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程。

實現(xiàn)一個動態(tài)方法解析的例子如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執(zhí)行foo函數(shù)
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {//如果是執(zhí)行foo函數(shù),就動態(tài)解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函數(shù)
}

打印結果:
2018-04-01 12:23:35.952670+0800 ocram[87546:23235469] Doing foo

可以看到雖然沒有實現(xiàn)foo:這個函數(shù),但是我們通過class_addMethod動態(tài)添加fooMethod函數(shù),并執(zhí)行fooMethod這個函數(shù)的IMP。從打印結果看,成功實現(xiàn)了。

如果resolve方法返回 NO ,運行時就會移到下一步:forwardingTargetForSelector。

備用接收者

如果目標對象實現(xiàn)了-forwardingTargetForSelector:,Runtime 這時就會調(diào)用這個方法,給你把這個消息轉發(fā)給其他對象的機會。

實現(xiàn)一個備用接收者的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函數(shù)
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執(zhí)行foo函數(shù)
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,進入下一步轉發(fā)
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [Person new];//返回Person對象,讓Person對象接收這個消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

打印結果:
2018-04-01 12:45:04.757929+0800 ocram[88023:23260346] Doing foo
可以看到我們通過forwardingTargetForSelector把當前ViewController的方法轉發(fā)給了Person去執(zhí)行了。打印結果也證明我們成功實現(xiàn)了轉發(fā)。

完整消息轉發(fā)

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發(fā)機制了。
首先它會發(fā)送-methodSignatureForSelector:消息獲得函數(shù)的參數(shù)和返回值類型。如果-methodSignatureForSelector:返回nil ,Runtime則會發(fā)出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。如果返回了一個函數(shù)簽名,Runtime就會創(chuàng)建一個NSInvocation 對象并發(fā)送 -forwardInvocation:消息給目標對象。

實現(xiàn)一個完整轉發(fā)的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函數(shù)
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執(zhí)行foo函數(shù)
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,進入下一步轉發(fā)
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,進入下一步轉發(fā)
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//簽名,進入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }
}
@end

打印結果:
2018-04-01 13:00:45.423385+0800 ocram[88353:23279961] Doing foo

從打印結果來看,我們實現(xiàn)了完整的轉發(fā)。通過簽名,Runtime生成了一個對象anInvocation,發(fā)送給了forwardInvocation,我們在forwardInvocation方法里面讓Person對象去執(zhí)行了foo函數(shù)。簽名參數(shù)v@:怎么解釋呢,這里蘋果文檔Type Encodings有詳細的解釋。

以上就是Runtime的三次轉發(fā)流程。下面我們講講Runtime的實際應用。

Runtime應用

Runtime簡直就是做大型框架的利器。它的應用場景非常多,下面就介紹一些常見的應用場景。

  • 關聯(lián)對象(Objective-C Associated Objects)給分類增加屬性
  • 方法魔法(Method Swizzling)方法添加和替換和KVO實現(xiàn)
  • 消息轉發(fā)(熱更新)解決Bug(JSPatch)
  • 實現(xiàn)NSCoding的自動歸檔和自動解檔
  • 實現(xiàn)字典和模型的自動轉換(MJExtension)
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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