鍍金池/ 教程/ iOS/ 面向切面編程
Case語(yǔ)句
美化代碼
參考資料
對(duì)象間的通訊
命名
條件語(yǔ)句
Protocols
NSNotification
面向切面編程
Categories
代碼組織

面向切面編程

Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區(qū)內(nèi)沒(méi)有那么有名,但是 AOP 在運(yùn)行時(shí)可以有巨大威力。 但是因?yàn)闆](méi)有事實(shí)上的標(biāo)準(zhǔn),Apple 也沒(méi)有開(kāi)箱即用的提供,也顯得不重要,開(kāi)發(fā)者都不怎么考慮它。

引用 Aspect Oriented Programming 維基頁(yè)面:

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). (一個(gè)切面可以通過(guò)在多個(gè) join points 中 實(shí)行 advice 改變基礎(chǔ)代碼的行為(程序的非切面的部分) )

在 Objective-C 的世界里,這意味著使用運(yùn)行時(shí)的特性來(lái)為 切面 增加適合的代碼。通過(guò)切面增加的行為可以是:

  • 在類的特定方法調(diào)用前運(yùn)行特定的代碼
  • 在類的特定方法調(diào)用后運(yùn)行特定的代碼
  • 增加代碼來(lái)替代原來(lái)的類的方法的實(shí)現(xiàn)

有很多方法可以達(dá)成這些目的,但是我們沒(méi)有深入挖掘,不過(guò)它們主要都是利用了運(yùn)行時(shí)。 Peter Steinberger 寫了一個(gè)庫(kù),Aspects 完美地適配了 AOP 的思路。我們發(fā)現(xiàn)它值得信賴以及設(shè)計(jì)得非常優(yōu)秀,所以我們就在這邊作為一個(gè)簡(jiǎn)單的例子。

對(duì)于所有的 AOP庫(kù),這個(gè)庫(kù)用運(yùn)行時(shí)做了一些非常酷的魔法,可以替換或者增加一些方法(比 method swizzling 技術(shù)更有技巧性)

Aspect 的 API 有趣并且非常強(qiáng)大:

+ (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;

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

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

換一句話說(shuō):這個(gè)代碼可以讓在 @selector 參數(shù)對(duì)應(yīng)的方法調(diào)用之后,在一個(gè) MyClass 的對(duì)象上(或者在一個(gè)類本身,如果方法是一個(gè)類方法的話)執(zhí)行 block 參數(shù)。

我們?yōu)?MyClass 類的 myMethod: 方法增加了切面。

通常 AOP 用來(lái)實(shí)現(xiàn)橫向切面的完美的適用的地方是統(tǒng)計(jì)和日志。

下面的例子里面,我們會(huì)用AOP用來(lái)進(jìn)行統(tǒng)計(jì)。統(tǒng)計(jì)是iOS項(xiàng)目里面一個(gè)熱門的特性,有很多選擇比如 Google Analytics, Flurry, MixPanel, 等等.

大部分統(tǒng)計(jì)框架都有教程來(lái)指導(dǎo)如何追蹤特定的界面和事件,包括在每一個(gè)類里寫幾行代碼。

在 Ray Wenderlich 的博客里有 文章 和一些示例代碼,通過(guò)在你的 view controller 里面加入 Google Analytics 進(jìn)行統(tǒng)計(jì)。

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

上面的代碼在按鈕點(diǎn)擊的時(shí)候發(fā)送了特定的上下文事件。但是當(dāng)你想追蹤屏幕的時(shí)候會(huì)更糟糕。

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

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

對(duì)于大部分有經(jīng)驗(yàn)的iOS工程師,這看起來(lái)不是很好的代碼。我們讓 view controller 變得更糟糕了。因?yàn)槲覀兗尤肓私y(tǒng)計(jì)事件的代碼,但是它不是 view controller 的職能。你可以反駁,因?yàn)槟阃ǔS刑囟ǖ膶?duì)象來(lái)負(fù)責(zé)統(tǒng)計(jì)追蹤,并且你將代碼注入了 view controller ,但是無(wú)論你隱藏邏輯,問(wèn)題仍然存在 :你最后還是在viewDidAppear: 后插入了代碼。

你可以用 AOP 來(lái)追蹤屏幕視圖來(lái)修改 viewDidAppear: 方法。同時(shí),我們可以用同樣的方法,來(lái)在其他感興趣的方法里面加入事件追蹤,比如任何用戶點(diǎn)擊按鈕的時(shí)候(比如頻繁地調(diào)用IBAction)

這個(gè)方法是干凈并且非侵入性的:

  • 這個(gè) view controller 不會(huì)被不屬于它的代碼污染
  • 為所有加入到我們代碼的切面定義一個(gè) SPOC 文件 (single point of customization)提供了可能
  • SPOC 應(yīng)該在 App 剛開(kāi)始啟動(dòng)的時(shí)候就加入切面
  • 公司負(fù)責(zé)統(tǒng)計(jì)的團(tuán)隊(duì)通常會(huì)提供統(tǒng)計(jì)文檔,羅列出需要追蹤的事件。這個(gè)文檔可以很容易映射到一個(gè) SPOC 文件。
  • 追蹤邏輯抽象化之后,擴(kuò)展到很多其他統(tǒng)計(jì)框架會(huì)很方便
  • 對(duì)于屏幕視圖,對(duì)于需要定義 selector 的方法,只需要在 SPOC 文件修改相關(guān)的類(相關(guān)的切面會(huì)加入到 viewDidAppear: 方法)。如果要同時(shí)發(fā)送屏幕視圖和時(shí)間,一個(gè)追蹤的 label 和其他元信息來(lái)提供額外數(shù)據(jù)(取決于統(tǒng)計(jì)提供方)

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

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"
                }
             ]
    };
}

這個(gè)提及的架構(gòu)在 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];
            });
        }];

    }

    // 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];
            });
        }];

    }
}
上一篇:下一篇:對(duì)象間的通訊