07 JANUARY 2015
上一篇介紹了Objective-C Messaging藏澳。利用 Objective-C 的 Runtime 特性仁锯,我們可以給語言做擴展,幫助解決項目開發(fā)中的一些設(shè)計和技術(shù)問題翔悠。這一篇业崖,我們來探索一些利用 Objective-C Runtime 的黑色技巧。這些技巧中最具爭議的或許就是 Method Swizzling 蓄愁。
介紹一個技巧双炕,最好的方式就是提出具體的需求,然后用它跟其他的解決方法做比較撮抓。
所以妇斤,先來看看我們的需求:對 App 的用戶行為進行追蹤和分析。簡單說丹拯,就是當(dāng)用戶看到某個View或者點擊某個Button的時候站超,就把這個事件記下來。
手動添加
最直接粗暴的方式就是在每個viewDidAppear里添加記錄事件的代碼乖酬。
@implementationMyViewController()- (void)viewDidAppear:(BOOL)animated{? ? [superviewDidAppear:animated];// Custom code// Logging[Logging logWithEventName:@“my view did appear”];}- (void)myButtonClicked:(id)sender{// Custom code// Logging[Logging logWithEventName:@“my button clicked”];}
這種方式的缺點也很明顯:它破壞了代碼的干凈整潔死相。因為Logging的代碼本身并不屬于ViewController里的主要邏輯。隨著項目擴大咬像、代碼量增加算撮,你的ViewController里會到處散布著Logging的代碼生宛。這時,要找到一段事件記錄的代碼會變得困難肮柜,也很容易忘記添加事件記錄的代碼陷舅。
你可能會想到用繼承或類別,在重寫的方法里添加事件記錄的代碼素挽。代碼可以是長的這個樣子:
@implementationUIViewController()- (void)myViewDidAppear:(BOOL)animated{? ? [superviewDidAppear:animated];// Custom code// Logging[Logging logWithEventName:NSStringFromClass([selfclass])];}- (void)myButtonClicked:(id)sender{// Custom code// LoggingNSString*name = [NSStringstringWithFormat:@“my buttonin%@ is clicked”, NSStringFromClass([selfclass])];? ? [Logging logWithEventName:name];}
Logging的代碼都很相似蔑赘,通過繼承或類別重寫相關(guān)方法是可以把它從主要邏輯中剝離出來狸驳。但同時也帶來新的問題:
你需要繼承UIViewController,UITableViewController,UICollectionViewController所有這些 ViewController 预明,或者給他們添加類別;
每個 ViewController 里的 ButtonClick 方法命名不可能都一樣耙箍;
你不能控制別人如何去實例化你的子類撰糠;
對于類別,你沒辦法調(diào)用到原來的方法實現(xiàn)辩昆。大多時候阅酪,我們重寫一個方法只是為了添加一些代碼,而不是完全取代它汁针。
如果有兩個類別都實現(xiàn)了相同的方法术辐,運行時沒法保證哪一個類別的方法會給調(diào)用。
Method Swizzling
Method Swizzling 利用 Runtime 特性把一個方法的實現(xiàn)與另一個方法的實現(xiàn)進行替換施无。
上一篇文章有講到每個類里都有一個 Dispatch Table 辉词,將方法的名字(SEL)跟方法的實現(xiàn)(IMP,指向 C 函數(shù)的指針)一一對應(yīng)猾骡。Swizzle 一個方法其實就是在程序運行時在 Dispatch Table 里做點改動瑞躺,讓這個方法的名字(SEL)對應(yīng)到另個 IMP 。
首先定義一個類別兴想,添加將要 Swizzled 的方法:
@implementationUIViewController(Logging)- (void)swizzled_viewDidAppear:(BOOL)animated{// call original implementation[selfswizzled_viewDidAppear:animated];// Logging[Logging logWithEventName:NSStringFromClass([selfclass])];}
代碼看起來可能有點奇怪幢哨,像遞歸不是么。當(dāng)然不會是遞歸嫂便,因為在 runtime 的時候捞镰,函數(shù)實現(xiàn)已經(jīng)被交換了。調(diào)用viewDidAppear:會調(diào)用你實現(xiàn)的swizzled_viewDidAppear:毙替,而在swizzled_viewDidAppear:里調(diào)用swizzled_viewDidAppear:實際上調(diào)用的是原來的viewDidAppear:岸售。
接下來實現(xiàn) swizzle 的方法 :
@implementationUIViewController (Logging)voidswizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector){// the method might not exist in the class, but in its superclassMethod originalMethod = class_getInstanceMethod(class,originalSelector);? ? Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);// class_addMethod will fail if original method already existsBOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));// the method doesn’t exist and we just added oneif(didAddMethod) {? ? ? ? class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));? ? }else{? ? ? ? method_exchangeImplementations(originalMethod, swizzledMethod);? ? }}
這里唯一可能需要解釋的是class_addMethod。要先嘗試添加原 selector 是為了做一層保護蔚龙,因為如果這個類沒有實現(xiàn)originalSelector冰评,但其父類實現(xiàn)了,那class_getInstanceMethod會返回父類的方法木羹。這樣method_exchangeImplementations替換的是父類的那個方法甲雅,這當(dāng)然不是你想要的解孙。所以我們先嘗試添加orginalSelector,如果已經(jīng)存在抛人,再用method_exchangeImplementations把原方法的實現(xiàn)跟新的方法實現(xiàn)給交換掉弛姜。
最后,我們只需要確保在程序啟動的時候調(diào)用swizzleMethod方法妖枚。比如廷臼,我們可以在之前UIViewController的 Logging 類別里添加+load:方法,然后在+load:里把viewDidAppear給替換掉:
@implementationUIViewController(Logging)+ (void)load{? ? swizzleMethod([selfclass],@selector(viewDidAppear:),@selector(swizzled_viewDidAppear:));}
一般情況下绝页,類別里的方法會重寫掉主類里相同命名的方法荠商。如果有兩個類別實現(xiàn)了相同命名的方法,只有一個方法會被調(diào)用续誉。但+load:是個特例莱没,當(dāng)一個類被讀到內(nèi)存的時候, runtime 會給這個類及它的每一個類別都發(fā)送一個+load:消息酷鸦。
其實饰躲,這里還可以更簡化點:直接用新的 IMP 取代原 IMP ,而不是替換臼隔。只需要有全局的函數(shù)指針指向原 IMP 就可以嘹裂。
void(gOriginalViewDidAppear)(id, SEL,BOOL);voidnewViewDidAppear(UIViewController*self, SEL _cmd,BOOLanimated)? {// call original implementationgOriginalViewDidAppear(self, _cmd, animated);// Logging[Logging logWithEventName:NSStringFromClass([selfclass])];}+ (void)load{? ? Method originalMethod = class_getInstanceMethod(self,@selector(viewDidAppear:));? ? gOriginalViewDidAppear = (void*)method_getImplementation(originalMethod);if(!class_addMethod(self,@selector(viewDidAppear:), (IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {? ? ? ? method_setImplementation(originalMethod, (IMP) newViewDidAppear);? ? }}
通過 Method Swizzling ,我們成功把邏輯代碼跟處理事件記錄的代碼解耦摔握。當(dāng)然除了 Logging 寄狼,還有很多類似的事務(wù),如 Authentication 和 Caching盒发。這些事務(wù)瑣碎例嘱,跟主要業(yè)務(wù)邏輯無關(guān),在很多地方都有宁舰,又很難抽象出來單獨的模塊拼卵。這種程序設(shè)計問題,業(yè)界也給了他們一個名字 -Cross Cutting Concerns蛮艰。
而像上面例子用 Method Swizzling 動態(tài)給指定的方法添加代碼腋腮,以解決 Cross Cutting Concerns 的編程方式叫:Aspect Oriented Programming
Aspect Oriented Programming (面向切面編程)
Wikipedia 里對 AOP 是這么介紹的:
An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).
在 Objective-C 的世界里,這句話意思就是利用 Runtime 特性給指定的方法添加自定義代碼壤蚜。有很多方式可以實現(xiàn) AOP 即寡,Method Swizzling 就是其中之一。而且幸運的是袜刷,目前已經(jīng)有一些第三方庫可以讓你不需要了解 Runtime 聪富,就能直接開始使用 AOP 。
Aspects就是一個不錯的 AOP 庫著蟹,封裝了 Runtime 墩蔓, Method Swizzling 這些黑色技巧梢莽,只提供兩個簡單的API:
+ (id)aspect_hookSelector:(SEL)selectorwithOptions:(AspectOptions)optionsusingBlock:(id)blockerror:(NSError**)error;-(id)aspect_hookSelector:(SEL)selectorwithOptions:(AspectOptions)optionsusingBlock:(id)blockerror:(NSError**)error;
使用 Aspects 提供的 API,我們之前的例子會進化成這個樣子:
@implementationUIViewController(Logging)+ (void)load{? ? [UIViewControlleraspect_hookSelector:@selector(viewDidAppear:)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? withOptions:AspectPositionAfter? ? ? ? ? ? ? ? ? ? ? ? ? ? ? usingBlock:^(id aspectInfo) {NSString*className = NSStringFromClass([[aspectInfo instance] class]);? ? ? ? [Logging logWithEventName:className];? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } error:NULL];}
你可以用同樣的方式在任何你感興趣的方法里添加自定義代碼奸披,比如 IBAction 的方法里昏名。更好的方式,你提供一個 Logging 的配置文件作為唯一處理事件記錄的地方:
@implementationAppDelegate(Logging)+ (void)setupLogging{NSDictionary*config = @{@"MainViewController": @{? ? ? ? ? ? GLLoggingPageImpression:@"page imp - main page",? ? ? ? ? ? GLLoggingTrackedEvents: @[? ? ? ? ? ? ? ? @{? ? ? ? ? ? ? ? ? ? GLLoggingEventName:@"button one clicked",? ? ? ? ? ? ? ? ? ? GLLoggingEventSelectorName:@"buttonOneClicked:",? ? ? ? ? ? ? ? ? ? GLLoggingEventHandlerBlock: ^(id aspectInfo) {? ? ? ? ? ? ? ? ? ? ? ? [Logging logWithEventName:@"button one clicked"];? ? ? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? @{? ? ? ? ? ? ? ? ? ? GLLoggingEventName:@"button two clicked",? ? ? ? ? ? ? ? ? ? GLLoggingEventSelectorName:@"buttonTwoClicked:",? ? ? ? ? ? ? ? ? ? GLLoggingEventHandlerBlock: ^(id aspectInfo) {? ? ? ? ? ? ? ? ? ? ? ? [Logging logWithEventName:@"button two clicked"];? ? ? ? ? ? ? ? ? ? },? ? ? ? ? ? ? ? },? ? ? ? ? ],? ? ? ? },@"DetailViewController": @{? ? ? ? ? ? GLLoggingPageImpression:@"page imp - detail page",? ? ? ? }? ? };? ? [AppDelegate setupWithConfiguration:config];}+ (void)setupWithConfiguration:(NSDictionary*)configs{// Hook Page Impression[UIViewControlleraspect_hookSelector:@selector(viewDidAppear:)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? withOptions:AspectPositionAfter? ? ? ? ? ? ? ? ? ? ? ? ? ? ? usingBlock:^(id aspectInfo) {NSString*className = NSStringFromClass([[aspectInfo instance] class]);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [Logging logWithEventName:className];? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } error:NULL];// Hook Eventsfor(NSString*classNameinconfigs) {? ? ? ? Class clazz = NSClassFromString(className);NSDictionary*config = configs[className];if(config[GLLoggingTrackedEvents]) {for(NSDictionary*eventinconfig[GLLoggingTrackedEvents]) {? ? ? ? ? ? ? ? SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);? ? ? ? ? ? ? ? AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];? ? ? ? ? ? ? ? [clazz aspect_hookSelector:selekor? ? ? ? ? ? ? ? ? ? ? ? ? ? ? withOptions:AspectPositionAfter? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? usingBlock:^(id aspectInfo) {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? block(aspectInfo);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } error:NULL];? ? ? ? ? ? }? ? ? ? }? ? }}
然后在-application:didFinishLaunchingWithOptions:里調(diào)用setupLogging:
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {// Override point for customization after application launch.[selfsetupLogging];returnYES;}
最后的話
利用 objective-C Runtime 特性和 Aspect Oriented Programming 阵面,我們可以把瑣碎事務(wù)的邏輯從主邏輯中分離出來轻局,作為單獨的模塊。它是對面向?qū)ο缶幊棠J降囊粋€補充样刷。Logging 是個經(jīng)典的應(yīng)用仑扑,這里做個拋磚引玉,發(fā)揮想象力颂斜,可以做出其他有趣的應(yīng)用夫壁。
使用 Aspects 完整的例子可以從這里獲得:AspectsDemo拾枣。
如果你有什么問題和想法沃疮,歡迎留言或者發(fā)郵件給我 peng@glowing.com 進行討論。