Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區(qū)內(nèi)沒有那么有名誊册,但是 AOP 在運(yùn)行時可以有巨大威力宰僧。 但是因?yàn)闆]有事實(shí)上的標(biāo)準(zhǔn)笋敞,Apple 也沒有開箱即用的提供,也顯得不重要挪凑,開發(fā)者都不怎么考慮它孕索。
引用 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 中附加的行為來改變基礎(chǔ)代碼的行為(程序的非切面的部分) )
在 Objective-C 的世界里,這意味著使用運(yùn)行時的特性來為指定的方法追加 切面 躏碳。切面所附加的行為可以是這樣的:
- 在類的特定方法調(diào)用前運(yùn)行特定的代碼
- 在類的特定方法調(diào)用后運(yùn)行特定的代碼
- 增加代碼來替代原來的類的方法的實(shí)現(xiàn)
有很多方法可以達(dá)成這些目的搞旭,但是我們沒有深入挖掘,不過它們主要都是利用了運(yùn)行時。 Peter Steinberger 寫了一個庫肄渗,Aspects 完美地適配了 AOP 的思路镇眷。我們發(fā)現(xiàn)它值得信賴以及設(shè)計得非常優(yōu)秀,所以我們就在這邊作為一個簡單的例子翎嫡。
對于所有的 AOP庫欠动,這個庫用運(yùn)行時做了一些非常酷的魔法惑申,可以替換或者增加一些方法(比 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;
比如具伍,下面的代碼會對于執(zhí)行 MyClass 類的 myMethod: (實(shí)例或者類的方法) 執(zhí)行塊參數(shù)。
[MyClass aspect_hookSelector:@selector(myMethod:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
...
}
error:nil];
換一句話說:任意的 MyClass 類型的對象(或者是類型本身當(dāng)這個 @selector 方法為類方法時)的 @selector 方法執(zhí)行完后硝桩,就會執(zhí)行這個代碼中塊參數(shù)所提供的代碼沿猜。
我們?yōu)?MyClass 類的 myMethod: 方法增加了切面。
通常 AOP 被用來實(shí)現(xiàn)橫向切面碗脊。統(tǒng)計與日志就是一個完美的例子啼肩。
下面的例子里面,我們會用AOP用來進(jìn)行統(tǒng)計衙伶。統(tǒng)計是iOS項(xiàng)目里面一個熱門的特性祈坠,有很多選擇比如 Google Analytics, Flurry, MixPanel, 等等.
大部分統(tǒng)計框架都有教程來指導(dǎo)如何追蹤特定的界面和事件,包括在每一個類里寫幾行代碼。
在 Ray Wenderlich 的博客里有 文章 和一些示例代碼炮捧,通過在你的 view controller 里面加入 Google Analytics 進(jìn)行統(tǒng)計忽冻。
- (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)擊的時候發(fā)送了特定的上下文事件。但是當(dāng)你想追蹤屏幕的時候會變得很糟躺同。
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value:@"Stopwatch"];
[tracker send:[[GAIDictionaryBuilder createAppView] build]];
}
對于大部分有經(jīng)驗(yàn)的iOS工程師,這看起來不是很好的代碼丸逸。我們讓 view controller 變得更糟糕了蹋艺。因?yàn)槲覀兗尤肓私y(tǒng)計事件的代碼,但是它不是 view controller 的職能黄刚。你可以反駁捎谨,因?yàn)槟阃ǔS刑囟ǖ膶ο髞碡?fù)責(zé)統(tǒng)計追蹤,并且你將代碼注入了 view controller 憔维,但是無論你隱藏邏輯涛救,問題仍然存在 :你最后還是在viewDidAppear: 后插入了代碼。
我們可以在類的 viewDidAppear: 方法上使用 AOP 來追蹤屏幕业扒,并且我們可以使用同樣的方法在其他我們感興趣的方法上添加事件追蹤检吆。比如當(dāng)用戶點(diǎn)擊某個按鈕時(比如:一般調(diào)用對應(yīng)的 IBAction).
方法很簡潔且不具侵入性:
- view controller 不會被不屬于它的代碼污染
- 為所有加入到我們代碼的切面指定一個 SPOC 文件 (single point of customization)提供了可能
- SPOC 應(yīng)該在 App 剛開始啟動的時候用來添加切面
- 如果SPOC文件異常,至少有一個 selector 或者 類 識別不出來,應(yīng)用將會在啟動時崩潰(對我們來說這很酷).
- 公司負(fù)責(zé)統(tǒng)計的團(tuán)隊通常會提供統(tǒng)計文檔程储,羅列出需要追蹤的事件咧栗。這個文檔可以很容易映射到一個 SPOC 文件逆甜。
- 追蹤邏輯抽象化之后,擴(kuò)展到很多其他統(tǒng)計框架會很方便
- 對于屏幕視圖致板,對于需要定義 selector 的方法交煞,只需要在 SPOC 文件修改相關(guān)的類(相關(guān)的切面會加入到 viewDidAppear: 方法)。如果要同時發(fā)送屏幕視圖和事件斟或,需要(依靠統(tǒng)計提供方)提供一個追蹤的標(biāo)示或者可能還需要提供其他的元信息素征。
我們可能希望一個 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"
}
]
};
}
提及的架構(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];
});
}
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];
}];
}
}