Runtime小結

原文連接html

首先,爲何說ObjC是動態語言objective-c

咱們看下蘋果官方文檔對runtime的定義安全

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.app

譯文以下框架

Objective-C運行時是一個運行時庫,它提供對Objective-C語言的動態屬性的支持,所以被全部Objective-C應用程序連接。 Objective-C運行時庫支持函數在/usr/lib/libobjc.A.dylib中的共享庫中實現。ide

在Objective-C中,消息直到運行時才綁定到方法實現。編譯器將把方法調用轉化爲消息發送函數

例如以下代碼ui

[receiver message]
複製代碼

將會被轉化爲這種調用方式atom

objc_msgSend(receiver, selector)
複製代碼

在消息須要綁定參數的時候會轉化以下spa

objc_msgSend(receiver, selector, arg1, arg2, ...)
複製代碼

那麼抓花爲發送消息以後都作了什麼呢?

[receiver message]
複製代碼
  1. 經過receiver的 isa 指針 查找它的 Class
  2. 查找 Class 下的 methodLists
  3. 若是 methodLists 沒有相應的方法則遞歸查找 superClass 的 methodLists
  4. 若是在 methodLists 裏面找到了 對應的 message 則 獲取實現指針 imp 並執行
  5. 發送方法返回值

這裏咱們發現還缺乏了一種狀況,那就是遞歸在父類的methodlist裏面也沒有找到對應的實現,這個時候就會報錯 unrecognized selector send to instance X

消息轉發


Runtime 爲這種可能提供了最後的機會,就是觸發消息轉發流程

  1. resolveClassMethod/resolveInstanceMethod ,向對象發送其沒法識別的消息後會觸發,在這裏能夠動態添加方法的實現
  2. forwardingTargetForSelector ,快速轉發,能夠把對應的消息發送給其餘對象
  3. methodSignatureForSelector ,對方法進行簽名(爲完整的消息轉發作準備)
  4. forwardInvocation ,進行完整的消息轉發(包括修改實際方法,對象等)
  5. doesNotRecognizeSelector , 最後若是還未執行方法,就會拋出錯誤

Show Me The Code:

動態添加方法:

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

@interface AViewController ()

@end

@implementation AViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(speak)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(speak)) {
        class_addMethod([self class], sel, (IMP)fakeSpeak, "v@:");
        // 關於最後一個參數能夠看https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        return true;
    }
    return [super resolveInstanceMethod:sel];
}

void fakeSpeak(id target, SEL _cmd){
    
    NSLog(@"method added");
}

@end
複製代碼

快速轉發

#import "AViewController.h"

#import <objc/runtime.h>

@interface AViewController ()

@end

@implementation AViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(speak)];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(speak)) {
        
        return [XXXX new];
    }
    
    return nil;
}

@end
複製代碼

完整轉發

#import "AViewController.h"

#import <objc/runtime.h>

@interface AViewController ()

@end

@implementation AViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(speak)];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(speak)) {
        
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation setSelector:@selector(otherMethod)];
    [anInvocation invokeWithTarget:self];
}

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

@end
複製代碼

對消息轉發的流程有了一些基本概念之後咱們就能夠稍微深刻看看方法交換這個理念了。

方法交換

有的時候咱們可能會面對一些需求,好比在每一個頁面中統一都作的一些處理,像訪問埋點等邏輯,若是一個一個去改寫的話十分麻煩,用繼承的方式去作慢慢會產生各類耦合的狀況,這裏,咱們可使用方法交換的方式去統一添加處理。

好比咱們須要在每個 ViewController viewDidLoad 的方法中輸出一個log 先建立一個 category

#import "UIViewController+Log.h"
#import <objc/runtime.h>

@implementation UIViewController (Log)

static void AGExchangeMethod(Class cls, SEL originSelector, SEL newSelector) {
    
    Method originMethod = class_getInstanceMethod(cls, originSelector);
    Method newMethod = class_getInstanceMethod(cls, newSelector);
    
    // method_exchangeImplementations(newMethod, originMethod);
    
    BOOL addMethod = class_addMethod(cls, originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
    
    if (addMethod) {
        
        class_replaceMethod(cls, newSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        
    }else {
        
        method_exchangeImplementations(newMethod, originMethod);
    }
}

+ (void)load {
    static dispatch_once_t once;
    dispatch_once(&once, ^{
       
        AGExchangeMethod([self class], @selector(viewDidLoad), @selector(Logging));
    });
}

- (void)Logging{
    
    NSLog(@"%s",__func__);
    
    [self Logging];
}

@end
複製代碼

編譯運行,你能夠看到控制檯會輸出 Logging

這裏有幾個地方須要特別留意下

  1. 若是是交換的系統方法,在新的方法內部必定要再調用這個方法一次,由於這個時候方法的imp指針已經交換,調用該方法就是調用系統方法,爲何要這麼作呢,由於這些系統的方法是黑盒的,有不少咱們不清楚的操做,若是不調用可能會給程序帶來問題
  2. 執行交換方法的最佳時機是在類方法load中, 該方法會在類被加載的時候執行
  3. 必定要使用GCD或其餘辦法保證交換的線程安全,僅執行一次,防止出現錯誤。

關聯對象


好比咱們想要爲 UIViewController 添加一個flag屬性記錄狀態,可是沒法更改 UIViewController,那麼咱們能夠在 category 中添加屬性

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (Log)

@property (nonatomic ,copy) NSString *flag;

@end

NS_ASSUME_NONNULL_END

複製代碼

而後在其餘的 viewController 中使用

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.flag = @"active";
}
複製代碼

運行後能夠看到崩潰 unrecognized selector sent to instance , 這是由於在 category 中 property修飾符並不會自動爲咱們生成成員變量,而咱們知道,屬性實際上是 ivar + getter & setter ,因此咱們可使用 runtime 來手動關聯:

在 category 的 .m 文件中增長如下代碼

- (void)setFlag:(NSString *)flag {
    objc_setAssociatedObject(self, @selector(flag), flag, OBJC_ASSOCIATION_COPY);
}

- (NSString *)flag {
    return objc_getAssociatedObject(self, _cmd);
}
複製代碼

而後就能夠在其餘 viewController 中隨意使用了,因爲 objc_setAssociatedObject 也是在ARC管理之下的因此咱們也沒必要手動釋放。

寫在最後

雖然 Runtime 有諸多魔幻的使用方法,可是不建議過多的使用(除非掌握的很熟練),除非是開發框架,不然多個互相交換的方法和動態的屬性在調試的時候會很無奈的。。。

相關文章
相關標籤/搜索