runtime 入門與簡介

方法調用 objc_msgSend

調用方法(函數)是語言常用的功能,在 Objective-C 中專業一點的叫法是 傳遞消息(pass a message)。Objective-C 的方法調用都是 動態綁定 ,而C語言中函數調用方式是 靜態綁定 ( static binding ),也就是說,在編譯時期就能決定和知道在運行時所調用的函數。ios

如下面代碼爲例:c++

void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
    if(type == 0){
        sayHello();
    }else{
        sayGoodBye();
    }
}

基本上,上面的代碼在編譯的時候編譯器就知道 sayHellosayGoodBye 兩個函數的存在,函數地址是硬編碼在指令之中的。可是若是換一種寫法:編程

void sayHello(){
}
void sayGoodBye(){
}
void saySomething(int type){
    void (*something) ();
    if(type == 0){
        something = sayHello;
    }else{
        something = sayGoodBye;
    }
    something();
}

這就得使用 動態綁定 ,待調用的函數地址須要到運行時才能讀取出來。
在 Objective-C 中,對某一個對象傳遞消息,會用動態綁定機制來決定究竟是調用哪一個方法。而Objective-C是 C 的超集,底層是由 C語言實現,可是對象接收消息後會調用哪一個方法都是在運行期決定。緩存

給對象發送消息能夠這麼來寫:app

id object = [list objectAtIndex:1];

在這行代碼中, list 稱爲 接收者objectAtIndex 叫作 選擇器, 選擇器和參數合起來稱爲消息。當編譯器看到這行代碼的時候,會換成標準的C語言函數調用:函數

void objc_msgSend(id self, SEL cmd, ...);
id lastObject = objc_msgSend(list, @selector(objectAtIndex:), parameter);

objc_msgSend 這個函數能夠接收兩個及兩個以上的參數,第一個參數是接收者,第二個參數是選擇器,後面的參數是保持順序的原來消息傳遞的參數,objc_msgSend會依據接收者和選擇器來決定調用哪一個方法,首先在接收者的方法列表中尋找,若是找不到就會沿着繼承體系去向上一層一層的尋找,若是仍舊找不到就會執行消息轉發(message forwarding)
當消息第一次傳遞以後,objc_msgSend 會將匹配結果進行緩存,下次會直接調用方法。消息傳遞除了objc_msgSend以外在特殊狀況下還會有其餘的方法來處理:測試

  • objc_msgSend_stret 若是待發送的消息返回一個結構體,就會調用這個函數來處理。編碼

  • objc_msgSend_fpret 若是消息返回的是浮點數,就會調用這個函數進行處理。atom

  • objc_msgSendSuper 若是要傳遞消息給父類。spa

總結:

  • 消息由 接收者、選擇器及參數構成,給某對象 發送消息( invoke a message ) 也就至關於在該對象上調用方法。

  • 發送給某對象的所有消息都要有動態消息派發系統( dynamic message dispatch system ) 來處理。

消息轉發

在上面介紹了運行時的消息傳遞機制,可是卻沒有說對象收到消息卻沒法解讀該怎麼辦。本篇博客就着重介紹當消息傳遞時沒法解讀的時候就會啓動的 消息轉發機制( message forwarding )

開發可能常常會遇到這種狀況:

2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff9f2d94f2 __exceptionPreprocess + 178
    1   libobjc.A.dylib                     0x00007fff90db3f7e objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    3   CoreFoundation                      0x00007fff9f249571 ___forwarding___ + 1009
    4   CoreFoundation                      0x00007fff9f2490f8 _CF_forwarding_prep_0 + 120
    5   runtime                             0x0000000100001c1c main + 124
    6   libdyld.dylib                       0x00007fff91df85ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

這個異常信息是由 NSObjectdoesNotRecognizeSelector: 方法拋出來的,原本是給 AutoDictionary 的一個實例對象發送消息,可是該對象並無 setDate: 方法,因此消息轉發給了 NSObject ,最後拋出異常。

先看下消息處理機制流程圖:

消息處理機制流程圖

