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