Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區(qū)內沒有那么有名,但是 AOP 在運行時可以有巨大威力。 但是因為沒有事實上的標準,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 中 實行 advice 改變基礎代碼的行為(程序的非切面的部分) )
在 Objective-C 的世界里,這意味著使用運行時的特性來為 切面 增加適合的代碼。通過切面增加的行為可以是:
有很多方法可以達成這些目的,但是我們沒有深入挖掘,不過它們主要都是利用了運行時。 Peter Steinberger 寫了一個庫,Aspects 完美地適配了 AOP 的思路。我們發(fā)現它值得信賴以及設計得非常優(yōu)秀,所以我們就在這邊作為一個簡單的例子。
對于所有的 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;
比如,下面的代碼會對于執(zhí)行 MyClass
類的 myMethod:
(實例或者類的方法) 執(zhí)行塊參數。
[MyClass aspect_hookSelector:@selector(myMethod:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo) {
...
}
error:nil];
換一句話說:這個代碼可以讓在 @selector
參數對應的方法調用之后,在一個 MyClass
的對象上(或者在一個類本身,如果方法是一個類方法的話)執(zhí)行 block 參數。
我們?yōu)?MyClass
類的 myMethod:
方法增加了切面。
通常 AOP 用來實現橫向切面的完美的適用的地方是統(tǒng)計和日志。
下面的例子里面,我們會用AOP用來進行統(tǒng)計。統(tǒng)計是iOS項目里面一個熱門的特性,有很多選擇比如 Google Analytics, Flurry, MixPanel, 等等.
大部分統(tǒng)計框架都有教程來指導如何追蹤特定的界面和事件,包括在每一個類里寫幾行代碼。
在 Ray Wenderlich 的博客里有 文章 和一些示例代碼,通過在你的 view controller 里面加入 Google Analytics 進行統(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]];
}
上面的代碼在按鈕點擊的時候發(fā)送了特定的上下文事件。但是當你想追蹤屏幕的時候會更糟糕。
- (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 變得更糟糕了。因為我們加入了統(tǒng)計事件的代碼,但是它不是 view controller 的職能。你可以反駁,因為你通常有特定的對象來負責統(tǒng)計追蹤,并且你將代碼注入了 view controller ,但是無論你隱藏邏輯,問題仍然存在 :你最后還是在viewDidAppear:
后插入了代碼。
你可以用 AOP 來追蹤屏幕視圖來修改 viewDidAppear:
方法。同時,我們可以用同樣的方法,來在其他感興趣的方法里面加入事件追蹤,比如任何用戶點擊按鈕的時候(比如頻繁地調用IBAction)
這個方法是干凈并且非侵入性的:
viewDidAppear:
方法)。如果要同時發(fā)送屏幕視圖和時間,一個追蹤的 label 和其他元信息來提供額外數據(取決于統(tǒng)計提供方)我們可能希望一個 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];
});
}];
}
// 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];
});
}];
}
}
更多建議: