公司專案用到一個三方開源庫,裡面有個bug,不能改動原始碼,我想來想去,只能通過runtime這個萬能的手段來解決。但是runtime 並不怎麼會用,怎麼辦,馬上學習唄。說到runtime,它是Objective-C裡面最核心的技術,被人們傳呼的神乎其神,但是感覺有一層神祕的面紗籠罩其上,畢竟使用場景不多,相信大多數開發者都不會熟練的運用。而網路上也有無數的文章來講解runtime,但是真的非常的亂,非常的碎片化,很少有講解的比較全面的。
最初是在onevcat的部落格上看到runtime的runtime的部落格,說句實話,看完後我還是蒙的,這裡面主要講了一下runtime 比較核心的功能-Method Swizzling,不過看完後還是有些不知如何下手的感覺。下面是我自己對runtime的整理,從零開始,由淺入深,並且帶了幾個runtime實際的應用場景。看完之後,你可以再回過頭來看喵神的這篇文章,應該就能看的懂了。
一:基本概念
Runtime基本是用C和彙編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這裡下到蘋果維護的開原始碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。Objective-C 從三種不同的層級上與 Runtime 系統進行互動,分別是通過 Objective-C 原始碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函式的直接呼叫。大部分情況下你就只管寫你的Objc程式碼就行,runtime 系統自動在幕後辛勤勞作著。
- RunTime簡稱執行時,就是系統在執行的時候的一些機制,其中最主要的是訊息機制。
- 對於C語言,函式的呼叫在編譯的時候會決定呼叫哪個函式,編譯完成之後直接順序執行,無任何二義性。
- OC的函式呼叫成為訊息傳送。屬於動態呼叫過程。在編譯的時候並不能決定真正呼叫哪個函式(事實證明,在編 譯階段,OC可以呼叫任何函式,即使這個函式並未實現,只要申明過就不會報錯。而C語言在編譯階段就會報錯)。
- 只有在真正執行的時候才會根據函式的名稱找 到對應的函式來呼叫。
二:runtime的具體實現
我們寫的oc程式碼,它在執行的時候也是轉換成了runtime方式執行的,更好的理解runtime,也能幫我們更深的掌握oc語言。
每一個oc的方法,底層必然有一個與之對應的runtime方法。
- 當我們用OC寫下這樣一段程式碼
[tableView cellForRowAtIndexPath:indexPath];
- 在編譯時RunTime會將上述程式碼轉化成[傳送訊息]
objc_msgSend(tableView, @selector(cellForRowAtIndexPath:),indexPath);
三:常見方法
unsigned int count;
- 獲取屬性列表
123objc_property_t *propertyList = class_copyPropertyList([self class], &count);for (unsigned int i=0; i%@", [NSString stringWithUTF8String:propertyName]);} - 獲取方法列表
123Method *methodList = class_copyMethodList([self class], &count);for (unsigned int i; i%@", NSStringFromSelector(method_getName(method)));}
- 獲取成員變數列表
123Ivar *ivarList = class_copyIvarList([self class], &count);for (unsigned int i; i%@", [NSString stringWithUTF8String:ivarName]);} - 獲取協議列表
123__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);for (unsigned int i; i%@", [NSString stringWithUTF8String:protocolName]);}
現在有一個Person類,和person建立的xiaoming物件,有test1和test2兩個方法
- 獲得類方法
1 2 3 |
Class PersonClass = object_getClass([Person class]); SEL oriSEL = @selector(test1); Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL); |
- 獲得例項方法
1 2 3 |
Class PersonClass = object_getClass([xiaoming class]); SEL oriSEL = @selector(test2); Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL); |
- 新增方法
1 |
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod)); |
- 替換原方法實現
1 |
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); |
- 交換兩個方法
1 |
method_exchangeImplementations(oriMethod, cusMethod); |
四:常見作用
- 動態的新增物件的成員變數和方法
- 動態交換兩個方法的實現
- 攔截並替換方法
- 在方法上增加額外功能
- 實現NSCoding的自動歸檔和解檔
- 實現字典轉模型的自動轉換
五:程式碼實現
要使用runtime,要先引入標頭檔案#import
這些程式碼的例項有淺入深逐步講解,最後附上一個我在公司專案中遇到的一個實際問題。
1. 動態變數控制
在程式中,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.程式碼參考
1 2 3 4 |
-(void)answer{ unsigned int count = 0; Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count); for (int i = 0; i |
2.動態新增方法
在程式當中,假設XiaoMing的中沒有guess
這個方法,後來被Runtime新增一個名字叫guess的方法,最終再呼叫guess方法做出相應。那麼,Runtime是如何做到的呢?
1.動態給XiaoMing類中新增guess方法:
1 |
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 performSelector:@selector(guess)];
3.編寫guessAnswer的實現:
void guessAnswer(id self,SEL _cmd){
NSLog(@”i am from beijing”);
}
這個有兩個地方留意一下:
- void的前面沒有+、-號,因為只是C的程式碼。
- 必須有兩個指定引數(id self,SEL _cmd)
4.程式碼參考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
-(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"); } |
3:動態交換兩個方法的實現
在程式當中,假設XiaoMing的中有test1
和 test2
這兩個方法,後來被Runtime交換方法後,每次調動test1
的時候就會去執行test2
,調動test2
的時候就會去執行test1
, 。那麼,Runtime是如何做到的呢?
- 獲取這個類中的兩個方法並交換
1 2 3 |
Method m1 = class_getInstanceMethod([self.xiaoMing class], @selector(test1)); Method m2 = class_getInstanceMethod([self.xiaoMing class], @selector(test2)); method_exchangeImplementations(m1, m2); |
交換方法之後,以後每次呼叫這兩個方法都會交換方法的實現
4:攔截並替換方法
在程式當中,假設XiaoMing的中有test1
這個方法,但是由於某種原因,我們要改變這個方法的實現,但是又不能去動它的原始碼(正如一些開源庫出現問題的時候),這個時候runtime就派上用場了。
我們先增加一個tool類,然後寫一個我們自己實現的方法-change,
通過runtime把test1替換成change。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Class PersionClass = object_getClass([Person class]); Class toolClass = object_getClass([tool class]); ////源方法的SEL和Method SEL oriSEL = @selector(test1); Method oriMethod = class_getInstanceMethod(PersionClass, oriSEL); ////交換方法的SEL和Method SEL cusSEL = @selector(change); Method cusMethod = class_getInstanceMethod(toolClass, cusSEL); ////先嚐試給源方法新增實現,這裡是為了避免源方法沒有實現的情況 BOOL addSucc = class_addMethod(PersionClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod)); if (addSucc) { // 新增成功:將源方法的實現替換到交換方法的實現 class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else { //新增失敗:說明源方法已經有實現,直接將兩個方法的實現交換即 method_exchangeImplementations(oriMethod, cusMethod); } |
5:在方法上增加額外功能
有這樣一個場景,出於某些需求,我們需要跟蹤記錄APP中按鈕的點選次數和頻率等資料,怎麼解決?當然通過繼承按鈕類或者通過類別實現是一個辦法,但是帶來其他問題比如別人不一定會去例項化你寫的子類,或者其他類別也實現了點選方法導致不確定會呼叫哪一個,runtime可以這樣解決:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@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]; } @end |
load方法會在類第一次載入的時候被呼叫,呼叫的時間比較靠前,適合在這個方法裡做方法交換,方法交換應該被保證,在程式中只會執行一次。
6.實現NSCoding的自動歸檔和解檔
如果你實現過自定義模型資料持久化的過程,那麼你也肯定明白,如果一個模型有許多個屬性,那麼我們需要對每個屬性都實現一遍encodeObject
和 decodeObjectForKey
方法,如果這樣的模型又有很多個,這還真的是一個十分麻煩的事情。下面來看看簡單的實現方式。
假設現在有一個Movie類,有3個屬性,它的h
檔案這這樣的
1 2 3 4 5 6 7 8 9 10 |
#import //1. 如果想要當前類可以實現歸檔與反歸檔,需要遵守一個協議NSCoding @interface Movie : NSObject @property (nonatomic, copy) NSString *movieId; @property (nonatomic, copy) NSString *movieName; @property (nonatomic, copy) NSString *pic_url; @end |
如果是正常寫法, m
檔案應該是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#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的實現方式:
1 2 3 4 5 6 7 8 9 10 11 |
#import "Movie.h" #import @implementation Movie - (void)encodeWithCoder:(NSCoder *)encoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Movie class], &count); for (int i = 0; i |
這樣的方式實現,不管有多少個屬性,寫這幾行程式碼就搞定了。怎麼,還嫌麻煩,下面看看更加簡便的方法:兩句程式碼搞定。
我們把encodeWithCoder
和 initWithCoder
這兩個方法抽成巨集
1 2 3 4 5 6 7 8 |
#import "Movie.h" #import #define encodeRuntime(A) unsigned int count = 0; Ivar *ivars = class_copyIvarList([A class], &count); for (int i = 0; i |
我們可以把這兩個巨集單獨放到一個檔案裡面,這裡以後需要進行資料持久化的模型都可以直接使用這兩個巨集。
7.實現字典轉模型的自動轉換
字典轉模型的應用可以說是每個app必然會使用的場景,雖然實現的方式略有不同,但是原理都是一致的:遍歷模型中所有屬性,根據模型的屬性名,去字典中查詢key,取出對應的值,給模型的屬性賦值。
像幾個出名的開源庫:JSONModel,MJExtension等都是通過這種方式實現的。
- 先實現最外層的屬性轉換
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 建立對應模型物件 id objc = [[self alloc] init]; unsigned int count = 0; // 1.獲取成員屬性陣列 Ivar *ivarList = class_copyIvarList(self, &count); // 2.遍歷所有的成員屬性名,一個一個去字典中取出對應的value給模型屬性賦值 for (int i = 0; i OC 字串 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 2.3 _成員屬性名 => 字典key NSString *key = [ivarName substringFromIndex:1]; // 2.4 去字典中取出對應value給模型屬性賦值 id value = dict[key]; // 獲取成員屬性型別 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; } |
如果模型比較簡單,只有NSString,NSNumber等,這樣就可以搞定了。但是如果模型含有NSArray,或者NSDictionary等,那麼我們還需要進行第二步轉換。
- 內層陣列,字典的轉換
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
if ([value isKindOfClass:[NSDictionary class]] & ![ivarType containsString:@"NS"]) { // 是字典物件,並且屬性名對應型別是自定義型別 // 處理型別字串 @"User" -> User ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; ivarType = [ivarType stringByReplacingOccurrencesOfString:@""" withString:@""]; // 自定義物件,並且值是字典 // value:user字典 -> User模型 // 獲取模型(user)類物件 Class modalClass = NSClassFromString(ivarType); // 字典轉模型 if (modalClass) { // 字典轉模型 user value = [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]; } // 把模型陣列賦值給value value = arrM; } } |
我自己覺得系統自帶的KVC模式字典轉模型就挺好的,假設movie是一個模型物件,dict 是一個需要轉化的 [movie setValuesForKeysWithDictionary:dict];
這個是系統自帶的字典轉模型方法,個人感覺也還是挺好用的,不過使用這個方法的時候需要在模型裡面再實現一個方法才行,
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
重寫這個方法為了實現兩個目的:1. 模型中的屬性和字典中的key不一致的情況,比如字典中有個id
,我們需要把它賦值給uid
屬性;2. 字典中屬性比模型的屬性還多的情況。
如果出現以上兩種情況而沒有實現這個方法的話,程式就會崩潰。
這個方法的實現:
1 2 3 4 5 6 |
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { if ([key isEqualToString:@"id"]) { self.uid = value; } } |
六.幾個引數概念
以上的幾種方法應該算是runtime在實際場景中所應用的大部分的情況了,平常的編碼中差不多足夠用了。
如果從頭仔細看到尾,相信你基本的用法應該會了,雖然會用是主要的目的,有幾個基本的引數概念還是要了解一下的。
1.objc_msgSend
1 2 3 4 5 6 7 8 9 |
/* Basic Messaging Primitives * * On some architectures, use objc_msgSend_stret for some struct return types. * On some architectures, use objc_msgSend_fpret for some float return types. * On some architectures, use objc_msgSend_fp2ret for some float return types. * * These functions must be cast to an appropriate function pointer type * before being called. */ |
這是官方的宣告,從這個函式的註釋可以看出來了,這是個最基本的用於傳送訊息的函式。另外,這個函式並不能傳送所有型別的訊息,只能傳送基本的訊息。比如,在一些處理器上,我們必須使用objc_msgSend_stret
來傳送返回值型別為結構體的訊息,使用objc_msgSend_fpret
來傳送返回值型別為浮點型別的訊息,而又在一些處理器上,還得使用objc_msgSend_fp2ret來傳送返回值型別為浮點型別的訊息。
最關鍵的一點:無論何時,要呼叫objc_msgSend
函式,必須要將函式強制轉換成合適的函式指標型別才能呼叫。
從objc_msgSend
函式的宣告來看,它應該是不帶返回值的,但是我們在使用中卻可以強制轉換型別,以便接收返回值。另外,它的引數列表是可以任意多個的,前提也是要強制函式指標型別。
其實編譯器會根據情況在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四個方法中選擇一個來呼叫。如果訊息是傳遞給超類,那麼會呼叫名字帶有”Super”
的函式;如果訊息返回值是資料結構而不是簡單值時,那麼會呼叫名字帶有”stret”
的函式。
2.SEL
objc_msgSend
函式第二個引數型別為SEL,它是selector在Objc
中的表示型別(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的資料結構是SEL:
typedef struct objc_selector *SEL
;
其實它就是個對映到方法的C字串,你可以用 Objc 編譯器命令@selector()
或者 Runtime 系統的sel_registerName
函式來獲得一個SEL型別的方法選擇器。
不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變數型別不同也會導致它們具有相同的方法選擇器,於是 Objc 中方法命名有時會帶上引數型別(NSNumber一堆抽象工廠方法),Cocoa 中有好多長長的方法哦。
3.id
objc_msgSend
第一個引數型別為id,大家對它都不陌生,它是一個指向類例項的指標:
typedef struct objc_object *id
;
那objc_object又是啥呢:
struct objc_object { Class isa; }
;
objc_object
結構體包含一個isa指標,根據isa指標就可以順藤摸瓜找到物件所屬的類。
PS:isa指標不總是指向例項物件所屬的類,不能依靠它來確定型別,而是應該用class方法來確定例項物件的類。因為KVO的實現機理就是將被觀察物件的isa指標指向一箇中間類而不是真實的類,這是一種叫做 isa-swizzling 的技術,詳見官方文件.
4.Class
之所以說isa是指標是因為Class其實是一個指向objc_class結構體的指標:
typedef struct objc_class *Class
;
objc_class裡面的東西多著呢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; |
可以看到執行時一個類還關聯了它的超類指標,類名,成員變數,方法,快取,還有附屬的協議。
在objc_class
結構體中:ivars是objc_ivar_list
指標;methodLists
是指向objc_method_list
指標的指標。也就是說可以動態修改 *methodLists
的值來新增成員方法,這也是Category實現的原理.
上面講到的所有東西都在Demo裡,如果你覺得不錯,還請為我的Demo star一個。
demo下載
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式