【原】iOS動態性(三) Method Swizzling以及AOP編程:在運行時進行代碼注入

概述

今天咱們主要討論iOS runtime中的一種黑色技術,稱爲Method Swizzling。字面上理解Method Swizzling可能比較晦澀難懂,畢竟不是中文,不過你能夠理解爲「移花接木」或者「偷天換日」。git

用途

介紹某種技術的用途,最簡單的方式就是拋出一些應用場景來引出這種技術的必要性。所以,這裏我舉個例子以下。github

假設工程中有不少ViewController,我須要你統計每一個頁面間跳轉的次數。要求:對原工程的改動越少越好。編程

針對以上需求,你可能會立馬想出如下兩種方案:框架

方案一:ide

  在每一個ViewController的 viewWillAppear 或者 viewDidAppear 方法中對記錄跳轉次數的某個全局變量(設爲 g_viewTransCount )進行計數自增,代碼應該是這樣的:函數

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    g_viewTransCount++;
}

每一個ViewController類中都須要作此操做,顯然不合適。由於跳轉次數統計這種業務與APP的主業務並無強關聯,上面的代碼會形成耦合度太高。隨着APP業務的不斷擴大,代碼中這樣的雜質代碼會愈來愈大,維護也愈來愈困難。並且該方案也違背了咱們的要求:對原工程的改動越少越好。所以方案一是個不好的方法。因而咱們有了方案二。spa

 

方案二:code

  有沒有某種方法能夠不用對每一個ViewCotroller都修改呢?有!讓每一個ViewController都繼承某個新的ViewController(設爲BaseViewController),而後將統計的代碼放到BaseViewCotroller的 viewWillAppear或者viewDidAppear中。這種方案看似較合理,但有如下弊端:對象

  • 繼承自BaseViewCotroller的ViewController中仍舊須要顯式調用 [super viewDidAppear:animated]; 
  • 須要到全部ViewController的頭文件中更改其superClass爲BaseViewController

可見,方案二雖然相比方案一少一些看獲得的「代碼雜質」,但對工程的改動一樣是巨大的,尤爲當工程比較龐大時。blog

正由於以上方案的不完美,才引出本文的黑科技: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/機器視覺等領域展開交流!

相關文章
相關標籤/搜索