寫在前面
看完本篇之後你將獲得:
- 瞭解什麼是runtime
- 知道可以利用runtime做到哪些事情
- 掌握用runtime開發的常用方法
Runtime是開源的,任何時候你都可以從http://opensource.apple.com獲取。事實上檢視 Objective-C 原始碼是我理解它是如何工作的第一種方式,在某些問題上要比讀蘋果的文件要好。你也可以下載筆者在寫這篇文章時最新objc4-680.tar.gz。
引言
相信很多從事iOS開發的小夥伴們都聽過這樣一句形容runtime的話:
runtime就像是iOS開發中的妖怪,誰都聽說過,但少有人見(用)到過!
這句話是某知名培訓機構內某老師對學生們說的一句話(原諒我每年都會去down培訓視訊大致的過一遍),相信不少人尤其是初學的萌新們還沒了解過runtime,聽了這句話就被嚇到了!直接在心裡給runtime打個一打標籤[危險,慎用,底層,難,用不到,不用掌握]。以至於很多人做了有一段時間的iOS開發卻依然對其一知半解……
定義
Objective-C 的 Runtime 是一個執行時庫(Runtime Library),它是一個主要使用 C 和彙編寫的庫,為 C 新增了面相物件的能力並創造了 Objective-C。這就是說它在類資訊(Class information) 中被載入,完成所有的方法分發,方法轉發,等等。Objective-C runtime 建立了所有需要的結構體。
其實就在下個人的理解:runtime就是丫Objective-C 的靈魂!Objective-C之所以叫Objective-C是因為他比C語言不同,是物件導向的。但是Objective-C為什麼有面相物件的能力?就是因為有runtime這個鬼東西!
進階
我們為什麼要學習runtime?
- runtime可以遍歷物件的屬性
- runtime可以動態新增/修改屬性,動態新增/修改/替換方法,動態新增/修改/替換協議
- runtime可以動態建立類/物件/協議等等
- runtime可以方法攔截呼叫
其實runtime所能做的還不止這些,你甚至可以利用它來把一個Class A的例項物件a在程式中當作Class B的例項物件來用。所以很多iOS開發者把runtime叫做obj-C的黑魔法!
常用方法
先來個最簡單最基本的也是幾乎所有runtime文必備的例子:
obj-C:
[obj func];
runtime:objc_msgSend(obj, @selector(func);
很多初學者除了知道runtime把物件的方法呼叫轉化成訊息傳送的程式碼之後就不知道其他的了,但是顯然僅僅知道上述的轉化並沒有什麼“吡-”用,我們來看runtime中比較常用(實用)的幾種基本用法:
- 遍歷物件的屬性
首先定義一個簡單的類Person
123456@interface Person : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSInteger age;@end
然後在需要遍歷物件的屬性時
123456id personClass = objc_getClass("Person");unsigned int outCount;objc_property_t *properties = class_copyPropertyList(personClass, &outCount);for (int i = 0; i
這時就會列印出這個類物件的屬性相關資訊:name:T@”NSString”,C,N,V_name
age:Tq,N,V_age - 訊息轉發
其實講道理的話,訊息轉發不是三言兩語就可以講清的,我只能在這裡粗淺的介紹一下,讓大家會用而已。
週末可能會寫一篇詳解runtime的文章來細緻的介紹runtime中的術語以及runtime訊息傳送機制,動態方法解析,重定向以及訊息轉發。包括我們熟用但是可能不知道其原始碼是什麼樣的id,SEL,objc_object,objc_class以及其結構也會詳細的講解到,感興趣的小夥伴可以關注我,這樣我的新文章會第一時間推送給你。
我們這裡的[訊息轉發]指的就是我上面提到的動態方法解析,重定向以及訊息轉發,我們先來看一張圖:
動態方法解析:
從上圖可以知道,當對一個例項物件obj
傳送一條訊息func
時[obj func]
,當前obj
如果沒有對func
實現對應的方法,那麼就runtime會呼叫+ (BOOL)resolveInstanceMethod:(SEL)sel
方法允許開發者對當前受到的訊息func
做出響應,這就是動態方法解析。
繼續拿上面的Person舉例子,給Person類加一個體重weight屬性
1 |
@property (nonatomic, assign) NSInteger weight; |
然後在.m檔案中加入一下程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@implementation Person <a href='http://www.jobbole.com/members/Dynamic2016'>@dynamic</a> weight; //避免自動生成getter/setter方法 //重寫resolveInstanceMethod方法,動態方法解析 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(setWeight:)) { class_addMethod([self class], sel, (IMP)setPropertyDynamic, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } //用來響應setWeight的c語言方法 void setPropertyDynamic(id self, SEL _cmd) { NSLog(@"Dynamic setWeight"); } @end |
然後可以在程式碼就呼叫Person的setWeight
方法
1 2 |
Person *lision = [[Person alloc] init]; lision.weight = 75; |
這時候如果不重寫+ (BOOL)resolveInstanceMethod:(SEL)sel
方法本應該異常的,但是你可以發現程式會列印出資訊:
Dynamic setWeight
重定向:
那麼還是看圖說話,如果沒有重寫+ (BOOL)resolveInstanceMethod:(SEL)sel
方法,那就就會呼叫- (id)forwardingTargetForSelector:(SEL)aSelector
方法,把這個訊息讓另一個物件來處理,這次叫做重定向。
跟著上面的例子走,先另一個類People用來等待重定向:
1 2 3 |
@interface People : NSObject @end |
給新寫的People類加一個weight
方法,但是注意:People沒有weight屬性!
1 2 3 4 |
- (NSInteger)weight { return 70; } |
接下來我們重寫- (id)forwardingTargetForSelector:(SEL)aSelector
方法:
1 2 3 4 5 6 7 8 9 |
- (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(weight)) { People *people = [[People alloc] init]; return people; } return [super forwardingTargetForSelector:aSelector]; } |
然後我們在剛才的執行程式碼中:
1 |
NSLog(@"weight = %ld", lision.weight); |
然後執行,經歷過上面的例子你肯定知道不會異常啦,而且你會發現雖然你給weight屬性賦值明明是75,可是列印結果是:weight = 70。這就是Person類- (id)forwardingTargetForSelector:(SEL)aSelector
方法中把這條資訊拋給了people物件,呼叫了People類的weight
方法!
訊息轉發:
那麼如果上面的兩個方法都沒有重寫,並且訊息依然是當前物件沒有實現的方法,runtime才會啟用訊息轉發呼叫– (void)forwardInvocation:(NSInvocation *)anInvocation
,需要注意的是很多文章沒有提到這個方法花費代價較大,如果要實現把訊息轉發類似的功能建議最好使用重定向,而且再呼叫這個方法前runtime會先呼叫- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法。
我們跟著上面的例子,繼續給Person類加入屬性:
1 |
@property (nonatomic, copy) NSString *ID; |
以及上面提到的兩個方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(setID:)) { NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; //"v@:"代表的意思參見Objective-C Type Encodings,這裡的意思是返回值為空 return sig; } return nil; } - (void)forwardInvocation:(NSInvocation *)anInvocation { People *people = [[People alloc] init]; if ([people respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:people]; } } |
別忘記了在People類中新增對應的方法:
1 2 3 4 |
- (void)setID:(NSString *)ID { NSLog(@"People setID: %@", ID); } |
最後,我門只需要在執行程式碼塊中加入程式碼:
1 |
lision.ID = @"xxxx"; |
結果顯而易見,相信各位都知道將會列印資訊:
People setID: xxxx
寫在最後
其實runtime就是我們無時無刻不在用的東西,只是人們習慣對看不到的東西懷有恐懼心理而已。我們平時的obj-C程式碼都是被runtime轉譯為c和組合語言執行的。我個人認為大公司為什麼喜歡在面試時問runtime相關的東西是因為大公司往往不僅僅要會幹活的人,它還會要求這些會幹活的人知道其中的原理!我們自己也應該要求自己或多或少的理解這些原理,知道我們為什麼寫出的obj-C程式碼經歷了哪些過程run到我們的裝置上,不要敲了很多年的程式碼還是一隻只會幹活的碼農。