概述
今天我們主要討論iOS runtime中的一種黑色技術,稱為Method Swizzling。字面上理解Method Swizzling可能比較晦澀難懂,畢竟不是中文,不過你可以理解為“移花接木”或者“偷天換日”。
用途
介紹某種技術的用途,最簡單的方式就是拋出一些應用場景來引出這種技術的必要性。因此,這里我舉個例子如下。
假設工程中有很多ViewController,我需要你統計每個頁面間跳轉的次數。要求:對原工程的改動越少越好。
針對以上需求,你可能會立馬想出以下兩種方案:
方案一:
在每個ViewController的 viewWillAppear 或者 viewDidAppear 方法中對記錄跳轉次數的某個全局變量(設為 g_viewTransCount )進行計數自增,代碼應該是這樣的:
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; g_viewTransCount++; }
每個ViewController類中都需要做此操作,顯然不合適。因為跳轉次數統計這種業務與APP的主業務並沒有強關聯,上面的代碼會造成耦合度過高。隨着APP業務的不斷擴大,代碼中這樣的雜質代碼會越來越大,維護也越來越困難。而且該方案也違背了我們的要求:對原工程的改動越少越好。因此方案一是個很差的方法。於是我們有了方案二。
方案二:
有沒有某種方法可以不用對每個ViewCotroller都修改呢?有!讓每個ViewController都繼承某個新的ViewController(設為BaseViewController),然后將統計的代碼放到BaseViewCotroller的 viewWillAppear或者viewDidAppear中。這種方案看似較合理,但有以下弊端:
- 繼承自BaseViewCotroller的ViewController中仍舊需要顯式調用 [super viewDidAppear:animated];
- 需要到所有ViewController的頭文件中更改其superClass為BaseViewController
可見,方案二雖然相比方案一少一些看得到的“代碼雜質”,但對工程的改動同樣是巨大的,尤其當工程比較龐大時。
正因為以上方案的不完美,才引出本文的黑科技:Method Swizzling。
先概括一下在上述情景下使用Method Swizzling有哪些優勢:
- 不需要改動現有工程的任何文件
- 本次統計的代碼可復用給其他工程
實現
接下來就是激動人心的Coding Time了。讓我們解開Method Swizzling的神秘面紗。直接上代碼,有注釋。在工程中新建一個UIViewController的category:
#import "UIViewController+swizzling.h" #import <objc/runtime.h> @implementation UIViewController (swizzling) + (void)load { SEL origSel = @selector(viewDidAppear:); SEL swizSel = @selector(swiz_viewDidAppear:); [UIViewController swizzleMethods:[self class] originalSelector:origSel swizzledSelector:swizSel]; } //exchange implementation of two methods + (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel { Method origMethod = class_getInstanceMethod(class, origSel); Method swizMethod = class_getInstanceMethod(class, swizSel); //class_addMethod will fail if original method already exists BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod)); if (didAddMethod) { class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); } else { //origMethod and swizMethod already exist method_exchangeImplementations(origMethod, swizMethod); } } - (void)swiz_viewDidAppear:(BOOL)animated { NSLog(@"I am in - [swiz_viewDidAppear:]"); //handle viewController transistion counting here, before ViewController instance calls its -[viewDidAppear:] method //需要注入的代碼寫在此處 [self swiz_viewDidAppear:animated]; } @end
上述代碼做了這么一件事:在UIViewController的viewDidAppear:方法調用前插入了跳頁計數處理,這一切都在運行時完成。對於上述代碼有以下幾處需要介紹的:
+ (void)load 方法是一個類方法,當某個類的代碼被讀到內存后,runtime會給每個類發送 + (void)load 消息。因此 + (void)load 方法是一個調用時機相當早的方法,而且不管父類還是子類,其 + (void)load 方法都會被調用到,很適合用來插入swizzling方法
最核心的代碼要數 + (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel 了。從函數簽名可以看出,該函數是為了交換兩個方法內部實現。將目光移到Line23,交換兩個方法的內部實現主要依靠兩個runtime API:
class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); method_exchangeImplementations(origMethod, swizMethod);
再看一下Line32, - (void)swiz_viewDidAppear:(BOOL)animated 函數看起來像死循環,實際上不會的。原因請看我在下圖的注釋:
此外,通過斷點可以進一步判斷出view controller的viewDidAppear實際方法體與category的swiz_viewDidAppear方法的執行先后順序。為了更直觀地說明二者的順序,我們可以看一下我打出的Log:
通過Log所打印出的順序足以驗證我們的想法。
以上的method swizzling可以應用於iOS的任何類中對其進行代碼注入,並且絲毫不影響現有工程的代碼。例如,我再舉個例子(沒辦法,我就是喜歡舉例子,但我無非是想讓你掌握的更多一些)。你想統計整個工程中所有按鈕的點擊事件的次數,也就是touchUpInside event發生的次數。剛開始你可能會覺得稍微有些沒有頭緒,因為注入代碼的“切入點”相比於UIViewController的viewDidLoad等方法而言不是那么好找。這時候如果你能仔細考慮以下問題或許能找到思路:
- touchUpInside event發送給什么對象?
- 該對象本通過什么途徑接受這個消息?
第一個問題很好回答,event是發送給UIButton實例,本質上是發送給UIControl實例;
第二個問題你不懂的話就去看看UIControl的頭文件找找線索,於是在頭文件中我們找到這樣一個函數:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
看起來很靠近我們的需求, 事實上的確如此。這要從iOS的事件傳遞機制說起,當你在iOS設備上觸摸一個點時這個觸摸動作被包裝成一個UIEvent按照UIApplication->UIWindow->UIView的順序傳遞下去,當發現最后的接受者是UIControl時就會發送上述消息。因此,我們可以對sendAction:方法進行swizzling代碼注入來達到統計按鈕點擊次數的目的。更深入一些,則需要針對不同的action、target、event的狀態進行判斷,以達到更精准的統計。關於這一部分內容我將在下一篇iOS動態性系列文章中詳細探討,敬請期待!
OK,文章就到這里,小伙伴們洗洗睡吧。哈哈,開個玩笑,俗話說,“好戲都在后頭”,接下來的部分更好用。看來以上的method swizzling代碼你是否覺得太復雜了?此外,當你嘗試對多個類進行swizzle時會發現很多代碼是冗余的,每個category文件的框架都長得差不多。那是否有進一步封裝的可能性呢?那是必須的。慶幸的是有團隊已經幫我們封裝了,我們直接拿來用就可以。這就是有名的Aspect庫。
AOP編程以及Aspect庫
Aspect庫是對面向切面編程(Aspect Oriented Programming)的實現,里面封裝了Runtime的方法,也封裝了上文的Method Swizzling方法。因此我們也可以看到,Method Swizzling也是AOP編程的一種。Aspect的用途很廣泛,這里不具體展開,想了解更多的可以看一下官方github的介紹,已經夠詳細了。這里我們只介紹其基礎應用。Aspect只提供了兩個接口:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error { return aspect_add((id)self, selector, options, block, error); } /// @return A token which allows to later deregister the aspect. - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error { return aspect_add(self, selector, options, block, error); }
使用起來也非常方便,使用Aspect對本文最初提出的需求“統計每個頁面間跳轉的次數”進行改造,代碼變成這樣子:
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){ g_viewTransCount++ NSLog(@"[ASPECT] inject in class instance:%@", [info instance]); } error:NULL];
將以上代碼放到AppDelegate的 didFinishLaunchingWithOptions 函數最開始處即可,你可以參考我在文末貼出的代碼,使用一個專門的管理類來管理這些AOP代碼。
相比於上半部分的原始Method Swizzling代碼,使用Aspect有以下好處:
- 原則上不需要新建任何文件。這點很好理解,原始Method Swizzling需要新建category文件,當代碼注入的需要較多時會出現過多的文件以及冗余代碼。
- 可以對類的實例進行代碼注入,因為Aspect提供了實例方法以及類方法
寫在最后
Method Swizzling以及Runtime的一些特性就是iOS里的黑科技,如果能靈活應用的話可以在保證解決問題的前提下降低模塊之間的耦合度,提高代碼的可復用性。至於Method Swizzling與Aspect庫的選擇因人而異,我個人建議在最初階段先放下Aspect而只用Method Swizzling原始代碼去實現代碼注入。掌握本質總是不吃虧的。
本文的示例代碼:Github
歡迎關注我的github上的其他代碼,別忘記隨手點個Star,給我更多支持與鼓勵!
原創文章,轉載請注明 編程小翁@博客園,郵件zilin_weng@163.com,歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!