Runtime 概念
runtime(簡稱運行時),是一套純C(C和匯編寫的) 的API。而 OC 就是運行時機制(消息機制)。
在編譯階段,OC 調用并未實現的函數,只要聲明過就不會報錯,只有當運行的時候才會報錯,這是因為OC是運行時動態調用的。而C語言,函數的調用在編譯的時候會決定調用哪個函數,調用未實現的函數就會報錯
runtime 消息機制
OC方法調用本質:就是用 runtime發送一個消息,每一個 OC 的方法底層必然有一個與之對應的 runtime 方法.
消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現。
- 例子:
創建一個macos工程,就在main.m里寫下面簡單的代碼
Dog *dog = [[Dog alloc] init];
[dog run];
1. 導入 #import <objc/message.h>,因為這個里面包含下面兩個
#include <objc/objc.h>
#include <objc/runtime.h>
2.去到 build setting -> 搜索msg ->將Enable Strict Checking of objc_msgSend Calls 改為no
否則使用 objc_msgSend 編譯出錯,因為xcode默認不建議使用
3.去到main.m所在的目錄,在終端用下面命令編譯一下
clang -rewrite-objc main.m
就會生成一個main.cpp文件
4.打開該文件看最下面main方法,可以看到編譯后的代碼就是runtime
- 使用:
objc_msgSend(id self, SEL op, ...)
參數:oc對象,方法編號,其他參數...
Dog *dog = [[Dog alloc] init];
[dog run];
可以寫成下面的
//Class 類類型 就是一個特殊的對象
Dog *dog = objc_msgSend([Dog class], @selector(alloc));
dog = objc_msgSend(dog, @selector(init));
objc_msgSend(dog, @selector(run));
//
// 底層的實際寫法
Dog *dog = objc_msgSend(objc_getClass("Dog"),sel_registerName("alloc"));
dog = objc_msgSend(dog, sel_registerName("init"));
objc_msgSend(dog, @selector(run));
- 消息機制方法調用流程
對象方法:(保存到類對象的方法列表) ,類方法:(保存到元類(Meta Class)中方法列表)。
OC 在向一個對象發送消息時,runtime 庫會根據對象的 isa指針找到該對象對應的類或其父類中根據方法編號(SEL)去查找對應方法,找到只是最終函數實現地址(IMP),根據地址去方法區調用對應函數。
補充:每一個對象內部都有一個isa指針,這個指針是指向它的真實類型,根據這個指針就能知道將來調用哪個類的方法。
runtime 使用場景
- 動態交換兩個方法的實現(method swizzling)HOOK思想
需求:給系統的imageNamed添加額外功能(是否加載圖片成功)
方案一:繼承系統的類,重寫方法.(弊端:每次使用都需要導入)
方案二:搞個分類,定義一個能加載圖片并且能打印的方法(弊端:不能在分類中重寫系統方法imageNamed,因為會把系統的功能給覆蓋掉,而且分類中不能調用super,所以要 自己實現一個帶有擴展功能的方法.但這樣就得改調用的方法,改動大)
runtime方式實現步驟:
1.給UIImageView添加分類
2.自定義并實現帶有擴展功能的方法
3.交換方法
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"123"];
}
#import <objc/message.h>
@implementation UIImage (Image)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 獲取方法地址
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 交換方法地址
if (!class_addMethod([self class], @selector(ln_imageNamed:), method_getImplementation(ln_imageNamedMethod), method_getTypeEncoding(ln_imageNamedMethod))) {
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
});
}
// 自己定義的方法
+ (UIImage *)ln_imageNamed:(NSString *)name {
UIImage *image = [UIImage ln_imageNamed:name];
if (image) {
NSLog(@"load image success");
} else {
NSLog(@"load image failed");
}
return image;
}
@end
上面代碼執行過程,會先執行load方法,這個時候imageNamed:和ln_imageNamed:就交換了,走到viewDidLoad的 [UIImage imageNamed:@"123"] 時,實際上執行的是ln_imageNamed:,ln_imageNamed:里面又調用ln_imageNamed:,實際上調用的是imageNamed:,這樣就根據imageNamed:的返回值來判斷。
說明以及注意事項:
- 方法交換為什么寫在load方法
load 把類加載進內存的時候調用,只會調用一次 - 為了避免Swizzling的代碼被重復執行(調用[super load]),利用dispatch_once函數內代碼只會執行一次的特性。
- class_getClassMethod(獲取某個類的方法)
class_getInstanceMethod (獲取某個對象的方法) - IMP本質上就是函數指針,所以我們可以通過打印函數地址的方式,查看SEL和IMP的交換流程
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
NSLog(@"%p", method_getImplementation(imageNamedMethod));
NSLog(@"%p", method_getImplementation(ln_imageNamedMethod));
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
- 使用class_addMethod()函數對Method Swizzling做了一層驗證,如果self沒有實現被交換的方法,會導致失敗。而且self沒有交換的方法實現,但是父類有這個方法(或者自己有這個方法),這樣就會調用父類的方法,結果就不是我們想要的結果了。所以我們在這里通過class_addMethod()的驗證,如果self實現了這個方法,class_addMethod()函數將會返回NO,我們就可以對其進行交換了
- runtime結合kvc實現NSCoding的自動歸檔和解檔
如果一個模型有許多個屬性,那么我們需要對每個屬性都實現一遍encodeObject 和 decodeObjectForKey方法,如果這樣的模型又有很多個,就非常麻煩。
- 原來的做法
遵守協議NSCoding
@property (nonatomic, copy) NSString *name;
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:_Name forKey:@"name"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.movieName = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
- 新做法(主要代碼)
//解檔
- (void)decode:(NSCoder *)aDecoder {
// 一層層父類往上查找,對父類的屬性執行歸解檔方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有實現該方法再去調用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
// 歸檔
- (void)encode:(NSCoder *)aCoder {
// 一層層父類往上查找,對父類的屬性執行歸解檔方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
// 獲取成員變量的名字
const char *name = ivar_getName(ivar);
//// C字符串 -> OC字符串
NSString *key = [NSString stringWithUTF8String:name];
// 如果有實現該方法再去調用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [self valueForKeyPath:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
- 動態添加方法
如果一個類方法非常多,因為需要給每個方法生成映射表,實際上只要一個類實現了某個方法,就會被加載進內存,加載類到內存的時候就比較耗費資源。當硬件內存過小的時候,如果我們將每個方法都直接加到內存當中去,但是很久都不用一次,這樣就造成了浪費,那如果我想像懶加載一樣,先把方法定義好,但是只有當你用的時候我再加載你,這就需要動態添加了。
當performSelector方法調用某個sel的時候,這時候會到調用對象的+ (BOOL)resolveInstanceMethod:(SEL)sel方法中,如果這里返回是NO,就表示找不到。
- 看下面的例子
// 動態添加方法就不會報錯
Person * p = [[Person alloc] init];
[p performSelector:@selector(eat:) withObject:@"吃過了"];
//下面代碼在Person.m里
#import <objc/runtime.h>
void addEat(id self, SEL _cmd, NSString *str) {
NSLog(@"%@", str);
}
// 任何方法默認都有兩個隱式參數,self,_cmd(當前方法的方法編號)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// [NSStringFromSelector(sel) isEqualToString:@"run"];
if (sel == NSSelectorFromString(@"eat:")) {
BOOL isSuccess = class_addMethod(self, sel, (IMP)addEat, "v@:@");
return isSuccess;
}
return [super resolveInstanceMethod:sel];
}
- class_addMethod參數解釋(可以command+shift+0查看官方文檔)
class_addMethod(Class cls, SEL name, IMP imp,const char *types)
- class: 給哪個類添加方法
- SEL: 添加方法的方法編號
- IMP: 方法實現 (添加方法的函數實現(函數地址))
- type: 方法類型,(返回值+參數類型)
(1) v 返回值類型是void
(2)@ 對象->self
(3): 表示SEL->_cmd
(4)@ 第四個參數
- resolveInstanceMethod的作用
當調用了沒有實現的方法沒有實現就會調用,然后就可以根據他的參數sel(參數sel就是沒有實現的方法)來做一系列的操作。
4.給分類添加屬性
在分類中,所寫的@property (nonatomic, strong) NSString *name;都僅僅是生成了get和set方法,并沒有生成對應的_name屬性,但是有時候我們會有一種需求,想要讓分類中保存一下新的屬性值,因為set和get方法只能是對已經有的東西做操作,比如說最常用的UIView的分類我們對frame中的x,y,width,height做操作。
//給Person添加一個分類addProperty
//在Person+addProperty.h中
@property (nonatomic, strong) NSString *name;
//在Person+addProperty.m中
#import <objc/message.h>
@implementation Person (addProperty)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, @"name");
}
- (void)viewDidLoad {
[super viewDidLoad];
//給分類動態添加屬性
Person * p1 = [[Person alloc] init];
p1.name = @"這是給分類添加的屬性";
NSLog(@"%@",p1.name);
}
解釋:
objc_setAssociatedObject方法
/**
* 根據某個對象,還有key,還有對應的策略(copy,strong等) 動態的將值設置到這個對象的key上
* @param object 某個對象
* @param key 屬性名,根據key去獲取關聯的對象
* @param value 要設置的值
* @param policy 策略(copy,strong,assign等)
*/
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
objc_getAssociatedObject方法
/**
* 根據某個對象,還有key 動態的獲取到這個對象的key對應的屬性的值
* @param object 某個對象
* @param key key
* @return 對象的值
*/
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
4.實現字典轉模型的自動轉換
字典轉模型KVC實現會有很多弊端,利用運行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。
1.當字典的key和模型的屬性匹配不上。
2.模型中嵌套模型(模型屬性是另外一個模型對象)。
3.數組中裝著模型(模型的屬性是一個數組,數組中是一個個模型對象)。
注解:根據上面的三種特殊情況,先是字典的key和模型的屬性不對應的情況。不對應有兩種,一種是字典的鍵值大于模型屬性數量,這時候我們不需要任何處理,因為runtime是先遍歷模型所有屬性,再去字典中根據屬性名找對應值進行賦值,多余的鍵值對也當然不會去看了;另外一種是模型屬性數量大于字典的鍵值對,這時候由于屬性沒有對應值會被賦值為nil,就會導致crash,我們只需加一個判斷即可。考慮三種情況下面一一注解;
步驟:提供一個NSObject分類,專門字典轉模型,以后所有模型都可以通過這個分類實現字典轉模型。
MJExtension 字典轉模型實現,底層也是對 runtime 的封裝。
注:本文參考 http://www.lxweimin.com/p/19f280afcb24
更全面的例子參考 https://github.com/lizelu/ObjCRuntimeDemo