消息轉發分爲兩階段三步,第一階段先看接受消息的對象能不能本身處理這個沒法解讀的消息,這一步能夠動態的添加方法去解讀接受這個消息;第二階段是先看看對象本身不能處理這個消息,能不能交給其餘對象來進行處理,在這一步若是仍然沒法解讀消息,那麼就會走最後一步:把和消息有關的全部細節封裝到一個 NSInvocation 中,再詢問一次對象是否能解決。
看下三個方法:

// 詢問對象是否本身處理,是返回YES,通常會在這個方法裏面動態添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;

// 這一步詢問對象把消息交給哪一個對象來進行處理
- (id)forwardingTargetForSelector:(SEL)aSelector;

// 若是走到這一步的話,就把消息的全部信息封裝成 NSInvocation 對象進行 "最後通牒"
- (void)forwardInvocation:(NSInvocation *)anInvocation;

來一段代碼示例:
新建一個 AutoDictionary 類,添加一個 NSDate 類型的 date 屬性,在實現文件裏面用 @dynamic date; 禁止自動生成存取方法,這樣當代碼中給 AutoDictionary 實例對象的 date屬性賦值時就會出現消息沒法解讀的現象。
.h 文件:

@interface AutoDictionary : NSObject

@property (nonatomic, strong) NSDate *date;

@end

.m 實現文件代碼內容:

@interface AutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;

/**
 *  該類僅在實現文件 實現了
 *  - (NSDate *)date
 *  - (void)setDate:(NSDate *)date
 *  兩個方法,用於處理 AutoDictionary 沒法解讀的消息
 */
@property (nonatomic, strong) MethodCreator *methodCreator;
@end
@implementation AutoDictionary

@dynamic date;

- (instancetype)init{
    if (self = [super init]) {
        self.backingStore = [NSMutableDictionary dictionary];
        self.methodCreator = [MethodCreator new];
    }
    return self;
}

#pragma mark - 消息轉發機制 :1.動態添加方法 2.後備消息接收者 3.封裝NSInvocation,最後通牒
// 3. 封裝NSInvocation,最後通牒
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
}
// 2. 沒法接受消息,選擇由誰來接受
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.methodCreator;
}
// 1. 動態添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selString = NSStringFromSelector(sel);
    
    if ([selString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictSetter, "");
    }else{
        class_addMethod(self, sel, (IMP)autoDictGetter, "");
    }
    
    return YES;
}

id autoDictGetter (id self, SEL _cmd){
    
    AutoDictionary *dict = self;
    NSString *key = NSStringFromSelector(_cmd);
    return [dict.backingStore objectForKey:key];
}

void autoDictSetter (id self, SEL _cmd, id value){
    
    AutoDictionary *dict = self;
    
    NSString *selString = NSStringFromSelector(_cmd);
    
    NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)];
    
    key = [key lowercaseStringWithLocale:[NSLocale currentLocale]];
    
    if (value) {
        [dict.backingStore setObject:value forKey:key];
    }else{
        [dict.backingStore removeObjectForKey:key];
    }
}

@end

測試代碼:

AutoDictionary *dict = [AutoDictionary new];
dict.date = [NSDate date];
NSLog(@"dict.date = %@",dict.date);

給對象、分類添加實例變量

在開發中有時候想給對象實例添加個變量來存儲數據,但又沒法直接聲明,好比說既有類的分類。這個時候咱們就能夠經過 關聯對象 在運行時給對象關聯一個 對象 來存儲數據。(注意:並非真實的添加了一個實例變量)

關聯對象 能夠給某個對象關聯其餘對象並用key來區分其餘對象。須要注意的是,存儲對象的時候要指明 存儲策略,用來維護對象的內存管理語義。存儲策略是 objc_AssociationPolicy 枚舉定義,如下是存儲策略對應的 @property屬性:

存儲策略類型 對應的@property屬性
OBJC_ASSOCIATION_ASSIGN weak
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong
OBJC_ASSOCIATION_COPY copy

用下面的方法能夠管理關聯對象:

// 這個方法能夠根據指定策略給對象關聯對象值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

// 這個方法能夠獲取對象關聯對象值
id objc_getAssociatedObject(id object, const void *key)

// 這個方法能夠刪除指定對象的所有關聯對象值
void objc_removeAssociatedObjects(id object)

