Method Swizzling 和 AOP 實踐

 轉自--------http://tech.glowing.com/cn/method-swizzling-aop/

上一篇介紹了 Objective-C Messaging。利用 Objective-C 的 Runtime 特性,我們可以給語言做擴展,幫助解決項目開發中的一些設計和技術問題。這一篇,我們來探索一些利用 Objective-C Runtime 的黑色技巧。這些技巧中最具爭議的或許就是 Method Swizzling 。

介紹一個技巧,最好的方式就是提出具體的需求,然後用它跟其他的解決方法做比較。

所以,先來看看我們的需求:對 App 的用戶行爲進行追蹤和分析。簡單說,就是當用戶看到某個 View 或者點擊某個 Button 的時候,就把這個事件記下來。

手動添加

最直接粗暴的方式就是在每個 viewDidAppear 裏添加記錄事件的代碼。

@implementation MyViewController ()

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear: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 的代碼。這時,要找到一段事件記錄的代碼會變得困難,也很容易忘記添加事件記錄的代碼。

你可能會想到用繼承或者類別,在重寫的方法裏添加事件記錄的代碼。比如用類別的代碼大概長這個樣子:

@implementation UIViewController (Logging)

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

    // Custom code 

    // Logging
    [Logging logWithEventName:NSStringFromClass([self class])];
}


- (void)myButtonClicked:(id)sender
{
    // Custom code 

    // Logging
    NSString *name = [NSString stringWithFormat:@“my button in %@ is clicked”, NSStringFromClass([self class])];
    [Logging logWithEventName:name];
}

Logging 的代碼都很相似,通過繼承或類別重寫相關方法是可以把它從主要邏輯中剝離出來。但同時也帶來新的問題:

  1. 你需要繼承 UIViewControllerUITableViewControllerUICollectionViewController 所有這些 ViewController ,或者給他們添加類別;
  2. 每個 ViewController 裏的 ButtonClick 方法命名不可能都一樣;
  3. 你不能控制別人如何去實例化你的子類;
  4. 對於類別,你沒辦法調用到原來的方法實現。大多時候,我們重寫一個方法只是爲了添加一些代碼,而不是完全取代它。
  5. 如果有兩個類別都實現了相同的方法,運行時沒法保證哪一個類別的方法會給調用。

Method Swizzling

Method Swizzling 利用 Runtime 特性把一個方法的實現與另一個方法的實現進行替換。

上一篇文章 有講到每個類裏都有一個 Dispatch Table ,將方法的名字(SEL)跟方法的實現(IMP,指向 C 函數的指針)一一對應。Swizzle 一個方法其實就是在程序運行時在 Dispatch Table 裏做點改動,讓這個方法的名字(SEL)對應到另個 IMP 。

首先定義一個類別,添加將要 Swizzled 的方法:

@implementation UIViewController (Logging)

- (void)swizzled_viewDidAppear:(BOOL)animated
{
    // call original implementation
    [self swizzled_viewDidAppear:animated];

    // Logging
    [Logging logWithEventName:NSStringFromClass([self class])];
}

代碼看起來可能有點奇怪,像遞歸不是麼。當然不會是遞歸,因爲在 runtime 的時候,函數實現已經被交換了。調用 viewDidAppear: 會調用你實現的 swizzled_viewDidAppear:,而在 swizzled_viewDidAppear: 裏調用 swizzled_viewDidAppear: 實際上調用的是原來的 viewDidAppear: 。

接下來實現 swizzle 的方法 :

@implementation UIViewController (Logging)

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)  
{
    // the method might not exist in the class, but in its superclass
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    // class_addMethod will fail if original method already exists
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

    // the method doesn’t exist and we just added one
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } 
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

這裏唯一可能需要解釋的是 class_addMethod 。要先嚐試添加原 selector 是爲了做一層保護,因爲如果這個類沒有實現 originalSelector ,但其父類實現了,那 class_getInstanceMethod 會返回父類的方法。這樣 method_exchangeImplementations 替換的是父類的那個方法,這當然不是你想要的。所以我們先嚐試添加 orginalSelector ,如果已經存在,再用 method_exchangeImplementations 把原方法的實現跟新的方法實現給交換掉。

最後,我們只需要確保在程序啓動的時候調用 swizzleMethod 方法。比如,我們可以在之前 UIViewController 的 Logging 類別裏添加 +load: 方法,然後在 +load: 裏把 viewDidAppear 給替換掉:

@implementation UIViewController (Logging)

