之前通過學習官方文件對runtime有了初步的認識,接下來就要研究學習runtime到底能用在哪些地方,能如何改進我們的程式。
本文也可以從icocoa瀏覽。
Swizzling
Swizzling可以分為method swizzling和class(isa)swizzling兩種。顧名思義就是將方法/類在執行時替換掉。
Method Swizzling
在執行時替換/修改某個方法——可以是自己寫的方法也可以是系統的方法——當然一般是用於替換框架類中的方法。
//ZJView.m -Swizzling + (void)swizzleSetFrame { SEL originalSel = @selector(setFrame:); Class myClass = [self class]; Method originMethod = class_getInstanceMethod(myClass, originalSel); const char *originType = method_getTypeEncoding(originMethod); originalIMP = (void *)method_getImplementation(originMethod); class_replaceMethod(myClass, originalSel, (IMP)mySetFrame, originType); } static void mySetFrame(id self, SEL _cmd,CGRect frame) { NSLog(@"run mySetFrame"); if (originalIMP) { frame.origin.y += 20; originalIMP(self, _cmd, frame); } }
如上是在自定義的View類裡替換了setFrame方法(注意這樣做在實際的編碼中沒有意義,因為完全可以通過繼承做到這一點,這裡只是從程式碼的角度來理解method swizzling)。替換的方法可以放在+load裡,或者自行顯式的呼叫。這裡需要注意的是stackoverflow上有很多是這樣進行替換的:
if(class_addMethod() ) { class_replaceMethod(); } else { method_exchangeImplementations(); }
之所以按這樣的流程處理是想先檢測下class下是否有需要被替換的selector。但其實runtime已經考慮了這種情形,所以直接進行class_replaceMethod即可。 通常情況下,我們可以通過繼承來過載某個方法,但對於沒有繼承關係的類的方法過載ObjC提供了Category。比如在iOS5前,要自定義NavigationBar的背景,我們就是通過建立一個category來過載drawRect。但是這樣使用Category的話有以下弊端:
- 方法的原先實現被完全過載了,無法呼叫原先的實現。尤其是為庫類中方法過載的時候,我們往往希望獲得原先的實現,而不是簡單的全盤替換。
- 如果有多個category的時候,無法保證哪一個勝出。
所以category往往用於給框架類新增方法。在這種情況下,method swizzling就是一個很好的選擇。由於現在iOS的版本也日趨變多,有時也會遇到某些類的方法在不同iOS下有不同的表現。那麼,我們就可以根據實際情況,徵對不同的iOS版本,選擇繼續用預設的實現,或者自定義的實現,或者兩者的結合。此外為了避免衝突,method swizzling最好在+load函式裡呼叫。 鑑於在實踐中使用method swizzling的場合較少,個人體會不夠深刻,暫時個人理解只能在這個層次了。我在stakoverflow上找到一篇帖子,對於使用method swizzling需要注意的地方做了詳盡的說明。
Class(isa) Swizzling
runtime通過object_setClass來動態的替換物件的class。值得注意的是新class的長度要和原先class的長度一致。此外,KVO的實現就是利用了isa swizzling,iOS6PTL中也對此進行了說明。比如物件a要觀察b的某個屬性,在新增observer的時候,系統會生成一箇中間類,並把b的isa指標指向這個新類。這也說明因為是在runtime時處理KVO,使用KVO時一定要注意遵循相應的命名規範。
關聯指標(Associative References)
關聯指標指的是在runtime給某物件新增一個變數,新增的變數不會對原有的類產生任何影響——這是優於ObjC擴充套件(Extension)的地方,主要使用以下方法:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); objc_getAssociatedObject(id object, const void *key); void objc_removeAssociatedObjects(id object);
對於框架類,由於沒有原始碼,可以通過這種方式新增一些變數。比如UIView,UIAlertView都可以新增tag方便以後重新獲得,我們也可以通過關聯指標使其與某個物件相關聯。
內省(Introspection)
好像也能叫Reflection(反射),不過我不確定。Introspection是OO語言都應該具備的特性,它指的是在runtime時物件通過請求可以查詢自己類的關鍵資訊的能力。首先ObjC語言本身就有這樣的介面,比如:
-isKindOfClass:; -isMemberOfClass: -respondsToSelector: ;-conformsToProtocol: -isEqual:
這些分別對應著:
- 檢視所屬類以及類的繼承關係;
- 檢視是否實現了某個方法或者協議
- 判斷物件是否相等
這些功能在NSObject類和協議裡定義的,一般情況下,iOS上的類都能使用。 接下來要介紹兩個開源庫:Mantle 和Overcoat,他們是內省的重要應用。 在實際中,我們通常會在程式中設計一個Model層,用於Json和Object之間的轉化。比較完備的Model類會考慮到:
- NSString屬性
- 通過NSString生成的如NSURL等屬性
- NSNumber,NSDate等屬性的格式化
- 實現NSCoding,NSCopying協議等等
- 。。。。
一般情況下,程式裡的Model不會只有一個,全部這樣實現的話,很顯然有很大一部分程式碼是“冗餘”的但卻不能通過繼承之類的方法規避。Mantle就是一個很好的選擇,它將你的注意力集中到Model的設計,實現部分只需要一些必須的方法,如:
+ (NSDictionary *)JSONKeyPathsByPropertyKey { //提供Json中key與Model中屬性的對應,如果key與屬性一致可以忽略不寫 } + (NSValueTransformer *)JSONTransformerForKey:(NSString *)key { //對一些需要進行格式化處理的key進行選擇性操作 }
Introspection在Mantle中的應用就是runtime時通過class_copyPropertyList來獲取類的屬性列表,從而簡化我們的工作。 至於Overcoat則是AFNetworking和Mantle的一個結合。AFNetworking是繼ASINetwork後,iOS和OS X上出名的網路庫,而且維護更新比較活躍。Overcoat的主要工作就是把通過AFnetworking獲取的結果轉化成物件,轉化的過程就是使用了Mantle,並且把這部分工作放在了後臺進行。Overcoat提供了一個例子ReadingList,大家可以好好研究下。注意ReadingList是需要使用到cocoapods的,不知道的朋友,out啦~
動態屬性(方法)
之前提到過的,我們可以在runtime時新增方法,更進一步的,我們可以動態的新增屬性而不用實現宣告,下面的程式碼來自gist:
#import <objc/runtime.h> #import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic,strong) NSMutableDictionary *properties; @end @implementation Person -(id) init { self = [super init]; if (self){ _properties = [NSMutableDictionary new]; } return self; } // generic getter static id propertyIMP(id self, SEL _cmd) { return [[self properties] valueForKey:NSStringFromSelector(_cmd)]; } // generic setter static void setPropertyIMP(id self, SEL _cmd, id aValue) { id value = [aValue copy]; NSMutableString *key = [NSStringFromSelector(_cmd) mutableCopy]; // delete "set" and ":" and lowercase first letter [key deleteCharactersInRange:NSMakeRange(0, 3)]; [key deleteCharactersInRange:NSMakeRange([key length] - 1, 1)]; NSString *firstChar = [key substringToIndex:1]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:[firstChar lowercaseString]]; [[self properties] setValue:value forKey:key]; } + (BOOL)resolveInstanceMethod:(SEL)aSEL { if ([NSStringFromSelector(aSEL) hasPrefix:@"set"]) { class_addMethod([self class], aSEL, (IMP)setPropertyIMP, "v@:@"); } else { class_addMethod([self class], aSEL,(IMP)propertyIMP, "@@:"); } return YES; } @end int main(int argc, char *argv[]) { @autoreleasepool { Person *p = [Person new]; [p setName:@"Jon"]; NSLog(@"%@",[p name]); } }
以上是現階段對runtime的總結,更多內容有待進一步的探索,歡迎一起學習討論。