概述
今天我們主要讨論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,給我更多支援與鼓勵!
原創文章,轉載請注明 程式設計小翁@部落格園,郵件[email protected],歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!