Runtime系列三:Runtime在項目中使用場景

前言:

關于Runtime的資料網上一搜很多,但總是寫的只言片語,不太全面。最近花了一個星期的時間重新學習Runtime,并整理了一個系列文章,并發表出來,同時也感謝開源貢獻的開發者。這里共有三篇文章:

Runtime系列一:Runtime的前世今生

Runtime系列二:Runtime的原理

Runtime系列三:Runtime在項目中使用場景

一、方法交換Swizzling

使用場景:系統自帶的方法功能不夠,給系統自帶的方法擴展一些功能,并且保持原有的功能。

方式一:繼承系統的類,重寫方法.

方式二:使用runtime,交換方法.

在Objective-C中調用一個方法,其實是向一個對象發送消息,而查找消息的唯一依據是selector的名字。所以,我們可以利用Objective-C的runtime機制,實現在運行時交換selector對應的方法實現以達到我們的目的。每個類都有一個方法列表,存放著selector的名字和方法實現的映射關系。IMP有點類似函數指針,指向具體的Method實現

我們先看看SEL與IMP之間的關系圖:


交換圖1


從上圖可以看出來,每一個SEL與一個IMP一一對應,正常情況下通過SEL可以查找到對應消息的IMP實現。

但是,現在我們要做的就是把鏈接線解開,然后連到我們自定義的函數的IMP上。當然,交換了兩個SEL的IMP,還是可以再次交換回來了。交換后變成這樣的,如下圖


交換圖2

從圖中可以看出,我們通過swizzling特性,將selectorC的方法實現IMPc與selectorN的方法實現IMPn交換了,當我們調用selectorC,也就是給對象發送selectorC消息時,所查找到的對應的方法實現就是IMPn而不是IMPc了。

#import"UIViewController+swizzling.h"

#import@implementationUIViewController(swizzling)//load方法會在類第一次加載的時候被調用//調用的時間比較靠前,適合在這個方法里做方法交換

