面向切面編程

Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區內沒有那么有名,但是 AOP 在運行時可以有巨大威力。 但是因為沒有事實上的標準,Apple 也沒有開箱即用的提供,也顯得不重要,開發者都不怎么考慮它。

引用 Aspect Oriented Programming 維基頁面:
An aspect can alter the behavior of the base code (the non-aspect part of a program) 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). (一個切面可以通過在多個 join points 中附加的行為來改變基礎代碼的行為(程序的非切面的部分) )
在 Objective-C 的世界里,這意味著使用運行時的特性來為指定的方法追加 切面 。切面所附加的行為可以是這樣的:

  • 在類的特定方法調用前運行特定的代碼
  • 在類的特定方法調用后運行特定的代碼
  • 增加代碼來替代原來的類的方法的實現

有很多方法可以達成這些目的,但是我們沒有深入挖掘,不過它們主要都是利用了運行時。 Peter Steinberger 寫了一個庫,Aspects 完美地適配了 AOP 的思路。我們發現它值得信賴以及設計得非常優秀,所以我們就在這邊作為一個簡單的例子。

對于所有的 AOP庫,這個庫用運行時做了一些非??岬哪Х?,可以替換或者增加一些方法(比 method swizzling 技術更有技巧性)

Aspect 的 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;

比如,下面的代碼會對于執行 MyClass 類的 myMethod: (實例或者類的方法) 執行塊參數。

[MyClass aspect_hookSelector:@selector(myMethod:)
                 withOptions:AspectPositionAfter
                  usingBlock:^(id<AspectInfo> aspectInfo) {
            ...
        }
                       error:nil];

換一句話說:任意的 MyClass 類型的對象(或者是類型本身當這個 @selector 方法為類方法時)的 @selector 方法執行完后,就會執行這個代碼中塊參數所提供的代碼。

我們為 MyClass 類的 myMethod: 方法增加了切面。

通常 AOP 被用來實現橫向切面。統計與日志就是一個完美的例子。

下面的例子里面,我們會用AOP用來進行統計。統計是iOS項目里面一個熱門的特性,有很多選擇比如 Google Analytics, Flurry, MixPanel, 等等.

大部分統計框架都有教程來指導如何追蹤特定的界面和事件,包括在每一個類里寫幾行代碼。

在 Ray Wenderlich 的博客里有 文章 和一些示例代碼,通過在你的 view controller 里面加入 Google Analytics 進行統計。

- (void)logButtonPress:(UIButton *)button {
    id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
    [tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"UX"
                                                          action:@"touch"
                                                           label:[button.titleLabel text]
                                                           value:nil] build]];
}

上面的代碼在按鈕點擊的時候發送了特定的上下文事件。但是當你想追蹤屏幕的時候會變得很糟。

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

    id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
    [tracker set:kGAIScreenName value:@"Stopwatch"];
    [tracker send:[[GAIDictionaryBuilder createAppView] build]];
}

對于大部分有經驗的iOS工程師,這看起來不是很好的代碼。我們讓 view controller 變得更糟糕了。因為我們加入了統計事件的代碼,但是它不是 view controller 的職能。你可以反駁,因為你通常有特定的對象來負責統計追蹤,并且你將代碼注入了 view controller ,但是無論你隱藏邏輯,問題仍然存在 :你最后還是在viewDidAppear: 后插入了代碼。

我們可以在類的 viewDidAppear: 方法上使用 AOP 來追蹤屏幕,并且我們可以使用同樣的方法在其他我們感興趣的方法上添加事件追蹤。比如當用戶點擊某個按鈕時(比如:一般調用對應的 IBAction).

方法很簡潔且不具侵入性:

  • view controller 不會被不屬于它的代碼污染
  • 為所有加入到我們代碼的切面指定一個 SPOC 文件 (single point of customization)提供了可能
  • SPOC 應該在 App 剛開始啟動的時候用來添加切面
  • 如果SPOC文件異常,至少有一個 selector 或者 類 識別不出來,應用將會在啟動時崩潰(對我們來說這很酷).
  • 公司負責統計的團隊通常會提供統計文檔,羅列出需要追蹤的事件。這個文檔可以很容易映射到一個 SPOC 文件。
  • 追蹤邏輯抽象化之后,擴展到很多其他統計框架會很方便
  • 對于屏幕視圖,對于需要定義 selector 的方法,只需要在 SPOC 文件修改相關的類(相關的切面會加入到 viewDidAppear: 方法)。如果要同時發送屏幕視圖和事件,需要(依靠統計提供方)提供一個追蹤的標示或者可能還需要提供其他的元信息。

我們可能希望一個 SPOC 文件類似下面的(同樣的一個 .plist 文件會適配)

NSDictionary *analyticsConfiguration()
{
    return @{
        @"trackedScreens" : @[
            @{
                @"class" : @"ZOCMainViewController",
                @"label" : @"Main screen"
                }
             ],
        @"trackedEvents" : @[
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"loginViewFetchedUserInfo:user:",
                @"label" : @"Login with Facebook"
                },
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"loginViewShowingLoggedOutUser:",
                @"label" : @"Logout with Facebook"
                },
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"loginView:handleError:",
                @"label" : @"Login error with Facebook"
                },
            @{
                @"class" : @"ZOCMainViewController",
                @"selector" : @"shareButtonPressed:",
                @"label" : @"Share button"
                }
             ]
    };
}

提及的架構托管 在 Github 的EF Education First 中.

- (void)setupWithConfiguration:(NSDictionary *)configuration
{
    // screen views tracking
    for (NSDictionary *trackedScreen in configuration[@"trackedScreens"]) {
        Class clazz = NSClassFromString(trackedScreen[@"class"]);

        [clazz aspect_hookSelector:@selector(viewDidAppear:)
                       withOptions:AspectPositionAfter
                        usingBlock:^(id<AspectInfo> aspectInfo) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 
            ^{
                NSString *viewName = trackedScreen[@"label"];
                [tracker trackScreenHitWithName:viewName];
                });
            }
            error:nil];
        }];

    }

    // events tracking
    for (NSDictionary *trackedEvents in configuration[@"trackedEvents"]) {
        Class clazz = NSClassFromString(trackedEvents[@"class"]);
        SEL selektor = NSSelectorFromString(trackedEvents[@"selector"]);

        [clazz aspect_hookSelector:selektor
                       withOptions:AspectPositionAfter
                        usingBlock:^(id<AspectInfo> aspectInfo) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), 
                    ^{
                        UserActivityButtonPressedEvent *buttonPressEvent = \
                        [UserActivityButtonPressedEvent \
                                eventWithLabel:trackedEvents[@"label"]];
                        [tracker trackEvent:buttonPressEvent];
                    });
                }
            error:nil];
        }];

    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容