Runtime 总结
參考文章
1. `文/滕先洪(簡(jiǎn)書(shū)作者)原文鏈接:http://www.jianshu.com/p/ab966e8a82e2著作權(quán)歸作者所有,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),并標(biāo)注“簡(jiǎn)書(shū)作者”[下載地址] (https://github.com/XHTeng/XHRuntimeDemo)`2.`http://www.code4app.com/forum.php?mod=viewthread&tid=8241&highlight=runtime`什么是runtime
runtime 是 OC底層的一套C語(yǔ)言的API(引入<objc/runtime.h> 或<objc/message.h> ),編譯器最終都會(huì)將OC代碼轉(zhuǎn)化為運(yùn)行時(shí)代碼,通過(guò)終端命令編譯.m 文件:clang -rewrite-objc xxx.m可以看到編譯后的xxx.cpp(C++文件)。
- RunTime簡(jiǎn)稱(chēng)運(yùn)行時(shí),就是系統(tǒng)在運(yùn)行的時(shí)候的一些機(jī)制,其中最主要的是消息機(jī)制。
- 對(duì)于C語(yǔ)言,函數(shù)的調(diào)用在編譯的時(shí)候會(huì)決定調(diào)用哪個(gè)函數(shù),編譯完成之后直接順序執(zhí)行,無(wú)任何二義性。
- OC的函數(shù)調(diào)用成為消息發(fā)送。屬于動(dòng)態(tài)調(diào)用過(guò)程。在編譯的時(shí)候并不能決定真正調(diào)用哪個(gè)函數(shù)(事實(shí)證明,在編 譯階段,OC可以調(diào)用任何函數(shù),即使這個(gè)函數(shù)并未實(shí)現(xiàn),只要申明過(guò)就不會(huì)報(bào)錯(cuò)。而C語(yǔ)言在編譯階段就會(huì)報(bào)錯(cuò))。
- 只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱(chēng)找 到對(duì)應(yīng)的函數(shù)來(lái)調(diào)用。
- 我們寫(xiě)的oc代碼,它在運(yùn)行的時(shí)候也是轉(zhuǎn)換成了runtime方式運(yùn)行的,更好的理解runtime,也能幫我們更深的掌握oc語(yǔ)言。
- 每一個(gè)oc的方法,底層必然有一個(gè)與之對(duì)應(yīng)的runtime方法。
- 當(dāng)我們用OC寫(xiě)下這樣一段代碼[tableView cellForRowAtIndexPath:indexPath];
- 在編譯時(shí)RunTime會(huì)將上述代碼轉(zhuǎn)化成[發(fā)送消息]objc_msgSend(tableView, @selector(cellForRowAtIndexPath,indexPath);
runtime的作用
具體一點(diǎn)就是
案例匯總
案例一:方法簡(jiǎn)單的交換
需要用到的方法 <objc/runtime.h>
- 獲得某個(gè)類(lèi)的類(lèi)方法
Method class_getClassMethod(Class cls , SEL name) - 獲得某個(gè)類(lèi)的實(shí)例對(duì)象方法
Method class_getInstanceMethod(Class cls , SEL name) - 交換兩個(gè)方法的實(shí)現(xiàn)
void method_exchangeImplementations(Method m1 , Method m2)
創(chuàng)建一個(gè)Person類(lèi),類(lèi)中實(shí)現(xiàn)以下兩個(gè)類(lèi)方法,并在.h 文件中聲明
+ (void)run { NSLog(@"跑"); }+ (void)study { NSLog(@"學(xué)習(xí)"); }調(diào)用方法,并通過(guò)runtime實(shí)現(xiàn)方法交換
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{person *aperson = [[person alloc]init];[person run]; [person study];// 獲取兩個(gè)類(lèi)的類(lèi)方法 Method m1 = class_getClassMethod([person class], @selector(run)); Method m2 = class_getClassMethod([person class], @selector(study)); // 開(kāi)始交換方法實(shí)現(xiàn) method_exchangeImplementations(m1, m2); // 交換后,先打印學(xué)習(xí),再打印跑! [person run]; [person study];}打印結(jié)果為:
2016-07-11 14:10:28.033 runtime demo[37610:2393684] 跑 2016-07-11 14:10:28.034 runtime demo[37610:2393684] 學(xué)習(xí) 2016-07-11 14:10:28.034 runtime demo[37610:2393684] 學(xué)習(xí) 2016-07-11 14:10:28.034 runtime demo[37610:2393684] 跑案例二:攔截并替換方法
需求:比如iOS6 升級(jí) iOS7 后需要版本適配,根據(jù)不同系統(tǒng)使用不同樣式圖片(擬物化和扁平化),如何通過(guò)不去手動(dòng)一個(gè)個(gè)修改每個(gè)UIImage的imageNamed:方法就可 以實(shí)現(xiàn)為該方法中加入版本判斷語(yǔ)句?
步驟:
1、為UIImage建一個(gè)分類(lèi)(UIImage+Category)
2、在分類(lèi)中實(shí)現(xiàn)一個(gè)自定義方法,方法中寫(xiě)要在系統(tǒng)方法中加入的語(yǔ)句,比如版本判斷
3、分類(lèi)中重寫(xiě)UIImage的load方法,實(shí)現(xiàn)方法的交換(只要能讓其執(zhí)行一次方法交換語(yǔ)句,load再合適不過(guò)了)
+ (void)load { // 獲取兩個(gè)類(lèi)的類(lèi)方法 Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:)); Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:)); // 開(kāi)始交換方法實(shí)現(xiàn) method_exchangeImplementations(m1, m2); }注意:自定義方法中最后一定要再調(diào)用一下系統(tǒng)的方法,讓其有加載圖片的功能,但是由于方法交換,系統(tǒng)的方法名已經(jīng)變成了我們自定義的方法名(有點(diǎn)繞,就是用我們的名字能調(diào)用系統(tǒng)的方法,用系統(tǒng)的名字能調(diào)用我們的方法),這就實(shí)現(xiàn)了系統(tǒng)方法的攔截!
利用以上思路,我們還可以給 NSObject 添加分類(lèi),統(tǒng)計(jì)創(chuàng)建了多少個(gè)對(duì)象,給控制器添加分類(lèi),統(tǒng)計(jì)有創(chuàng)建了多少個(gè)控制器,特別是公司需求總變的時(shí)候,在一些原有控件或模塊上添加一個(gè)功能,建議使用該方法!
案例三:在分類(lèi)中設(shè)置屬性,給任何一個(gè)對(duì)象設(shè)置屬性
眾所周知,分類(lèi)中是無(wú)法設(shè)置屬性的,如果在分類(lèi)的聲明中寫(xiě)@property 只能為其生成get 和 set 方法的聲明,但無(wú)法生成成員變量,就是雖然點(diǎn)語(yǔ)法能調(diào)用出來(lái),但程序執(zhí)行后會(huì)crash,有人會(huì)想到使用全局變量呢?比如這樣:
int _age;- (int )age {return _age; }- (void)setAge:(int)age {_age = age; }但是全局變量程序整個(gè)執(zhí)行過(guò)程中內(nèi)存中只有一份,我們創(chuàng)建多個(gè)對(duì)象修改其屬性值都會(huì)修改同一個(gè)變量,這樣就無(wú)法保證像屬性一樣每個(gè)對(duì)象都擁有其自己的屬性值。這時(shí)我們就需要借助runtime為分類(lèi)增加屬性的功能了。
需要用到的方法 <objc/runtime.h>
- set方法,將值value 跟對(duì)象object 關(guān)聯(lián)起來(lái)(將值value 存儲(chǔ)到對(duì)象object 中)
參數(shù) object:給哪個(gè)對(duì)象設(shè)置屬性
參數(shù) key:一個(gè)屬性對(duì)應(yīng)一個(gè)Key,將來(lái)可以通過(guò)key取出這個(gè)存儲(chǔ)的值,key 可以是任何類(lèi)型:double、int 等,建議用char 可以節(jié)省字節(jié)
參數(shù) value:給屬性設(shè)置的值
參數(shù)policy:存儲(chǔ)策略 (assign 、copy 、 retain就是strong)
void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy) - 利用參數(shù)key 將對(duì)象object中存儲(chǔ)的對(duì)應(yīng)值取出來(lái)
id objc_getAssociatedObject(id object , const void *key)
步驟:
1、創(chuàng)建一個(gè)分類(lèi),比如給任何一個(gè)對(duì)象都添加一個(gè)name屬性,就是NSObject添加分類(lèi)(NSObject+Category)
2、先在.h 中@property 聲明出get 和 set 方法,方便點(diǎn)語(yǔ)法調(diào)用
@property(nonatomic,copy)NSString *name;3、在.m 中重寫(xiě)set 和 get 方法,內(nèi)部利用runtime 給屬性賦值和取值
char nameKey;- (void)setName:(NSString *)name {// 將某個(gè)值跟某個(gè)對(duì)象關(guān)聯(lián)起來(lái),將某個(gè)值存儲(chǔ)到某個(gè)對(duì)象中objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC); }- (NSString *)name {return objc_getAssociatedObject(self, &nameKey); }案例四:獲得一個(gè)類(lèi)的所有成員變量
最典型的用法就是一個(gè)對(duì)象在歸檔和解檔的 encodeWithCoder和initWithCoder:方法中需要該對(duì)象所有的屬性進(jìn)行decodeObjectForKey: 和 encodeObject:,通過(guò)runtime我們聲明中無(wú)論寫(xiě)多少個(gè)屬性,都不需要再修改實(shí)現(xiàn)中的代碼了。
需要用到的方法 <objc/runtime.h>
- 獲得某個(gè)類(lèi)的所有成員變量(outCount 會(huì)返回成員變量的總數(shù))
參數(shù):
1、哪個(gè)類(lèi)
2、放一個(gè)接收值的地址,用來(lái)存放屬性的個(gè)數(shù)
3、返回值:存放所有獲取到的屬性,通過(guò)下面兩個(gè)方法可以調(diào)出名字和類(lèi)型
- 獲得成員變量的名字
- 獲得成員變量的類(lèi)型
獲取Person類(lèi)中所有成員變量的名字和類(lèi)型
unsigned int outCount = 0; Ivar *ivars = class_copyIvarList([Person class], &outCount);// 遍歷所有成員變量 for (int i = 0; i < outCount; i++) {// 取出i位置對(duì)應(yīng)的成員變量Ivar ivar = ivars[i];const char *name = ivar_getName(ivar);const char *type = ivar_getTypeEncoding(ivar);NSLog(@"成員變量名:%s 成員變量類(lèi)型:%s",name,type); } // 注意釋放內(nèi)存! free(ivars);利用runtime 獲取所有屬性來(lái)重寫(xiě)歸檔解檔方法
如果你實(shí)現(xiàn)過(guò)自定義模型數(shù)據(jù)持久化的過(guò)程,那么你也肯定明白,如果一個(gè)模型有許多個(gè)屬性,那么我們需要對(duì)每個(gè)屬性都實(shí)現(xiàn)一遍encodeObject 和 decodeObjectForKey方法,如果這樣的模型又有很多個(gè),這還真的是一個(gè)十分麻煩的事情。下面來(lái)看看簡(jiǎn)單的實(shí)現(xiàn)方式。
假設(shè)現(xiàn)在有一個(gè)Movie類(lèi),有3個(gè)屬性,它的h文件這這樣的
如果是正常寫(xiě)法, m文件應(yīng)該是這樣的:
#import "Movie.h" @implementation Movie- (void)encodeWithCoder:(NSCoder *)aCoder {[aCoder encodeObject:_movieId forKey:@"id"];[aCoder encodeObject:_movieName forKey:@"name"];[aCoder encodeObject:_pic_url forKey:@"url"];}- (id)initWithCoder:(NSCoder *)aDecoder {if (self = [super init]) {self.movieId = [aDecoder decodeObjectForKey:@"id"];self.movieName = [aDecoder decodeObjectForKey:@"name"];self.pic_url = [aDecoder decodeObjectForKey:@"url"];}return self; } @end如果這里有100個(gè)屬性,那么我們也只能把100個(gè)屬性都給寫(xiě)一遍。
不過(guò)你會(huì)使用runtime后,這里就有更簡(jiǎn)便的方法。
下面看看runtime的實(shí)現(xiàn)方式:
這樣的方式實(shí)現(xiàn),不管有多少個(gè)屬性,寫(xiě)這幾行代碼就搞定了。怎么,還嫌麻煩,下面看看更加簡(jiǎn)便的方法:兩句代碼搞定。
我們把encodeWithCoder 和 initWithCoder這兩個(gè)方法抽成宏
我們可以把這兩個(gè)宏單獨(dú)放到一個(gè)文件里面,這里以后需要進(jìn)行數(shù)據(jù)持久化的模型都可以直接使用這兩個(gè)宏。
案例五:利用runtime 獲取所有屬性來(lái)進(jìn)行字典轉(zhuǎn)模型
以往我們都是利用KVC進(jìn)行字典轉(zhuǎn)模型,但是它還是有一定的局限性,例如:模型屬性和鍵值對(duì)對(duì)應(yīng)不上會(huì)crash(雖然可以重寫(xiě)setValue:forUndefinedKey:方法防止報(bào)錯(cuò)),模型屬性是一個(gè)對(duì)象或者數(shù)組時(shí)不好處理等問(wèn)題,所以無(wú)論是效率還是功能上,利用runtime進(jìn)行字典轉(zhuǎn)模型都是比較好的選擇。
字典轉(zhuǎn)模型我們需要考慮三種特殊情況:
1.當(dāng)字典的key和模型的屬性匹配不上
2.模型中嵌套模型(模型屬性是另外一個(gè)模型對(duì)象)
3.數(shù)組中裝著模型(模型的屬性是一個(gè)數(shù)組,數(shù)組中是一個(gè)個(gè)模型對(duì)象)
字典轉(zhuǎn)模型的應(yīng)用可以說(shuō)是每個(gè)app必然會(huì)使用的場(chǎng)景,雖然實(shí)現(xiàn)的方式略有不同,但是原理都是一致的:遍歷模型中所有屬性,根據(jù)模型的屬性名,去字典中查找key,取出對(duì)應(yīng)的值,給模型的屬性賦值。
像幾個(gè)出名的開(kāi)源庫(kù):JSONModel,MJExtension等都是通過(guò)這種方式實(shí)現(xiàn)的。
先實(shí)現(xiàn)最外層的屬性轉(zhuǎn)換
// 創(chuàng)建對(duì)應(yīng)模型對(duì)象
unsigned int count = 0;// 1.獲取成員屬性數(shù)組 Ivar *ivarList = class_copyIvarList(self, &count);// 2.遍歷所有的成員屬性名,一個(gè)一個(gè)去字典中取出對(duì)應(yīng)的value給模型屬性賦值 for (int i = 0; i < count; i++) {// 2.1 獲取成員屬性Ivar ivar = ivarList;// 2.2 獲取成員屬性名 C -> OC 字符串NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];// 2.3 _成員屬性名 => 字典keyNSString *key = [ivarName substringFromIndex:1];// 2.4 去字典中取出對(duì)應(yīng)value給模型屬性賦值id value = dict[key];// 獲取成員屬性類(lèi)型NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];}
id objc = [[self alloc] init];
如果模型比較簡(jiǎn)單,只有NSString,NSNumber等,這樣就可以搞定了。但是如果模型含有NSArray,或者NSDictionary等,那么我們還需要進(jìn)行第二步轉(zhuǎn)換。
內(nèi)層數(shù)組,字典的轉(zhuǎn)換
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { // 是字典對(duì)象,并且屬性名對(duì)應(yīng)類(lèi)型是自定義類(lèi)型// 處理類(lèi)型字符串 @\"User\" -> UserivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];// 自定義對(duì)象,并且值是字典// value:user字典 -> User模型// 獲取模型(user)類(lèi)對(duì)象Class modalClass = NSClassFromString(ivarType);// 字典轉(zhuǎn)模型if (modalClass) {// 字典轉(zhuǎn)模型 uservalue = [modalClass objectWithDict:value];}}if ([value isKindOfClass:[NSArray class]]) {// 判斷對(duì)應(yīng)類(lèi)有沒(méi)有實(shí)現(xiàn)字典數(shù)組轉(zhuǎn)模型數(shù)組的協(xié)議if ([self respondsToSelector:@selector(arrayContainModelClass)]) {// 轉(zhuǎn)換成id類(lèi)型,就能調(diào)用任何對(duì)象的方法id idSelf = self;// 獲取數(shù)組中字典對(duì)應(yīng)的模型NSString *type = [idSelf arrayContainModelClass][key];// 生成模型Class classModel = NSClassFromString(type);NSMutableArray *arrM = [NSMutableArray array];// 遍歷字典數(shù)組,生成模型數(shù)組for (NSDictionary *dict in value) {// 字典轉(zhuǎn)模型id model = [classModel objectWithDict:dict];[arrM addObject:model];}// 把模型數(shù)組賦值給valuevalue = arrM;}}
我自己覺(jué)得系統(tǒng)自帶的KVC模式字典轉(zhuǎn)模型就挺好的,假設(shè)movie是一個(gè)模型對(duì)象,dict 是一個(gè)需要轉(zhuǎn)化的 [movie setValuesForKeysWithDictionary:dict]; 這個(gè)是系統(tǒng)自帶的字典轉(zhuǎn)模型方法,個(gè)人感覺(jué)也還是挺好用的,不過(guò)使用這個(gè)方法的時(shí)候需要在模型里面再實(shí)現(xiàn)一個(gè)方法才行,
- (void)setValue: (id)value forUndefinedKey: (NSString *)key
重寫(xiě)這個(gè)方法為了實(shí)現(xiàn)兩個(gè)目的:
這個(gè)方法的實(shí)現(xiàn):
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { if ([key isEqualToString:@"id"]) {self.uid = value; } }案例六:動(dòng)態(tài)變量控制
在程序中,xiaoming的age是10,后來(lái)被runtime變成了20,來(lái)看看runtime是怎么做到的。
1.動(dòng)態(tài)獲取XiaoMing類(lèi)中的所有屬性[當(dāng)然包括私有]
`Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);`2.遍歷屬性找到對(duì)應(yīng)name字段
`const char *varName = ivar_getName(var);`3.修改對(duì)應(yīng)的字段值成20
`object_setIvar(self.xiaoMing, var, @"20");`4.代碼參考
-(void)answer{unsigned int count = 0;Ivar ivar = class_copyIvarList([self.xiaoMing class], &count);for (int i = 0; i<count; i++) { Ivar var = ivar[i]; const char varName = ivar_getName(var); NSString *name = [NSString stringWithUTF8String:varName]; if ([name isEqualToString:@”_age”]) { object_setIvar(self.xiaoMing, var, @”20”); break; } } NSLog(@”XiaoMing’s age is %@”,self.xiaoMing.age); }
案例七:動(dòng)態(tài)添加方法
在程序當(dāng)中,假設(shè)XiaoMing的中沒(méi)有g(shù)uess這個(gè)方法,后來(lái)被Runtime添加一個(gè)名字叫g(shù)uess的方法,最終再調(diào)用guess方法做出相應(yīng)。那么,Runtime是如何做到的呢?
1.動(dòng)態(tài)給XiaoMing類(lèi)中添加guess方法:
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@: ");這里參數(shù)地方說(shuō)明一下:
(IMP)guessAnswer 意思是guessAnswer的地址指針;
"v@:" 意思是,v代表無(wú)返回值void,如果是i則代表int;@代表 id sel; : 代表 SEL _cmd;
“v@:@@” 意思是,兩個(gè)參數(shù)的沒(méi)有返回值。
2.調(diào)用guess方法響應(yīng)事件:
[self.xiaoMing performSelectorselector(guess)];3.編寫(xiě)guessAnswer的實(shí)現(xiàn):
void guessAnswer(id self,SEL _cmd){NSLog(@"i am from beijing");}這個(gè)有兩個(gè)地方留意一下:
- void的前面沒(méi)有+、-號(hào),因?yàn)橹皇荂的代碼。
- 必須有兩個(gè)指定參數(shù)(id self,SEL _cmd)。
代碼參考:
-(void)answer{ class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");if ([self.xiaoMing respondsToSelector:@selector(guess)]) {[self.xiaoMing performSelector:@selector(guess)];} else{NSLog(@"Sorry,I don't know");}}void guessAnswer(id self,SEL _cmd){NSLog(@"i am from beijing");}案例八:在方法上增加額外功能
有這樣一個(gè)場(chǎng)景,出于某些需求,我們需要跟蹤記錄APP中按鈕的點(diǎn)擊次數(shù)和頻率等數(shù)據(jù),怎么解決?當(dāng)然通過(guò)繼承按鈕類(lèi)或者通過(guò)類(lèi)別實(shí)現(xiàn)是一個(gè)辦法,但是帶來(lái)其他問(wèn)題比如別人不一定會(huì)去實(shí)例化你寫(xiě)的子類(lèi),或者其他類(lèi)別也實(shí)現(xiàn)了點(diǎn)擊方法導(dǎo)致不確定會(huì)調(diào)用哪一個(gè),runtime可以這樣解決:
@implementation UIButton (Hook)+ (void)load {static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{Class selfClass = [self class];SEL oriSEL = @selector(sendAction:to:forEvent:);Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);SEL cusSEL = @selector(mySendAction:to:forEvent:);Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));if (addSucc) {class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));}else {method_exchangeImplementations(oriMethod, cusMethod);}});}- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { [CountTool addClickCount]; [self mySendAction:action to:target forEvent:event]; }@endload方法會(huì)在類(lèi)第一次加載的時(shí)候被調(diào)用,調(diào)用的時(shí)間比較靠前,適合在這個(gè)方法里做方法交換,方法交換應(yīng)該被保證,在程序中只會(huì)執(zhí)行一次。
轉(zhuǎn)載于:https://www.cnblogs.com/zhuyaguang/p/5660247.html
總結(jié)
以上是生活随笔為你收集整理的Runtime 总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Python开发【第八篇】:网络编程 S
- 下一篇: 为什么我们要赚钱?