1. AOP簡介
AOP: Aspect Oriented Programming 面向切面程式設計。
面向切面程式設計(也叫面向方面):Aspect Oriented Programming(AOP),是目前軟體開發中的一個熱點。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。
AOP是OOP的延續,是(Aspect Oriented Programming)的縮寫,意思是面向切面(方面)程式設計。
主要的功能是:日誌記錄,效能統計,安全控制,事務處理,異常處理等等。
主要的意圖是:將日誌記錄,效能統計,安全控制,事務處理,異常處理等程式碼從業務邏輯程式碼中劃分出來,通過對這些行為的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改 變這些行為的時候不影響業務邏輯的程式碼。
可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的一種技術。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是呼叫者和被呼叫者之間的解耦,AOP可以說也是這種目標的一種實現。
假設把應用程式想成一個立體結構的話,OOP的利刃是縱向切入系統,把系統劃分為很多個模組(如:使用者模組,文章模組等等),而AOP的利刃是橫向切入系統,提取各個模組可能都要重複操作的部分(如:許可權檢查,日誌記錄等等)。由此可見,AOP是OOP的一個有效補充。
注意:AOP不是一種技術,實際上是程式設計思想。凡是符合AOP思想的技術,都可以看成是AOP的實現
2. iOS中的AOP
利用 Objective-C 的 Runtime 特性,我們可以給語言做擴充套件,幫助解決專案開發中的一些設計和技術問題。這一篇,我們來探索一些利用 Objective-C Runtime 的黑色技巧。這些技巧中最具爭議的或許就是 Method Swizzling 。
其次,用不用就看專案規模和團隊規模。有些業務確實非常適合使用AOP,比如log,AOP還可以用來debug
AOP的優勢:
- 減少切面業務的開發量,“一次開發終生使用”,比如日誌
- 減少程式碼耦合,方便複用。切面業務的程式碼可以獨立出來,方便其他應用使用
- 提高程式碼review的質量,比如我可以規定某些類的某些方法才用特定的命名規範,這樣review的時候就可以發現一些問題
AOP的弊端:
- 它破壞了程式碼的乾淨整潔。
(因為 Logging 的程式碼本身並不屬於 ViewController 裡的主要邏輯。隨著專案擴大、程式碼量增加,你的 ViewController 裡會到處散佈著 Logging 的程式碼。這時,要找到一段事件記錄的程式碼會變得困難,也很容易忘記新增事件記錄的程式碼)
3. iOS AOP實戰
玩轉 Method Swizzling
1.事務攔截,安全可變容器
iOS中有各類容器的概念,容器分可變容器和非可變容器,可變容器一般內部在實現上是一個連結串列,在進行各類(insert 、remove、 delete、 update )難免有空操作、指標越界的問題。
最粗暴的方式就是在使用可變容器的時間,每次操作都必須手動做空判斷、索引比較這些操作:
1 2 3 4 5 6 7 |
NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; if (obj) { [dic setObject:obj forKey:@"key"]; } NSMutableArray *array = [[NSMutableArray alloc] init]; if (index |
在程式碼中大量的使用這鞋操作實在是太過於繁瑣了,試想如果可變容器自身如何能做這些相容豈不是更好。可能會想到繼承的方法來解決,但是專案中儘可能的避免過多的派生(至於派生的弊端這裡就不多說了);或者想到分類,這裡也不盡人意。
Method Swizzling 移花接木
runtime 這裡就不多多說了(swift裡面已經對這個概念的說法從心轉變成了 Reflection),objective c中每個方法的名字(SEL)跟函式的實現(IMP)是一一對應的,Swizzle的原理只是在這個地方做下手腳,將原來方法名與實現的指向交叉處理,就能達到一個新的效果。
廢話少說,直接上程式碼:
這裡使用NSMutableArray 做例項,為NSMutableArray追加一個新的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@implementation NSMutableArray (safe) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ id obj = [[self alloc] init]; [obj swizzleMethod:@selector(addObject:) withMethod:@selector(safeAddObject:)]; [obj swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex:)]; [obj swizzleMethod:@selector(insertObject:atIndex:) withMethod:@selector(safeInsertObject:atIndex:)]; [obj swizzleMethod:@selector(removeObjectAtIndex:) withMethod:@selector(safeRemoveObjectAtIndex:)]; [obj swizzleMethod:@selector(replaceObjectAtIndex:withObject:) withMethod:@selector(safeReplaceObjectAtIndex:withObject:)]; }); } - (void)safeAddObject:(id)anObject { if (anObject) { [self safeAddObject:anObject]; }else{ NSLog(@"obj is nil"); } } - (id)safeObjectAtIndex:(NSInteger)index { if(index |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector { Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, origSelector); Method swizzledMethod = class_getInstanceMethod(class, newSelector); BOOL didAddMethod = class_addMethod(class, origSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, newSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } |
這裡唯一可能需要解釋的是 class_addMethod 。要先嚐試新增原 selector 是為了做一層保護,因為如果這個類沒有實現 originalSelector ,但其父類實現了,那 class_getInstanceMethod 會返回父類的方法。這樣 method_exchangeImplementations 替換的是父類的那個方法,這當然不是你想要的。所以我們先嚐試新增 orginalSelector ,如果已經存在,再用 method_exchangeImplementations 把原方法的實現跟新的方法實現給交換掉。
safeAddObject 程式碼看起來可能有點奇怪,像遞迴不是麼。當然不會是遞迴,因為在 runtime 的時候,函式實現已經被交換了。呼叫 objectAtIndex: 會呼叫你實現的 safeObjectAtIndex:,而在 NSMutableArray: 裡呼叫 safeObjectAtIndex: 實際上呼叫的是原來的 objectAtIndex: 。
如此以來,一直擔心的問題就迎刃而解了,不僅在可變陣列、可變字典等容器內都可以做自己想做的事情。
2. Aspects 一個基於Objective-c的AOP開發框架
業務埋點、日誌列印分離
相信大多童鞋們在重構程式碼的時間經常會從一些問題入手,例如輕量級controller、MVVM等,這些無非是對原有邏輯進一步抽象、區分、分離,重新抽象資料模型、viewmodel;相關程式碼放入分類;考慮業務層次抽取剝離父類;mananger、factory等。經歷一大翻工作controller 中程式碼終於減少了,但是仍舊留下一堆的埋點、日誌log的相關程式碼。
Aspects是一個很不錯的 AOP 庫,封裝了 Runtime , Method Swizzling 這些黑色技巧,只提供兩個簡單的API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (id)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; - (id)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) { NSString *className = NSStringFromClass([[aspectInfo instance] class]); [Logging logWithEventName:className]; } error:NULL]; } |
相對來說如果想要捕捉到viewDidAppear 的log列印,或者是頁面PV的統計上報,我們從原有的業務中將這部分程式碼剝離出來,掉換IMP指向以後我們可以在usingBlock 內做自己想做的事情, 這樣就能達到上述想要的目的了。
Aspects擴充套件使用:
頁面的PV統計,事件點選統計,可以事先寫在配置檔案裡面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ @"MainViewController": @{ GLLoggingPageImpression: @"page imp - main page", GLLoggingTrackedEvents: @[ @{ GLLoggingEventName: @"button one clicked", GLLoggingEventSelectorName: @"buttonOneClicked:", GLLoggingEventHandlerBlock: ^(id aspectInfo) { [Logging logWithEventName:@"button one clicked"]; }, }, @{ GLLoggingEventName: @"button two clicked", GLLoggingEventSelectorName: @"buttonTwoClicked:", GLLoggingEventHandlerBlock: ^(id aspectInfo) { [Logging logWithEventName:@"button two clicked"]; }, }, ], }, @"DetailViewController": @{ GLLoggingPageImpression: @"page imp - detail page", } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@implementation AppDelegate (Logging) + (void)setupLogging{ [AppDelegate setupWithConfiguration:config]; } + (void)setupWithConfiguration:(NSDictionary *)configs { // Hook Page Impression [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id 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) { block(aspectInfo); } error:NULL]; } } } } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [self setupLogging]; return YES; } |