Runtime- 結合Demo, 讓你輕鬆搞定

Runtime.jpeg
關於 Runtime
的學習資料網上有很多了,但是大部分看起來有些晦澀難懂,看過一遍後讓人感覺有些走馬觀花, 還是理解不透 Runtime
.所以趁著這幾天的空閒時間, 我對自己理解的 Runtime
總結了一下,專門寫了一個 Demo
, 主要講一些常用的方法功能,以實用為主,這樣才能更好更快的掌握 Runtime
的特性。結合著 Demo
學習會讓你更快掌握, 搞定後不論是在開發還是面試的時候, 我相信對您的作用會比較大
強烈建議
結合著 Demo
程式碼邊看程式碼邊看文件 iOSer/YTRuntimeDemo" target="_blank" rel="nofollow,noindex">Demo Github連結 。
一.Runtime簡介
我們應該都知道 Objective-C
是一門動態語言,它會將一些工作放在程式碼執行時才處理而並非編譯時。也就是說,有很多類和成員變數在我們編譯的時是不知道的,而在執行時,我們所編寫的程式碼會轉換成完整的確定的程式碼執行。
因此,只靠編譯器是不夠的,我們還需要一個執行時系統( Runtime system
)來處理編譯後的程式碼。
Runtime
即我們通常叫的執行時,也就是程式在執行的時候做的事情。是 Objective-C
底層的一套 C
語言的API,是 iOS
內部的核心之一,我們平時編寫的 Objective-C
程式碼,底層都是基於它來實現的, Objective-C
程式碼編譯後,其實都是 Runtime
形式的 C
語言程式碼。
二.Runtime的作用
1.有些 Objective-C
不好實現的功能, 就可以使用 Runtime
, 比如:
- 動態交換兩個方法的實現(常用於交換系統方法);
- 動態新增物件的成員變數和成員方法;
- 獲得某個類的所有成員變數及方法.
2.有時候專案中遇到很多具體的問題, 就需要使用 Runtime
來實現了,比如:
-
iOS
黑魔法Swizzle
的使用, 多用於攔截系統自帶的方法呼叫,比如攔截imageNamed:、viewDidLoad、alloc等; - 實現分類
Category
中可以增加屬性; - 實現NSCoding的自動歸檔和自動解檔;
- 實現字典和模型的自動轉換.
三.Runtime的使用
上面講的可能讓大家感覺還是不好理解, 比較書面, 下面我結合著具體的 Demo
來詳細上面說到的功能. 強烈建議
結合著 Demo
程式碼邊看程式碼邊看文件 Demo Github連結 .
1. iOS
黑魔法 Swizzle
要使用 Swizzle
, 首先需要引入標頭檔案 <objc/runtime.h>
.
交換兩個方法的實現方法是:
void method_exchangeImplementations(Method m1 , Method m2)
-
交換自定義類的方法實現
建立一個
Man
類, 類中實現下面兩個方法, 同時需要在.h中宣告.
+ (void)eat { NSLog(@"吃"); } + (void)drink { NSLog(@"喝"); }
在使用這個 Man
類的時候, 呼叫方法:
[Man eat]; [Man drink];
打印出來的結果, 會先列印 吃
, 然後列印 喝
.
接下來使用 Swizzle
, 交換兩個方法的實現, 獲取類方法使用 class_getClassMethod
,獲取物件方法使用 class_getInstanceMethod
.
// 獲取兩個類的類方法 Method m1 = class_getClassMethod([Man class], @selector(eat)); Method m2 = class_getClassMethod([Manclass], @selector(drink)); // 開始交換方法實現 method_exchangeImplementations(m1, m2); // 交換後,還是先呼叫 eat,然後呼叫 drink [Man eat]; [Man drink];
打印出來的結果是, 先列印 喝
, 再列印 吃
, 能夠很明顯的看出呼叫的還是這兩個方法, 但方法的實現已經交換.
- 系統方法的攔截交換
比如遇到需求 iOS9 以上的版本需要使用另一套圖片, 這時候需要在一個個使用的地方判斷版本來載入不同的圖片嗎? 這樣會不會太繁瑣呢? 有好的解決方法嗎?
這時候就可以使用 Swizzle
, 來攔截 UIImage
的 imageName
這個載入圖片的系統方法, 來交換成我們自己的方法.
(1) 建立一個 UIImage
的分類:(UIImage+Category);
(2) 在分類中實現一個自定義方法,方法中寫要在系統方法中加入的語句,比如版本判斷修改圖片名;
//自定義方法 + (UIImage *)yt_ImageNamed:(NSString *)name { double version = [[UIDevice currentDevice].systemVersion doubleValue]; if (version >= 9.0) { name = [name stringByAppendingString:@"_ios9"]; } return [UIImage yt_ImageNamed:name]; //方法交換後, 呼叫imageNamed方法, 讓有載入圖片的功能 }
注: 在自定義方法最後需要呼叫系統的 imageNamed
方法, 來實現載入圖片的功能, 因為交換了方法實現, 所以這裡呼叫的是交換後的自定義方法, 其實呼叫的是系統的 imageNamed
方法, 這裡需要想想理解一下.
(3) Category
中重寫 UIImage
的 load
方法,實現方法的交換(只要能讓其執行一次方法交換語句,load再合適不過了)
攔截交換:
+ (void)load { //獲取兩個類的類方法 Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:)); Method m2 = class_getClassMethod([UIImage class], @selector(yt_ImageNamed:)); //開始交換方法實現 method_exchangeImplementations(m1, m2); //注 在使用中, 如果iOS9以上版本使用另一版本的圖片, 就可以交換系統的方法, 直接使用 imageNamed方法, 呼叫的是yt_ImageNamed的實現 }
這樣就實現了攔截交換系統方法的功能, 在專案中遇到類似的問題可以靈活運用.
2.分類 Category
中建立屬性
大家都知道, 一般情況下在 iOS
分類中是無法設定屬性的,如果在分類的宣告中寫 @property
只能為其生成 get
和 set
方法的宣告,但無法生成成員變數,就是雖然點語法能調用出來,但程式執行後會crash.
針對分類中建立屬性, Runtime
可以巧妙的實現,使用一下方法:
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
講需要設定的屬性值繫結到當前類即可, 具體步驟如下:
(1).建立一個分類 Category
,比如給任何一個物件都新增一個 name
屬性,就是 NSObject
新增分類( NSObject+Category
);
(2).先在.h 中 @property
宣告出 get
和 set
方法,方便點語法呼叫;
@interface NSObject (Category) @property (nonatomic, copy) NSString *name; //宣告屬性, 系統生成set和get方法,方便點語法呼叫 @end
(3).在.m 中重寫 name
的 set
和 get
方法,內部利用 Runtime
給屬性賦值和取值.
#import "NSObject+Category.h" #import <objc/runtime.h> //.m中重寫set和get方法, 內部利用runtime給屬性賦值和取值 @implementation NSObject (Category) char nameKey; //用於取值的key //set - (void)setName:(NSString *)name{ //將name值和物件關聯起來, 將name值儲存到當前物件中 /*引數: object: 給哪個物件設定屬性; key: 一個屬性對應一個key, 儲存後需要通過這個key取出值, key可為double,int等任意型別, 建議用char可節省位元組; value: 給屬性設定的值; policy: 儲存策略 (assign, copy, retain); */ objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY); } //get - (NSString *)name{ return objc_getAssociatedObject(self, &nameKey); } @end
3.獲取類的所有成員變數
一個物件在歸檔和解檔的 encodeWithCoder
和 initWithCoder:
方法中需要該物件所有的屬性進行 decodeObjectForKey:
和 encodeObject:
,一般情況下需要對每個屬性都寫歸解檔, 新增或刪除屬性對應也要修改, 十分的不方便, 但是通過 Runtime
我們宣告中無論寫多少個屬性,都不需要再修改實現中的程式碼了。
(1)比如一個 Person
類,需要對它的成員變數進行歸解檔, 步驟如下:
- 通過
runtime
獲取當前所有成員變數名, 然後獲取到各個變數值, 以變數名為key
進行歸檔:
//歸檔 - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; //獲取所有成員變數 unsigned int outCount = 0; /* 引數: 1.哪個類 2.接收值的地址, 用於存放屬性的個數 3.返回值: 存放所有獲取到的屬性, 可調出名字和型別 */ Ivar *ivarArray = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i++) { Ivar ivar = ivarArray[i]; //將每個成員變數名轉換為NSString物件型別 NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //忽略不需要歸檔的屬性 if ([[self ignoredNames] containsObject:key]) { continue; //跳過本次迴圈 } //通過成員變數名, 取出成員變數的值 id value = [self valueForKey:key]; //再把值歸檔 [coder encodeObject:value forKey:key]; //這兩部就相當於 [coder encodeObject: @(self.name) forKey:@"_name"]; } free(ivarArray); }
- 通過
runtime
獲取到所有成員變數名, 以變數名為key
解檔取出值:
//解檔 - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { //獲取所有成員變數 unsigned int outCount = 0; Ivar *ivarArray = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i++) { Ivar ivar = ivarArray[i]; //獲取每個成員變數名並轉換為NSString物件型別 NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //忽略不需要解檔的屬性 if ([[self ignoredNames] containsObject:key]) { continue; } //根據變數名解檔取值, 無論是什麼型別 id value = [coder decodeObjectForKey:key]; //取出的值再設定給屬性 [self setValue:value forKey:key]; //這兩步相當於以前的 self.name = [coder decodeObjectForKey:@"_name"]; } free(ivarArray); //釋放記憶體 } return self; }
以上就實現了利用 runtime
進行歸解檔, 比之前一個個變數進行方便了很多, 但是在實際的運用中, 如果遇到一個類需要歸解檔就這樣寫, 多個需要重複寫, 這時候可以 在 NSObject
的分類中時間歸解檔, 這樣各個類使用時候只需要簡單的幾句就可以實現, 步驟如下:
(1).為 NSObject
建立分類, 並在 .h 中宣告歸解檔的方法, 便於子類的使用;
@interface NSObject (Extension) - (NSArray *)ignoredNames; - (void)encode:(NSCoder *)aCoder; //重寫方法, 避免覆蓋系統方法 - (void)decode:(NSCoder *)aDecoder; @end
(2)歸檔:
- (void)encode:(NSCoder *)aCoder{ //一層層父類往上查詢, 對父類的屬性執行歸解檔方法 Class c = self.class; while (c && c != [NSObject class]) { unsigned int outCount = 0; Ivar *ivarArray = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i++) { Ivar ivar = ivarArray[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //如果有實現該方法再去呼叫 if ([self respondsToSelector:@selector(ignoredNames)]) { if ([[self ignoredNames] containsObject:key]) { continue; } } id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; //歸檔 } free(ivarArray); c = [c superclass]; //向上查詢父類 } }
(3).解檔:
- (void)decode:(NSCoder *)aDecoder{ Class c = self.class; while (c && c != [NSObject class]) { unsigned int outCount = 0; Ivar *ivarAaary = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i++) { Ivar ivar = ivarAaary[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(ivarAaary); c = [c superclass]; } }
上面的程式碼宣告的方法, 我換了一個方法名(不然會覆蓋系統原來的方法!),同時加了一個忽略屬性方法是否被實現的判斷,便於在使用時候對不需要進行歸解檔的屬性進行判斷, 同時還加上了對父類屬性的歸解檔迴圈。
這樣再使用之後只需要簡單的幾行程式碼就可以實現歸解檔, 例如對 Cat
類進行歸解檔:
@implementation Car //設定需要忽略的屬性 - (NSArray *)ignoredNames{ return @[@"head"]; } //在系統方法中呼叫自定義方法 - (instancetype)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { [self decode:coder]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [self encode:coder]; } @end
4.字典轉模型
一般我們都是使用 KVC
進行字典轉模型,但是它還是有一定的侷限性,例如:模型屬性和鍵值對對應不上會crash(雖然可以重寫 setValue:forUndefinedKey:
方法防止報錯),模型屬性是一個物件或者陣列時不好處理等問題,所以無論是效率還是功能上,利用 runtime
進行字典轉模型都是比較好的選擇.
字典轉模型我們需要考慮三種特殊情況:
1.字典的key和模型的屬性匹配不上;
2.模型中巢狀模型(模型屬性是另外一個模型物件);
3.陣列中裝著模型(模型的屬性是一個數組,陣列中是一個個模型物件).
針對上面的三種特殊情況,我們一個個詳解下處理過程.
(1).先是字典的 key
和模型的屬性不對應的情況。
不對應的情況有兩種,一種是字典的鍵值大於模型屬性數量,這時候我們不需要任何處理,因為 runtime
是先遍歷模型所有屬性,再去字典中根據屬性名找對應值進行賦值,多餘的鍵值對也當然不會去看了;另外一種是模型屬性數量大於字典的鍵值對,這時候由於屬性沒有對應值會被賦值為 nil
,就會導致 crash
,我們只需加一個判斷即可,程式碼如下:
- (void)setDict:(NSDictionary *)dict { 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)]; // 成員變數名轉為屬性名(去掉下劃線 _ ) key = [key substringFromIndex:1]; // 取出字典的值 id value = dict[key]; // 如果模型屬性數量大於字典鍵值對數理,模型屬性會被賦值為nil而報錯,這時候判斷值是nil的話, 忽略這個模型的屬性即可. if (value == nil) continue; // 將字典中的值設定到模型上 [self setValue:value forKeyPath:key]; } free(ivars); c = [c superclass]; } }
(2).模型屬性是另外一個模型物件的情況, 這時候我們就需要利用 runtime
的 ivar_getTypeEncoding
方法獲取模型物件型別,對該模型物件型別再進行字典轉模型,也就是進行遞迴,需要注意的是我們要排除系統的物件型別,例如NSString,下面的方法中我添加了一個類方法方便遞迴。
#import "NSObject+JSONExtension.h" #import <objc/runtime.h> @implementation NSObject (JSONExtension) - (void)setDict:(NSDictionary *)dict { 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)]; // 成員變數名轉為屬性名(去掉下劃線 _ ) key = [key substringFromIndex:1]; // 取出字典的值 id value = dict[key]; // 如果模型屬性數量大於字典鍵值對數理,模型屬性會被賦值為nil而報錯 if (value == nil) continue; // 獲得成員變數的型別 NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; // 如果屬性是物件型別 NSRange range = [type rangeOfString:@"@"]; if (range.location != NSNotFound) { // 那麼擷取物件的名字(比如@"Dog",擷取為Dog) type = [type substringWithRange:NSMakeRange(2, type.length - 3)]; // 排除系統的物件型別 if (![type hasPrefix:@"NS"]) { // 將物件名轉換為物件的型別,將新的物件字典轉模型(遞迴) Class class = NSClassFromString(type); value = [class objectWithDict:value]; } } // 將字典中的值設定到模型上 [self setValue:value forKeyPath:key]; } free(ivars); c = [c superclass]; } } + (instancetype )objectWithDict:(NSDictionary *)dict { NSObject *obj = [[self alloc]init]; [obj setDict:dict]; return obj; }
(3).第三種情況是模型的屬性是一個數組,陣列中是一個個模型物件,我們既然能獲取到屬性型別,那就可以攔截到模型的那個陣列屬性,進而對陣列中每個資料遍歷並字典轉模型,但是我們不知道陣列中的模型都是什麼型別,我們可以宣告一個方法,該方法目的不是讓其呼叫,而是讓其實現並返回陣列中模型的型別, 這樣就可以對陣列中的資料進行字典轉模型.
在分類中聲明瞭 arrayObjectClass
方法, 子類呼叫返回陣列中模型的型別即可.
@interface NSObject (JSONExtension) - (void)setDict: (NSDictionary *)dict; + (instancetype)objectWithDict: (NSDictionary *)dict; //告訴陣列中都是什麼型別的模型物件 - (NSString *)arrayObjectClass; @end
然後進行字典轉模型:
#import "NSObject+JSONExtension.h" #import <objc/runtime.h> @implementation NSObject (JSONExtension) - (void)setDict:(NSDictionary *)dict{ Class c = self.class; while (c && c != [NSObject class]) { unsigned int outCount = 0; Ivar *ivarArray = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i++) { Ivar ivar = ivarArray[i]; NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //成員變數名轉為屬性名(去掉下劃線_) key = [key substringFromIndex:1]; //取出字典的值 id value = dict[key]; //如果模型屬性數量大於字典鍵值對數量,則key對應dict中沒有值, 模型屬性會被賦值為nil而報錯 if (value == nil) { continue; } //獲得成員變數的型別 NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; //如果屬性是物件型別 NSRange range = [type rangeOfString:@""]; if (range.location != NSNotFound) { //那麼擷取物件的名字(比如@"Dog", 擷取為Dog) type = [type substringWithRange:NSMakeRange(2, type.length - 3)]; //排除系統的物件型別 if (![type hasPrefix:@"NS"]) { //將物件名轉換為物件的型別, 將新的物件字典轉模型(遞迴) Class class = NSClassFromString(type); value = [class objectWithDict:value]; }else if ([type isEqualToString:@"NSArray"]){ //如果是陣列型別, 將陣列中的每個模型進行字典轉模型 NSArray *array = (NSArray *)value; NSMutableArray *mArray = [NSMutableArray array];//先建立一個臨時陣列存放模型 //獲取到每個模型的型別 id class; if ([self respondsToSelector:@selector(arrayObjectClass)]) { NSString *classStr = [self arrayObjectClass]; class = NSClassFromString(classStr); }else{ NSLog(@"陣列內模型是未知型別"); return; } //將陣列中的所有模型進行字典轉模型 for (int i = 0; i < array.count; i++) { [mArray addObject:[class objectWithDict:value[i]]]; } value = mArray; } } //將字典中的值設定到模型上 [self setValue:value forKey:key]; } } } + (instancetype)objectWithDict:(NSDictionary *)dict{ NSObject *obj = [[self alloc] init]; [obj setDict:dict]; return obj; } @end
以上介紹了幾點 Runtime
的特性, 並結合我們開發中可能遇到的情況就行講解, 這樣大家可以更好的理解, 建議大家對照著我的 Demo 詳細看下, 自己也試一試, 只有自己動手才能真正的理解.
有什麼問題可以隨時給我留言, 我看到後會第一時間回覆, 如果看完文章感覺對您有所幫忙的話, 不妨關注喜歡下哦, 看 demo
時候麻煩也 star
下!!!