關(guān)于
Runtime
的學習資料網(wǎng)上有很多了,但是大部分看起來有些晦澀難懂,看過一遍后讓人感覺有些走馬觀花, 還是理解不透Runtime
.所以趁著這幾天的空閑時間, 我對自己理解的Runtime
總結(jié)了一下鸟废,專門寫了一個Demo
, 主要講一些常用的方法功能佩微,以實用為主旅敷,這樣才能更好更快的掌握Runtime
的特性绞佩。結(jié)合著Demo
學習會讓你更快掌握, 搞定后不論是在開發(fā)還是面試的時候, 我相信對您的作用會比較大
強烈建議
結(jié)合著Demo
代碼邊看代碼邊看文檔 Demo Github鏈接。
一.Runtime簡介
我們應(yīng)該都知道 Objective-C
是一門動態(tài)語言丑掺,它會將一些工作放在代碼運行時才處理而并非編譯時获印。也就是說,有很多類和成員變量在我們編譯的時是不知道的街州,而在運行時兼丰,我們所編寫的代碼會轉(zhuǎn)換成完整的確定的代碼運行。
因此唆缴,只靠編譯器是不夠的鳍征,我們還需要一個運行時系統(tǒng)(Runtime system
)來處理編譯后的代碼。
Runtime
即我們通常叫的運行時面徽,也就是程序在運行的時候做的事情艳丛。是 Objective-C
底層的一套C
語言的API,是 iOS
內(nèi)部的核心之一匣掸,我們平時編寫的 Objective-C
代碼,底層都是基于它來實現(xiàn)的氮双,Objective-C
代碼編譯后碰酝,其實都是Runtime
形式的C
語言代碼。
二.Runtime的作用
1.有些Objective-C
不好實現(xiàn)的功能, 就可以使用Runtime
, 比如:
- 動態(tài)交換兩個方法的實現(xiàn)(常用于交換系統(tǒng)方法);
- 動態(tài)添加對象的成員變量和成員方法;
- 獲得某個類的所有成員變量及方法.
2.有時候項目中遇到很多具體的問題, 就需要使用Runtime
來實現(xiàn)了,比如:
-
iOS
黑魔法Swizzle
的使用, 多用于攔截系統(tǒng)自帶的方法調(diào)用,比如攔截imageNamed:戴差、viewDidLoad送爸、alloc等; - 實現(xiàn)分類
Category
中可以增加屬性; - 實現(xiàn)NSCoding的自動歸檔和自動解檔暖释;
- 實現(xiàn)字典和模型的自動轉(zhuǎn)換.
三.Runtime的使用
上面講的可能讓大家感覺還是不好理解, 比較書面, 下面我結(jié)合著具體的
Demo
來詳細上面說到的功能.強烈建議
結(jié)合著Demo
代碼邊看代碼邊看文檔 Demo Github鏈接.
1.iOS
黑魔法 Swizzle
要使用Swizzle
, 首先需要引入頭文件 <objc/runtime.h>
.
交換兩個方法的實現(xiàn)方法是:
void method_exchangeImplementations(Method m1 , Method m2)
- 交換自定義類的方法實現(xiàn)
創(chuàng)建一個Man
類, 類中實現(xiàn)下面兩個方法, 同時需要在.h中聲明.
+ (void)eat {
NSLog(@"吃");
}
+ (void)drink {
NSLog(@"喝");
}
在使用這個Man
類的時候, 調(diào)用方法:
[Man eat];
[Man drink];
打印出來的結(jié)果, 會先打印吃
, 然后打印 喝
.
接下來使用Swizzle
, 交換兩個方法的實現(xiàn), 獲取類方法使用class_getClassMethod
袭厂,獲取對象方法使用class_getInstanceMethod
.
// 獲取兩個類的類方法
Method m1 = class_getClassMethod([Man class], @selector(eat));
Method m2 = class_getClassMethod([Manclass], @selector(drink));
// 開始交換方法實現(xiàn)
method_exchangeImplementations(m1, m2);
// 交換后,還是先調(diào)用 eat,然后調(diào)用 drink
[Man eat];
[Man drink];
打印出來的結(jié)果是, 先打印 喝
, 再打印吃
, 能夠很明顯的看出調(diào)用的還是這兩個方法, 但方法的實現(xiàn)已經(jīng)交換.
- 系統(tǒng)方法的攔截交換
比如遇到需求 iOS9 以上的版本需要使用另一套圖片, 這時候需要在一個個使用的地方判斷版本來加載不同的圖片嗎? 這樣會不會太繁瑣呢? 有好的解決方法嗎?
這時候就可以使用Swizzle
, 來攔截UIImage
的 imageName
這個加載圖片的系統(tǒng)方法, 來交換成我們自己的方法.
(1) 創(chuàng)建一個UIImage
的分類:(UIImage+Category);
(2) 在分類中實現(xiàn)一個自定義方法球匕,方法中寫要在系統(tǒng)方法中加入的語句纹磺,比如版本判斷修改圖片名;
//自定義方法
+ (UIImage *)yt_ImageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 9.0) {
name = [name stringByAppendingString:@"_ios9"];
}
return [UIImage yt_ImageNamed:name]; //方法交換后, 調(diào)用imageNamed方法, 讓有加載圖片的功能
}
注: 在自定義方法最后需要調(diào)用系統(tǒng)的
imageNamed
方法, 來實現(xiàn)加載圖片的功能, 因為交換了方法實現(xiàn), 所以這里調(diào)用的是交換后的自定義方法, 其實調(diào)用的是系統(tǒng)的imageNamed
方法, 這里需要想想理解一下.
(3) Category
中重寫 UIImage
的 load
方法,實現(xiàn)方法的交換(只要能讓其執(zhí)行一次方法交換語句谐丢,load再合適不過了)
攔截交換:
+ (void)load {
//獲取兩個類的類方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(yt_ImageNamed:));
//開始交換方法實現(xiàn)
method_exchangeImplementations(m1, m2); //注 在使用中, 如果iOS9以上版本使用另一版本的圖片, 就可以交換系統(tǒng)的方法, 直接使用 imageNamed方法, 調(diào)用的是yt_ImageNamed的實現(xiàn)
}
這樣就實現(xiàn)了攔截交換系統(tǒng)方法的功能, 在項目中遇到類似的問題可以靈活運用.
2.分類Category
中創(chuàng)建屬性
大家都知道, 一般情況下在
iOS
分類中是無法設(shè)置屬性的,如果在分類的聲明中寫@property
只能為其生成get
和set
方法的聲明蚓让,但無法生成成員變量乾忱,就是雖然點語法能調(diào)用出來,但程序執(zhí)行后會crash.
針對分類中創(chuàng)建屬性, Runtime
可以巧妙的實現(xiàn),使用一下方法:
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
講需要設(shè)置的屬性值綁定到當前類即可, 具體步驟如下:
(1).創(chuàng)建一個分類Category
历极,比如給任何一個對象都添加一個name
屬性窄瘟,就是NSObject
添加分類(NSObject+Category
);
(2).先在.h 中 @property
聲明出 get
和 set
方法,方便點語法調(diào)用;
@interface NSObject (Category)
@property (nonatomic, copy) NSString *name; //聲明屬性, 系統(tǒng)生成set和get方法,方便點語法調(diào)用
@end
(3).在.m 中重寫name
的 set
和 get
方法趟卸,內(nèi)部利用 Runtime
給屬性賦值和取值.
#import "NSObject+Category.h"
#import <objc/runtime.h>
//.m中重寫set和get方法, 內(nèi)部利用runtime給屬性賦值和取值
@implementation NSObject (Category)
char nameKey; //用于取值的key
//set
- (void)setName:(NSString *)name{
//將name值和對象關(guān)聯(lián)起來, 將name值存儲到當前對象中
/*參數(shù):
object: 給哪個對象設(shè)置屬性;
key: 一個屬性對應(yīng)一個key, 存儲后需要通過這個key取出值, key可為double,int等任意類型, 建議用char可節(jié)省字節(jié);
value: 給屬性設(shè)置的值;
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:
蹄葱,一般情況下需要對每個屬性都寫歸解檔, 添加或刪除屬性對應(yīng)也要修改, 十分的不方便, 但是通過Runtime
我們聲明中無論寫多少個屬性,都不需要再修改實現(xiàn)中的代碼了锄列。
(1)比如一個 Person
類,需要對它的成員變量進行歸解檔, 步驟如下:
- 通過
runtime
獲取當前所有成員變量名, 然后獲取到各個變量值, 以變量名為key
進行歸檔:
//歸檔
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
//獲取所有成員變量
unsigned int outCount = 0;
/*
參數(shù):
1.哪個類
2.接收值的地址, 用于存放屬性的個數(shù)
3.返回值: 存放所有獲取到的屬性, 可調(diào)出名字和類型
*/
Ivar *ivarArray = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivarArray[i];
//將每個成員變量名轉(zhuǎn)換為NSString對象類型
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
//忽略不需要歸檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue; //跳過本次循環(huán)
}
//通過成員變量名, 取出成員變量的值
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];
//獲取每個成員變量名并轉(zhuǎn)換為NSString對象類型
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
//忽略不需要解檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
//根據(jù)變量名解檔取值, 無論是什么類型
id value = [coder decodeObjectForKey:key];
//取出的值再設(shè)置給屬性
[self setValue:value forKey:key];
//這兩步相當于以前的 self.name = [coder decodeObjectForKey:@"_name"];
}
free(ivarArray); //釋放內(nèi)存
}
return self;
}
以上就實現(xiàn)了利用 runtime
進行歸解檔, 比之前一個個變量進行方便了很多, 但是在實際的運用中, 如果遇到一個類需要歸解檔就這樣寫, 多個需要重復(fù)寫, 這時候可以 在 NSObject
的分類中時間歸解檔, 這樣各個類使用時候只需要簡單的幾句就可以實現(xiàn), 步驟如下:
(1).為 NSObject
創(chuàng)建分類, 并在 .h 中聲明歸解檔的方法, 便于子類的使用;
@interface NSObject (Extension)
- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder; //重寫方法, 避免覆蓋系統(tǒng)方法
- (void)decode:(NSCoder *)aDecoder;
@end
(2)歸檔:
- (void)encode:(NSCoder *)aCoder{
//一層層父類往上查找, 對父類的屬性執(zhí)行歸解檔方法
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)];
//如果有實現(xiàn)該方法再去調(diào)用
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];
}
}
上面的代碼聲明的方法, 我換了一個方法名(不然會覆蓋系統(tǒng)原來的方法M荚啤),同時加了一個忽略屬性方法是否被實現(xiàn)的判斷邻邮,便于在使用時候?qū)Σ恍枰M行歸解檔的屬性進行判斷, 同時還加上了對父類屬性的歸解檔循環(huán)竣况。
這樣再使用之后只需要簡單的幾行代碼就可以實現(xiàn)歸解檔, 例如對 Cat
類進行歸解檔:
@implementation Car
//設(shè)置需要忽略的屬性
- (NSArray *)ignoredNames{
return @[@"head"];
}
//在系統(tǒng)方法中調(diào)用自定義方法
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
[self decode:coder];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[self encode:coder];
}
@end
4.字典轉(zhuǎn)模型
一般我們都是使用
KVC
進行字典轉(zhuǎn)模型,但是它還是有一定的局限性筒严,例如:模型屬性和鍵值對對應(yīng)不上會crash(雖然可以重寫setValue:forUndefinedKey:
方法防止報錯)丹泉,模型屬性是一個對象或者數(shù)組時不好處理等問題,所以無論是效率還是功能上鸭蛙,利用runtime
進行字典轉(zhuǎn)模型都是比較好的選擇.
字典轉(zhuǎn)模型我們需要考慮三種特殊情況:
1.字典的key和模型的屬性匹配不上;
2.模型中嵌套模型(模型屬性是另外一個模型對象);
3.數(shù)組中裝著模型(模型的屬性是一個數(shù)組摹恨,數(shù)組中是一個個模型對象).
針對上面的三種特殊情況,我們一個個詳解下處理過程.
(1).先是字典的 key
和模型的屬性不對應(yīng)的情況娶视。
不對應(yīng)的情況有兩種晒哄,一種是字典的鍵值大于模型屬性數(shù)量,這時候我們不需要任何處理,因為 runtime
是先遍歷模型所有屬性揩晴,再去字典中根據(jù)屬性名找對應(yīng)值進行賦值勋陪,多余的鍵值對也當然不會去看了;另外一種是模型屬性數(shù)量大于字典的鍵值對硫兰,這時候由于屬性沒有對應(yīng)值會被賦值為nil
诅愚,就會導(dǎo)致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)];
// 成員變量名轉(zhuǎn)為屬性名(去掉下劃線 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型屬性數(shù)量大于字典鍵值對數(shù)理劫映,模型屬性會被賦值為nil而報錯,這時候判斷值是nil的話, 忽略這個模型的屬性即可.
if (value == nil) continue;
// 將字典中的值設(shè)置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
(2).模型屬性是另外一個模型對象的情況, 這時候我們就需要利用 runtime
的ivar_getTypeEncoding
方法獲取模型對象類型违孝,對該模型對象類型再進行字典轉(zhuǎn)模型,也就是進行遞歸泳赋,需要注意的是我們要排除系統(tǒng)的對象類型雌桑,例如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)];
// 成員變量名轉(zhuǎn)為屬性名(去掉下劃線 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型屬性數(shù)量大于字典鍵值對數(shù)理校坑,模型屬性會被賦值為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)];
// 排除系統(tǒng)的對象類型
if (![type hasPrefix:@"NS"]) {
// 將對象名轉(zhuǎn)換為對象的類型千诬,將新的對象字典轉(zhuǎn)模型(遞歸)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}
}
// 將字典中的值設(shè)置到模型上
[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).第三種情況是模型的屬性是一個數(shù)組耍目,數(shù)組中是一個個模型對象,我們既然能獲取到屬性類型,那就可以攔截到模型的那個數(shù)組屬性徐绑,進而對數(shù)組中每個數(shù)據(jù)遍歷并字典轉(zhuǎn)模型邪驮,但是我們不知道數(shù)組中的模型都是什么類型,我們可以聲明一個方法傲茄,該方法目的不是讓其調(diào)用毅访,而是讓其實現(xiàn)并返回數(shù)組中模型的類型, 這樣就可以對數(shù)組中的數(shù)據(jù)進行字典轉(zhuǎn)模型.
在分類中聲明了 arrayObjectClass
方法, 子類調(diào)用返回數(shù)組中模型的類型即可.
@interface NSObject (JSONExtension)
- (void)setDict: (NSDictionary *)dict;
+ (instancetype)objectWithDict: (NSDictionary *)dict;
//告訴數(shù)組中都是什么類型的模型對象
- (NSString *)arrayObjectClass;
@end
然后進行字典轉(zhuǎn)模型:
#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)];
//成員變量名轉(zhuǎn)為屬性名(去掉下劃線_)
key = [key substringFromIndex:1];
//取出字典的值
id value = dict[key];
//如果模型屬性數(shù)量大于字典鍵值對數(shù)量,則key對應(yīng)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)];
//排除系統(tǒng)的對象類型
if (![type hasPrefix:@"NS"]) {
//將對象名轉(zhuǎn)換為對象的類型, 將新的對象字典轉(zhuǎn)模型(遞歸)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}else if ([type isEqualToString:@"NSArray"]){
//如果是數(shù)組類型, 將數(shù)組中的每個模型進行字典轉(zhuǎn)模型
NSArray *array = (NSArray *)value;
NSMutableArray *mArray = [NSMutableArray array];//先創(chuàng)建一個臨時數(shù)組存放模型
//獲取到每個模型的類型
id class;
if ([self respondsToSelector:@selector(arrayObjectClass)]) {
NSString *classStr = [self arrayObjectClass];
class = NSClassFromString(classStr);
}else{
NSLog(@"數(shù)組內(nèi)模型是未知類型");
return;
}
//將數(shù)組中的所有模型進行字典轉(zhuǎn)模型
for (int i = 0; i < array.count; i++) {
[mArray addObject:[class objectWithDict:value[i]]];
}
value = mArray;
}
}
//將字典中的值設(shè)置到模型上
[self setValue:value forKey:key];
}
}
}
+ (instancetype)objectWithDict:(NSDictionary *)dict{
NSObject *obj = [[self alloc] init];
[obj setDict:dict];
return obj;
}
@end
以上介紹了幾點Runtime
的特性, 并結(jié)合我們開發(fā)中可能遇到的情況就行講解, 這樣大家可以更好的理解, 建議大家對照著我的 Demo 詳細看下, 自己也試一試, 只有自己動手才能真正的理解.
有什么問題可以隨時給我留言, 我看到后會第一時間回復(fù), 如果看完文章感覺對您有所幫忙的話, 不妨關(guān)注喜歡下哦, 看 demo
時候麻煩也 star
下!!!