+ (void)load{//方法交換應該被保證,在程序中只會執行一次staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{

//獲得viewController的生命周期方法的selector

SEL systemSel =@selector(viewWillAppear:);

//自己實現的將要被交換的方法的selector

SEL swizzSel =@selector(swiz_viewWillAppear:);

//兩個方法的Method

Method ? systemMethod = class_getInstanceMethod([selfclass], systemSel); ? ??

? Method swizzMethod = class_getInstanceMethod([selfclass], swizzSel);

//首先動態添加方法,實現是被交換的方法,返回值表示添加成功還是失敗BOOLisAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));

if(isAdd) {

//如果成功,說明類中不存在這個方法的實現//將被交換方法的實現替換到這個并不存在的實現class_replaceMethod(self,swizzSel,method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));? ? ? ? }

else{

//否則,交換兩個方法的實現method_exchangeImplementations(systemMethod, swizzMethod);? ? ? ? }? ? });}

- (void)swiz_viewWillAppear:(BOOL)animated{/

/這時候調用自己,看起來像是死循環//但是其實自己的實現已經被替換了

[self ?swiz_viewWillAppear:animated];

NSLog(@"swizzle");}

@end

在一個自己定義的viewController中重寫viewWillAppear

- (void)viewWillAppear:(BOOL)animated{

? ? [super viewWillAppear:animated];? ?

? ? NSLog(@"viewWillAppear");

}

二、設置關聯值

使用場景:現在你準備用一個系統的類,但是系統的類并不能滿足你的需求,你需要額外添加一個屬性。給一個類聲明屬性,其實本質就是給這個類添加關聯,并不是直接把這個值的內存空間添加到類存空間。分類只能添加方法

1.設置關聯值

這種情況的一般解決辦法就是繼承。

但是,只增加一個屬性,就去繼承一個類,總是覺得太麻煩類。

這個時候,runtime的關聯屬性就發揮它的作用了。

1、添加關聯對象

-(void)addAssociatedObject:(id)object{objc_setAssociatedObject(self,@selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}//獲取關聯對象-(id)getAssociatedObject{returnobjc_getAssociatedObject(self, _cmd);}

注意:這里面我們把getAssociatedObject方法的地址作為唯一的key,_cmd代表當前調用方法的地址。

參數說明:

object:與誰關聯,通常是傳self

key:唯一鍵,在獲取值時通過該鍵獲取,通常是使用static

const void *來聲明

value:關聯所設置的值

policy:內存管理策略,比如使用copy

voidobjc_setAssociatedObject(idobject,constvoid*key, idvalue, objc _AssociationPolicy policy)

2.獲取關聯值

參數說明:

object:與誰關聯,通常是傳self,在設置關聯時所指定的與哪個對象關聯的那個對象

key:唯一鍵,在設置關聯時所指定的鍵

idobjc_getAssociatedObject(idobject,constvoid*key)

3.取消關聯

voidobjc_removeAssociatedObjects(idobject)

關聯策略

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){OBJC_ASSOCIATION_ASSIGN =0,// 表示弱引用關聯,通常是基本數據類型OBJC_ASSOCIATION_RETAIN_NONATOMIC =1,// 表示強引用關聯對象,是線程安全的OBJC_ASSOCIATION_COPY_NONATOMIC =3,// 表示關聯對象copy,是線程安全的OBJC_ASSOCIATION_RETAIN =01401,// 表示強引用關聯對象,不是線程安全的OBJC_ASSOCIATION_COPY =01403// 表示關聯對象copy,不是線程安全的};

@implementationViewController

- (void)viewDidLoad {?

? [super viewDidLoad];// Do any additional setup after loading the view, typically from a nib.

// 給系統NSObject類動態添加屬性name

NSObject*objc = [[NSObjectalloc] init];?

? objc.name =@"123";NSLog(@"%@",objc.name);}

@end

// 定義關聯的key static const char*key ="name";

@implementationNSObject(Property)

- (NSString*)name{// 根據關聯的key,獲取關聯的值。returnobjc_getAssociatedObject(self, key);}

- (void)setName:(NSString*)name{

// 第一個參數:給哪個對象添加關聯/

/ 第二個參數:關聯的key,通過這個key獲取

// 第三個參數:關聯的value

// 第四個參數:關聯的策略objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}

@end

三、動態添加方法

使用場景:如果一個類方法非常多,加載類到內存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。

@implementationViewController

- (void)viewDidLoad {?

? [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.

Person *p = [[Person alloc] init];// 默認person,沒有實現eat方法,可以通過performSelector調用,但是會報錯。

// 動態添加方法就不會報錯[p performSelector:@selector(eat)];}

@end

@implementationPerson//

void(*)()// 默認方法都有兩個隱式參數,

void eat(idself,SEL sel){NSLog(@"%@ %@",self,NSStringFromSelector(sel));}

// 當一個對象調用未實現的方法,會調用這個方法處理,并且會把對應的方法列表傳過來.// 剛好可以用來判斷,未實現的方法是不是我們想要動態添加的方法

+ (BOOL)resolveInstanceMethod:(SEL)sel{

if(sel ==@selector(eat)) {

// 動態添加eat方法// 第一個參數:給哪個類添加方法

// 第二個參數:添加方法的方法編號

// 第三個參數:添加方法的函數實現(函數地址)

// 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmdclass_addMethod(self,@selector(eat), eat,"v@:");? ? }return[superresolveInstanceMethod:sel];}

@end


四、字典轉模型

設計模型

模型屬性,通常需要跟字典中的key一一對應

問題:一個一個的生成模型屬性,很慢?

需求:能不能自動根據一個字典,生成對應的屬性。

解決:提供一個分類,專門根據字典生成對應的屬性字符串。

@implementation ?NSObject(Log)// 自動打印屬性字符串

+ (void)resolveDict:(NSDictionary*)dict{

// 拼接屬性字符串代碼NSMutableString*strM = [NSMutableString string];

// 1.遍歷字典,把字典中的所有key取出來,生成對應的屬性代碼

[dict enumerateKeysAndObjectsUsingBlock:^(id_Nonnull key,id_Nonnull obj,BOOL* _Nonnull stop) {

// 類型經常變,抽出來

NSString*type;

if([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {? ?

? ? ? ? type =@"NSString";? ? ? ? }

else if([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){?

? ? ? ? ? type =@"NSArray";? ? ? ? }

else if([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){? ? ? ?

? ? type =@"int";? ? ? ? }

else if([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){? ? ? ?

? ? type =@"NSDictionary";? ? ? ? }

// 屬性字符串NSString*str;

if([type containsString:@"NS"]) {? ?

? str = [NSStringstringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];? ? ? ? }

else{? ? ? ? ? ?

str = [NSStringstringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];? ? ? ? }

// 每生成屬性字符串,就自動換行。

[strM appendFormat:@"\n%@\n",str];? ? }];

// 把拼接好的字符串打印出來,就好了。NSLog(@"%@",strM);}

@end

字典轉模型的方式一:KVC

@implementation Status

+ (instancetype)statusWithDict:(NSDictionary *)dict{

?Status*status= [[self alloc] init];?

? [status setValuesForKeysWithDictionary:dict];

return ?status;

}

@end

KVC字典轉模型弊端:必須保證,模型中的屬性和字典中的key一一對應。

如果不一致,就會調用[ setValue:forUndefinedKey:]

報key找不到的錯。

分析:模型中的屬性和字典的key不一一對應,系統就會調用setValue:forUndefinedKey:報錯。

解決:重寫對象的setValue:forUndefinedKey:,把系統的方法覆蓋,

就能繼續使用KVC,字典轉模型了。

-(void)setValue:(id)valueforUndefinedKey:(NSString*)key{}

字典轉模型的方式二:Runtime

思路:利用運行時,遍歷模型中所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。

步驟:提供一個NSObject分類,專門字典轉模型,以后所有模型都可以通過這個分類轉。

@implementation ?ViewController

- (void)viewDidLoad {? ?

[superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.//

解析Plist文件

NSString*filePath = [[NSBundlemainBundle]pathForResource:@"status.plist"ofType:nil];

NSDictionary*statusDict = [NSDictionary ?dictionaryWithContentsOfFile:filePath];

// 獲取字典數組NSArray*dictArr = statusDict[@"statuses"];

// 自動生成模型的屬性字符串//?

? [NSObject resolveDict:dictArr[0][@"user"]];

_statuses = [NSMutableArray array];/

/ 遍歷字典數組

for(NSDictionary*dictindict Arr)

{? ? ? ? Status *status = [Status modelWithDict:dict];? ? ?

? [_statuses addObject:status];? ?

}

// 測試數據NSLog(@"%@ %@",_statuses,[_statuses[0] user]);}

@end

@implementation ?NSObject(Model)

+ (instancetype)modelWithDict:(NSDictionary*)dict

{

// 思路:遍歷模型中所有屬性-》使用運行時

// 0.創建對應的對象id objc = [[selfalloc] init];

// 1.利用runtime給對象中的成員屬性賦值

// class_copyIvarList:獲取類中的所有成員屬性

// Ivar:成員屬性的意思

// 第一個參數:表示獲取哪個類中的成員屬性

// 第二個參數:表示這個類有多少成員屬性,傳入一個Int變量地址,會自動給這個變量賦值

// 返回值Ivar *:指的是一個ivar數組,會把所有成員屬性放在一個數組中,通過返回的數組就能全部獲取到。/* 類似下面這種寫法

Ivar ivar;

Ivar ivar1;

Ivar ivar2;

// 定義一個ivar的數組a

Ivar a[] = {ivar,ivar1,ivar2};

// 用一個Ivar *指針指向數組第一個元素

Ivar *ivarList = a;

// 根據指針訪問數組第一個元素

ivarList[0];

*/unsignedintcount;

// 獲取類中的所有成員屬性Ivar *ivarList = class_copyIvarList(self, &count);

for(inti =0; i < count; i++) {

// 根據角標,從數組取出對應的成員屬性Ivar ivar = ivarList[i];

// 獲取成員屬性名

NSString*name = [NSString stringWithUTF8String:ivar_getName(ivar)];

// 處理成員屬性名->字典中的key

// 從第一個角標開始截取

NSString*key = [name substringFromIndex:1];

// 根據成員屬性名去字典中查找對應的value

id value = dict[key];

// 二級轉換:如果字典中還有字典,也需要把對應的字典轉換成模型

// 判斷下value是否是字典

if([value isKindOfClass:[NSDictionaryclass]]) {

// 字典轉模型// 獲取模型的類對象,調用modelWithDict

// 模型的類名已知,就是成員屬性的類型

// 獲取成員屬性類型

NSString*type = [NSString ?stringWithUTF8String:ivar_getTypeEncoding(ivar)];

// 生成的是這種@"@\"User\"" 類型 -》 @"User"? 在OC字符串中 \" -> ",\是轉義的意思,不占用字符// 裁剪類型字符串NSRangerange = [type rangeOfString:@"\""];? ? ?

? ? type = [type substringFromIndex:range.location + range.length];? ? ?

? ? ? range = [type rangeOfString:@"\""];// 裁剪到哪個角標,不包括當前角標type = [type substringToIndex:range.location];

// 根據字符串類名生成類對象

Class modelClass =NSClassFromString(type);

if(modelClass) {

// 有對應的模型才需要轉

// 把字典轉模型value? =? [modelClass modelWithDict:value];? ? ? ? ? ? }? ? ? ? }

// 三級轉換:NSArray中也是字典,把數組中的字典轉換成模型

// 判斷值是否是數組

if([value isKindOfClass:[NSArrayclass]]) {

// 判斷對應類有沒有實現字典數組轉模型數組的協議

if([self respondsToSelector:@selector(arrayContainModelClass)]) {/

/ 轉換成id類型,就能調用任何對象的方法id

id Self =self;

// 獲取數組中字典對應的模型

NSString*type =? [id ?Self arrayContainModelClass][key];

// 生成模型

Class classModel =NSClassFromString(type);

NSMutableArray*arrM = [NSMutableArray ?array];

// 遍歷字典數組,生成模型數組for(NSDictionary*dict in value) {

// 字典轉模型id model =? [classModel modelWithDict:dict];? ? ? ? ? ? ?

? ?[arrM addObject:model];? ? ? ? ? ? ? ? }

// 把模型數組賦值給value ?value = arrM;? ? ? ? ? ? }? ? ? ? }

if(value) {

// 有值,才需要給模型的屬性賦值

// 利用KVC給模型中的屬性賦值[objc setValue:value forKey:key];? ? ? ? }? ?

}return ? objc;}

@end

五、參考文章

http://www.lxweimin.com/p/e071206103a4

http://www.lxweimin.com/p/adf0d566c887

http://www.lxweimin.com/p/927c8384855a

http://chun.tips/2014/11/05/objc-runtime-1/#more

http://blog.sunnyxx.com/2016/08/13/reunderstanding-runtime-0/

http://blog.csdn.net/wzzvictory/article/details/8624057

http://www.cocoachina.com/ios/20151208/14595.html

http://www.lxweimin.com/p/46dd81402f63

六、后記

說實話,剛開始做開發是后,也一直聽說runtime,但項目中用的少,直到最近項目不是太忙,才重新看看蘋果的runtime機制,有一種驀然回首,茅塞頓開的感覺,學會runtime,一是可以更好的幫助你理解OC的運行機制。還有一點就是可以裝-B

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容