對於關聯對象這個OC特性,咱們能夠把對象想象成一個 NSDictionary,關聯對象須要一個 key( 類型是 opaque pointer,無類型的指針 ) 來區分,咱們能夠把要添加的變量名做爲 key ,把變量的值做爲關聯的對象來存儲到 」對象「 這個 NSDictionary 中。
因此,關聯對象的

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

方法相似於字典的

[dict setObject: forKey:]

方法。

在存儲和獲取關聯對象時須要用一個相等的 key ,由於是給 Class 的實例對象關聯對象,因此通常用靜態變量來作 key

說的再多,不如上段代碼!

好比說,咱們給 NSString 實例加上個 NSDate 類型的 date 變量。什麼?給字符串加個日期變量是要幹嫋?我要給字符串過個生日不行嗎! 別鬧,舉個栗子嘛!(捂臉逃跑~~~)

首先,咱們先給 NSString 新建個名爲 RT 的 category。
在頭文件中有個 NSDate 類型的 date 屬性:

//  NSString+RT.h
//  runtime
#import <Foundation/Foundation.h>

@interface NSString (RT)

@property (nonatomic, strong) NSDate *date;

@end

在分類中的屬性只會生成 getset 方法,並不會生成變量。
因此咱們須要重寫 getset 方法,關聯對象以變相實現添加變量,在現實文件中:

//  NSString+RT.m
//  runtime
#import <objc/runtime.h>
#import "NSString+RT.h"

@implementation NSString (RT)

static void *runtime_date_key = "date";
- (NSDate *)date{
    return objc_getAssociatedObject(self, runtime_date_key);
}

- (void)setDate:(NSDate *)date{
    objc_setAssociatedObject(self, runtime_date_key, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

須要注意的是,關聯對象用到的 key 是個無類型的指針,通常來講是靜態來修飾。
另外,給對象關聯的只能是對象,若是是 intfloat 等類型須要 NSNumber 進行包裝。
由於 date 是強引用和非原子屬性,因此關聯策略用 OBJC_ASSOCIATION_RETAIN_NONATOMIC

而後執行代碼:

NSString *string = @"runtimeTestString";
string.date = [NSDate date];
NSLog(@"string.date = %@",string.date);

輸出結果:

2016-04-12 21:27:31.099 runtime[2837:103727] string.date = 2016-04-12 13:27:31 +0000

注意:

  • 定義關聯對象時須要指定內存管理語義,用來模擬對象對變量的擁有關係

  • 儘可能避免使用關聯對象,由於若是出現bug不易於問題排查

iOS 開發中的 AOP

Objective-C 中,類的方法列表會把選擇器的名稱映射到方法的實現上,這樣 動態消息轉發系統 就能夠以此找到須要調用的方法。這些方法是以函數指針的形式來表示,這種指針叫作 IMP
以下:

id (*IMP) (id, SEL, ...)

Objective-C 的 runtime 機制以此提供了獲取和交換映射IMP的的接口:

// 獲取方法
Method class_getInstanceMethod(Class cls, SEL name);

// 交換兩個方法
void method_exchangeImplementations(Method m1, Method m2)

咱們能夠經過上面兩個方法來進行選擇器和所映射的IMP進行交換:

來,直接上代碼示例,好比咱們的要實現功能是在每一個控制器的viewDidLoad方法裏面log一下,通常有三種實現方式:

  1. 直接修改每一個頁面的 view controller 代碼,簡單粗暴;

  2. 子類化 view controller ,並讓咱們的 view controller 都繼承這些子類;

  3. 使用 Method Swizzling 進行 hook,以達到 AOP 編程的思想

第一種實現的代碼是在每一個類的裏面都這麼寫:

- (void)viewDidLoad {
    [super viewDidLoad];
    DDLog();
}

第二種是隻在基類裏面寫。而後全部的控制器都繼承這個基類。
最後一種是最佳的解決方案:

@implementation UIViewController (Log)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(log_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)log_viewDidLoad{
    [self log_viewDidLoad];
    DDLog(...);
}

@end

注意:

  • 爲何使用 + (void)load ?由於父類、子類和分類的該方法是分別調用,互不影響,並且是在類被加載的時候一定會調用的方法。



本文首發於 https://iosgg.cn/tags/#Objc/runtime
相關文章
相關標籤/搜索