首先,為什麼說ObjC是動態語言
我們看下蘋果官方文件對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.
譯文如下
Objective-C執行時是一個執行時庫,它提供對Objective-C語言的動態屬性的支援,因此被所有Objective-C應用程式連結。 Objective-C執行時庫支援函式在/usr/lib/libobjc.A.dylib中的共享庫中實現。
在Objective-C中,訊息直到執行時才繫結到方法實現。編譯器將把方法呼叫轉化為訊息傳送
例如如下程式碼
[receiver message]
複製程式碼
將會被轉化為這種呼叫方式
objc_msgSend(receiver, selector)
複製程式碼
在訊息需要繫結引數的時候會轉化如下
objc_msgSend(receiver, selector, arg1, arg2, ...)
複製程式碼
那麼抓花為傳送訊息之後都做了什麼呢?
[receiver message]
複製程式碼
- 通過receiver的 isa 指標 查詢它的 Class
- 查詢 Class 下的 methodLists
- 如果 methodLists 沒有相應的方法則遞迴查詢 superClass 的 methodLists
- 如果在 methodLists 裡面找到了 對應的 message 則 獲取實現指標 imp 並執行
- 傳送方法返回值
這裡我們發現還缺少了一種情況,那就是遞迴在父類的methodlist裡面也沒有找到對應的實現,這個時候就會報錯 unrecognized selector send to instance X
訊息轉發
Runtime 為這種可能提供了最後的機會,就是觸發訊息轉發流程
- resolveClassMethod/resolveInstanceMethod ,向物件傳送其無法識別的訊息後會觸發,在這裡可以動態新增方法的實現
- forwardingTargetForSelector ,快速轉發,可以把對應的訊息傳送給其他物件
- methodSignatureForSelector ,對方法進行簽名(為完整的訊息轉發做準備)
- forwardInvocation ,進行完整的訊息轉發(包括修改實際方法,物件等)
- 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
這裡有幾個地方需要特別留意下
- 如果是交換的系統方法,在新的方法內部一定要再呼叫這個方法一次,因為這個時候方法的imp指標已經交換,呼叫該方法就是呼叫系統方法,為什麼要這麼做呢,因為這些系統的方法是黑盒的,有很多我們不清楚的操作,如果不呼叫可能會給程式帶來問題
- 執行交換方法的最佳時機是在類方法
load
中, 該方法會在類被載入的時候執行 - 一定要使用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 有諸多魔幻的使用方法,但是不建議過多的使用(除非掌握的很熟練),除非是開發框架,否則多個互相交換的方法和動態的屬性在除錯的時候會很無奈的。。。