概述
接上篇《Runtime基本原理及Demo》,了解了Runtime相關(guān)基礎(chǔ)知識(shí)后,我們談一下Runtime常見(jiàn)的幾個(gè)應(yīng)用場(chǎng)景。
Runtime常見(jiàn)應(yīng)用場(chǎng)景
具體應(yīng)用攔截系統(tǒng)自帶的方法調(diào)用(Method Swizzling黑魔法)
實(shí)現(xiàn)給分類增加屬性
實(shí)現(xiàn)字典的模型和自動(dòng)轉(zhuǎn)換
JSPatch替換已有的OC方法實(shí)行等
一、Method Swizzling
Method Swizzling是改變一個(gè)已存在的selector的實(shí)現(xiàn)的技術(shù)验靡”侗觯可以使用它來(lái)在Runtime通過(guò)修改類的分發(fā)表中selector對(duì)應(yīng)的函數(shù),來(lái)修改selector的實(shí)現(xiàn)晴叨。
我們常用Method Swizzling來(lái)將系統(tǒng)的方法換為我們自定義的方法凿宾,給系統(tǒng)方法添加一些需要的功能,來(lái)實(shí)現(xiàn)某些需求兼蕊。例如初厚,跟蹤程序每個(gè)ViewController展示給用戶的次數(shù),可以通過(guò)Method Swizzling替換ViewDidAppear初始方法孙技。再例如更換全局UILabel默認(rèn)字體产禾,可以通過(guò)Method Swizzling替換UILabel初始方法來(lái)修改等。
實(shí)例
因?yàn)镸ethod Swizzling的實(shí)現(xiàn)模式比較固定牵啦,所以將其抽象為一個(gè)分類亚情,可以直接方便調(diào)用.
NSObject Category
#import@interfaceNSObject (Swizzling)+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector;@end#import"NSObject+Swizzling.h"#import@implementationNSObject (Swizzling)+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector? ? ? ? ? ? ? ? ? ? ? ? bySwizzledSelector:(SEL)swizzledSelector{? ? Classclass= [selfclass];Method originalMethod = class_getInstanceMethod(class,originalSelector);Method swizzleMethod = class_getInstanceMethod(class,swizzledSelector);BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));if(didAddMethod) {? ? ? ? class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));? ? }else{? ? ? ? method_exchangeImplementations(originalMethod, swizzleMethod);? ? }}@end
class_addMethod:實(shí)現(xiàn)會(huì)覆蓋父類的方法實(shí)現(xiàn)。但不會(huì)取代本類中已存在的實(shí)現(xiàn)哈雏,如果本類中包含一個(gè)同名的實(shí)現(xiàn)楞件,則函數(shù)返回NO。這里為源SEL添加IMP是為了避免源SEL沒(méi)有實(shí)現(xiàn)IMP的情況裳瘪。若添加成功則說(shuō)明源SEL沒(méi)有實(shí)現(xiàn)IMP土浸,將源SEL的IMP替換到交換SEL的IMP;若添加失敗則說(shuō)明源SEL已經(jīng)有IMP了彭羹,直接將兩個(gè)SEL的IMP交換就可以了黄伊。
class_replaceMethod:該函數(shù)的行為可以分為兩種:如果類中不存在name指定的方法,則類似于clss_addMethod函數(shù)一樣會(huì)添加方法派殷;如果類中已存在name指定的方法还最,則類似于method_setImplementation一樣代替原方法的實(shí)現(xiàn)。
實(shí)現(xiàn)第一個(gè)場(chǎng)景:跟蹤程序每個(gè)ViewController展示給用戶的次數(shù)毡惜,可以通過(guò)Method Swizzling替換ViewDidAppear初始方法拓轻。
創(chuàng)建一個(gè)UIViewController的分類,重寫(xiě)自定義的ViewDidAppear方法经伙,并在其+load方法中實(shí)現(xiàn)ViewDidAppear方法的交換扶叉。
#import@interfaceUIViewController(Swizzling)@end#import"UIViewController+Swizzling.h"#import"NSObject+Swizzling.h"@implementationUIViewController(Swizzling)+(void)load{staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? ? [UIViewControllermethodSwizzlingWithOriginalSelector:@selector(viewDidAppear:)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? bySwizzledSelector:@selector(my_ViewDidAppear:)];? ? });? ? }-(void) my_ViewDidAppear:(BOOL)animated{? ? [selfmy_ViewDidAppear:animated];NSLog(@"===== %@ viewDidAppear=====",[selfclass]);}@end
這里有幾個(gè)注意點(diǎn):
Swizzling應(yīng)該總在+load中執(zhí)行
在OC中,Runtime會(huì)在類初始加載時(shí)調(diào)用+load方法橱乱,在類第一次被調(diào)用時(shí)實(shí)現(xiàn)+initialize方法。由于Method Swizzling會(huì)影響到類的全局狀態(tài)粱甫,所以要盡量避免在并發(fā)處理中出現(xiàn)競(jìng)爭(zhēng)情況泳叠。+load方法能保證在類的初始化過(guò)程中被加載,并保證這種改變應(yīng)用級(jí)別的行為的一致性茶宵。
要使用dispatch_once執(zhí)行方法交換
方法交換要求線程安全危纫,而且保證在任何情況下只能交換一次。
二、實(shí)現(xiàn)給分類增加屬性
開(kāi)發(fā)中常需要在不改變某個(gè)類的前提下為其添加一個(gè)新的屬性种蝶,尤其是為系統(tǒng)的類添加新的屬性契耿,這個(gè)時(shí)候就可以利用Runtime的關(guān)聯(lián)對(duì)象(Associated Objects)來(lái)為分類添加新的屬性了。
關(guān)聯(lián)對(duì)象是Runtime的一個(gè)特性螃征,Runtime中定義了三個(gè)允許你講將任何鍵值在Runtime關(guān)聯(lián)到對(duì)象上的函數(shù):
objc_setAssociatedObject
設(shè)置關(guān)聯(lián)對(duì)象:
voidobjc_setAssociatedObject(idobject,constvoid*key, idvalue, objc_AssociationPolicy policy)
object:需要設(shè)置關(guān)聯(lián)對(duì)象的對(duì)象
key:關(guān)聯(lián)對(duì)象的key搪桂,推薦使用selector
value:關(guān)聯(lián)對(duì)象的值,id類型
policy:關(guān)聯(lián)對(duì)象的策略盯滚,屬性可以根據(jù)定義在枚舉類型objc_AssociationPolicy上的行為被關(guān)聯(lián)在對(duì)象上踢械。類似于@property創(chuàng)建時(shí)設(shè)置的關(guān)鍵字。
objc_getAssociatedObject
獲取關(guān)聯(lián)對(duì)象
idobjc_getAssociatedObject(idobject,constvoid*key)
objc_removeAssociatedObjects
移除某個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象魄藕,此方法不常用内列。
通過(guò)提供的方法我們就可以對(duì)存在的類在拓展中添加自定義的屬性了。
實(shí)例:
為UIImage類添加一個(gè)downLoadURL的屬性背率。
#import@interfaceUIImage(downLoadURL)@property(nonatomic,strong)NSString*downLoadURL;@end#import"UIImage+downLoadURL.h"#import@implementationUIImage(downLoadURL)-(void)setDownLoadURL:(NSString*)downLoadURL{? ? objc_setAssociatedObject(self,@selector(downLoadURL), downLoadURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}-(NSString*)downLoadURL{returnobjc_getAssociatedObject(self,@selector(downLoadURL));}@end
有名的第三方庫(kù)LTNavigationBar在實(shí)現(xiàn)滑動(dòng)界面時(shí)導(dǎo)航欄顯隱功能時(shí)也使用了Associated Objects话瞧,為UINavigation類添加了一個(gè)UIView類的屬性overlay,使功能實(shí)現(xiàn)起來(lái)更加簡(jiǎn)便寝姿。大家可以閱讀一下源碼交排。
LTNavigationBar
三、實(shí)現(xiàn)字典的模型和自動(dòng)轉(zhuǎn)換
優(yōu)秀的JSON轉(zhuǎn)模型第三方庫(kù)JSONModel会油、YYModel等都利用runtime對(duì)屬性進(jìn)行獲取个粱,賦值等操作,要比KVC進(jìn)行模型轉(zhuǎn)換更加強(qiáng)大翻翩,更有效率都许。
閱讀YYModel的源碼可以看出,YY大神對(duì)NSObject的內(nèi)容進(jìn)行了又一次封裝嫂冻,添加了許多描述內(nèi)容胶征。其中YYClassInfo是對(duì)Class進(jìn)行了再次封裝,而YYClassIvarInfo桨仿、YYClassMethodInfo睛低、YYClPropertyInfo分別是對(duì)Class的Ivar、Method和property進(jìn)行了封裝和描述服傍。在提取Class的相關(guān)信息時(shí)都運(yùn)用了Runtime钱雷。
源碼中提取Class的Ivar的相關(guān)代碼:
unsignedintivarCount =0;? ? Ivar *ivars = class_copyIvarList(cls, &ivarCount);if(ivars) {? ? ? ? NSMutableDictionary *ivarInfos = [NSMutableDictionarynew];? ? ? ? _ivarInfos = ivarInfos;for(unsignedinti =0; i < ivarCount; i++) {? ? ? ? ? ? YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];if(info.name) ivarInfos[info.name] = info;? ? ? ? }free(ivars);? ? }
其中的運(yùn)用的Runtime相關(guān)知識(shí)在上篇已說(shuō)明,不再贅述吹零。
為了方便大家更好的理解字典轉(zhuǎn)模型罩抗,我粗略的寫(xiě)了一個(gè)極簡(jiǎn)陋版的轉(zhuǎn)模型方案供大家理解其思路,更優(yōu)秀完美的思路建議大家閱讀優(yōu)秀第三方框架的源碼灿椅。
@interfaceNSObject(ZCModel)+(instancetype) setModelWithDict:(NSDictionary*)dict;@end@implementationNSObject(ZCModel)+(instancetype) setModelWithDict:(NSDictionary*)dict{? ? Class cls = [selfclass];idModel = [[selfalloc]init];unsignedintcount;//提取Class的property列表objc_property_t *property_t = class_copyPropertyList(cls, &count);//遍歷列表套蒂,對(duì)每個(gè)property分別處理for(inti =0; i< count; i++) {? ? ? ? objc_property_t property = property_t[i];NSString*key = [NSStringstringWithUTF8String:property_getName(property)];idvalue = dict[key];if(!value)continue;//提取property的attribute信息NSString*attribure = [NSStringstringWithUTF8String:property_getAttributes(property)];//從attribute信息中提取其class的信息if([attribure hasPrefix:@"T@"]) {NSRangerange =? [attribure rangeOfString:@"\","];NSString*typeString = [attribure substringWithRange:NSMakeRange(3, range.location? -3)];NSLog(@"the property class is %@",typeString);//對(duì)非NS開(kāi)頭的class處理為嵌套的model钞支,對(duì)model進(jìn)行遞歸,轉(zhuǎn)為模型if(![typeString hasPrefix:@"NS"]) {? ? ? ? ? ? ? ? Class modelClass =NSClassFromString(typeString);? ? ? ? ? ? ? ? value = [modelClass setModelWithDict:value];? ? ? ? ? ? }? ? ? ? }//將字典中的值設(shè)置給模型[Model setValue:value forKeyPath:key];? ? }? ? free(property_t);returnModel;}@end
四操刀、JSPatch替換已有的OC方法實(shí)行
JSPatch是個(gè)優(yōu)秀開(kāi)源項(xiàng)目烁挟,只需要在項(xiàng)目里引入極小的引擎文件就可以使用JavaScript調(diào)用任何OC的原生接口,替換任意的OC原生方法骨坑。目前主要用于下發(fā)JS腳本替換原生OC代碼撼嗓,實(shí)時(shí)修復(fù)線上bug,更多詳情可以閱讀JSPatch技術(shù)文檔卡啰。
JSPatch能做到通過(guò)JS調(diào)用和改寫(xiě)OC方法最根本的原因是OC是動(dòng)態(tài)語(yǔ)言静稻,OC上所有方法的調(diào)用和類的生成都通過(guò)OC Runtime在運(yùn)行時(shí)進(jìn)行,我們可以通過(guò)類名/方法名反射得到相應(yīng)的類和方法匈辱。理論上你可以在運(yùn)行時(shí)通過(guò)類名/方法名調(diào)用任何OC方法振湾,替換任何類的實(shí)現(xiàn)以及新增任意類,所以JSPatch的基本原理是:JS傳遞字符串給OC亡脸,OC通過(guò)Runtime接口調(diào)用和替換OC方法押搪。這是最基礎(chǔ)的原理。
在JSPatch實(shí)現(xiàn)方法替換上浅碾,通過(guò)Selector調(diào)用方法時(shí)大州,會(huì)從methodList鏈表里找到對(duì)應(yīng)的Method進(jìn)行調(diào)用,這個(gè)methodList上的元素是可以動(dòng)態(tài)替換的垂谢,可以把某個(gè)Selector對(duì)應(yīng)的函數(shù)指針I(yè)MP替換成新的厦画,也可以拿到已有的某個(gè)Selector對(duì)應(yīng)的函數(shù)指針I(yè)MP,讓另一個(gè)Selector跟它對(duì)應(yīng)滥朱,Runtime提供了相應(yīng)的方法實(shí)現(xiàn)這些根暑。
總結(jié)
通過(guò)以上四個(gè)實(shí)例可以了解到Runtime在我們實(shí)際開(kāi)發(fā)中常用的幾個(gè)場(chǎng)景,Runtime強(qiáng)大的功能是一把雙刃劍徙邻,一方面它涉及許多底層實(shí)現(xiàn)排嫌,如果使用不善很可能造成許多問(wèn)題,所以在開(kāi)發(fā)中要謹(jǐn)慎仔細(xì)的使用缰犁;但另一方面淳地,Runtime底層功能讓我們可以實(shí)現(xiàn)類的動(dòng)態(tài)配置、消息傳遞和方法替換等功能帅容,實(shí)現(xiàn)項(xiàng)目需求颇象,提高開(kāi)發(fā)效率,更好的理解OC這門(mén)動(dòng)態(tài)語(yǔ)言并徘。
Runtime知識(shí)體系龐大遣钳,本文僅拋磚引玉,希望對(duì)大家有所幫助饮亏。
參考文章
Objective-C Runtime 運(yùn)行時(shí)之一:類與對(duì)象