+ (void)load
{
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}

一般情況下,類別裏的方法會重寫掉主類裏相同命名的方法。如果有兩個類別實現了相同命名的方法,只有一個方法會被調用。但 +load: 是個特例,當一個類被讀到內存的時候, runtime 會給這個類及它的每一個類別都發送一個 +load: 消息。

其實,這裏還可以更簡化點:直接用新的 IMP 取代原 IMP ,而不是替換。只需要有全局的函數指針指向原 IMP 就可以。

void (gOriginalViewDidAppear)(id, SEL, BOOL);

void newViewDidAppear(UIViewController *self, SEL _cmd, BOOL animated)  
{
    // call original implementation
    gOriginalViewDidAppear(self, _cmd, animated);

    // Logging
    [Logging logWithEventName:NSStringFromClass([self class])];
}

+ (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 ,我們成功把邏輯代碼跟處理事件記錄的代碼解耦。當然除了 Logging ,還有很多類似的事務,如 Authentication 和 Caching。這些事務瑣碎,跟主要業務邏輯無關,在很多地方都有,又很難抽象出來單獨的模塊。這種程序設計問題,業界也給了他們一個名字 - Cross Cutting Concerns

而像上面例子用 Method Swizzling 動態給指定的方法添加代碼,以解決 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 特性給指定的方法添加自定義代碼。有很多方式可以實現 AOP ,Method Swizzling 就是其中之一。而且幸運的是,目前已經有一些第三方庫可以讓你不需要了解 Runtime ,就能直接開始使用 AOP 。

Aspects 就是一個不錯的 AOP 庫,封裝了 Runtime , Method Swizzling 這些黑色技巧,只提供兩個簡單的API:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                          withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

使用 Aspects 提供的 API,我們之前的例子會進化成這個樣子:

@implementation UIViewController (Logging)

+ (void)load
{
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) {
        NSString *className = NSStringFromClass([[aspectInfo instance] class]);
        [Logging logWithEventName:className];
                               } error:NULL];
}

你可以用同樣的方式在任何你感興趣的方法裏添加自定義代碼,比如 IBAction 的方法裏。更好的方式,你提供一個 Logging 的配置文件作爲唯一處理事件記錄的地方:

@implementation AppDelegate (Logging)

+ (void)setupLogging
{
    NSDictionary *config = @{
        @"MainViewController": @{
            GLLoggingPageImpression: @"page imp - main page",
            GLLoggingTrackedEvents: @[
                @{
                    GLLoggingEventName: @"button one clicked",
                    GLLoggingEventSelectorName: @"buttonOneClicked:",
                    GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                        [Logging logWithEventName:@"button one clicked"];
                    },
                },
                @{
                    GLLoggingEventName: @"button two clicked",
                    GLLoggingEventSelectorName: @"buttonTwoClicked:",
                    GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                        [Logging logWithEventName:@"button two clicked"];
                    },
                },
           ],
        },

        @"DetailViewController": @{
            GLLoggingPageImpression: @"page imp - detail page",
        }
    };

    [AppDelegate setupWithConfiguration:config];
}

+ (void)setupWithConfiguration:(NSDictionary *)configs
{
    // Hook Page Impression
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) {
                                       NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                    [Logging logWithEventName:className];
                               } error:NULL];

    // Hook Events
    for (NSString *className in configs) {
        Class clazz = NSClassFromString(className);
        NSDictionary *config = configs[className];

        if (config[GLLoggingTrackedEvents]) {
            for (NSDictionary *event in config[GLLoggingTrackedEvents]) {
                SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);
                AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];

                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionAfter
                                usingBlock:^(id<AspectInfo> aspectInfo) {
                                    block(aspectInfo);
                                } error:NULL];

            }
        }
    }
}

然後在 -application:didFinishLaunchingWithOptions: 裏調用 setupLogging

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    [self setupLogging];
    return YES;
}

最後的話

利用 objective-C Runtime 特性和 Aspect Oriented Programming ,我們可以把瑣碎事務的邏輯從主邏輯中分離出來,作爲單獨的模塊。它是對面向對象編程模式的一個補充。Logging 是個經典的應用,這裏做個拋磚引玉,發揮想象力,可以做出其他有趣的應用。

使用 Aspects 完整的例子可以從這裏獲得:AspectsDemo

如果你有什麼問題和想法,歡迎留言或者發郵件給我 [email protected] 進行討論。

Reference

發佈了65 篇原創文章 · 獲贊 11 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章