一、 AOP简介
在了解AOP的同时,最好把OOP的概念也一起了解一下:
* AOP: Aspect Oriented Programming 面向切面编程。
* OOP: Object Oriented Programming 面向对象编程。
我喜欢一个关于AOP与OOP这两种思想编程的比喻,形象而生动,容易理解:假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。由此可见,AOP是OOP的一个有效补充。
注意: AOP不是一种技术,实际上是编程思想。凡是符合AOP思想的技术,都可以看成是AOP的实现
二、AOP的功能和用途
主要的功能是:日志记录、性能统计、安全控制(这里主要指可变容器的安全处理)、事务处理、异常处理等等。
主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
三、优点
可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。
四、iOS中的AOP
利用 Obj-C 的 Runtime 特性(运行时编程),给语言做扩展,帮助解决项目开发中的一些设计和技术问题。
AOP的优势:
减少切面业务的开发量,“一次开发终生使用”,比如日志
减少代码耦合,方便复用。切面业务的代码可以独立出来,方便其他应用使用
提高代码review的质量,比如我可以规定某些类的某些方法才用特定的命名规范,这样review的时候就可以发现一些问题
AOP的弊端:
它破坏了代码的干净整洁。(因为 AOP部分 的代码本身并不属于 ViewController 里的主要逻辑。随着项目扩大、代码量增加,你的 ViewController 里会到处散布着 Logging 的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码)
由于改模块本身的独立性,在修改代码的时候,容易遗忘对该部分代码的调整。
五、Aspects[应用广泛的AOP开发]框架的应用
Aspects是一个很不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API
/** 隐藏导航栏 @param selector:表示要拦截指定对象的方法 @param selector:options是一个枚举类型: AspectPositionAfter表示方法执行后会触发usingBlock:的代码; AspectPositionBefore表示方法执行前会触发usingBlock:的代码; AspectPositionInstead表示替代方法执行直接触发usingBlock:的代码 @param options :就是拦截事件后执行的自定义方法。我们可以在这个block里面添加我们要执行的代码。 */ + (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;
注: Aspect不支持不同类中含有相同名称的方法时,也不支持重复调用同类的同一方法,会出现不能正确替换或业务处理的情况
1、事务处理
在不影响整个APP项目的情况下,将一些独立的小功能集成到项目,比如大转盘功能,搜索更能,日志写入,第三方物流信息请求列表实现、二维码扫描等一些可以独立于项目的小功能。以下为实现页面跳转的功能
1、将Aspects导入项目,可以手动导入,也可以使用Pod导入项目
platform :ios, '8.0' target '项目名称' do #AOP的库 pod 'Aspects', '1.4.1',:inhibit_warnings => true end
2、创建一个扩展类并添加【Aspects】类目,然后在这里实现切入的位置并实现方法
#import "AppDelegate.h" @interface AppDelegate (SmallFeature) ///小功能添加 -(void)setSmallFeature; @end
#i
mport "AppDelegate+SmallFeature.h" #import <Aspects.h> @implementation AppDelegate (SmallFeature) ///小功能添加 -(void)setSmallFeature{ [self jumpToFirVC]; } ///跳转页面(此位置可以跳转二维码扫面等的独立页面) -(void)jumpToFirVC{ Class firVC = NSClassFromString(@"ZMFirVC"); __weak typeof(self) weakSelf = self; SEL action = NSSelectorFromString(@"btnAction:"); [firVC aspect_hookSelector:action withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> aspectInfo,UIButton *btn){ Class snatchTreasure = NSClassFromString(@"ZMSecVC"); [weakSelf.navigationController pushViewController:[snatchTreasure new] animated:YES]; } error:NULL]; }
3、调用方法并实现
#import <UIKit/UIKit.h> @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) UINavigationController * navigationController; @end
#import "AppDelegate.h" #import "AppDelegate+SmallFeature.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self setSmallFeature]; return YES; }
2、友盟自定义统计分析
个人觉得AOP在这一块的使用还是很方便的,可以独立成块,一劳永逸。在这里我创建了一个小工具类【ZMMobClickTool】,对自定义统计接口的进一步继承封装;一个扩展类【AppDelegate+AopUMStatistical】,实现在需要统计的位置切入;两个【xxx.plist】文件,一个用于列表页面的选择性上传,一个是将统计事件列表化。由于代码比较多,请直接【下载Demo】查看。
3、日志记录
我们在项目中收集用户的日志,以及用户行为,以用来分析Bug,以及提升产品质量。稍微大一点的项目往往包含很多的模块,以及下面会有更多的子模块,所以如果把收集日志的操作具体加载到每个事件中,显然这种做法是不可取的。其原因有二:
第一:所有收集用户行为的操作不属于业务逻辑范畴,我们不需要分散到各个业务中。
第二:这种方式的添加不利于后期维护,而且改动量是巨大的。
方法实现:
// // ZMLogging.h // AOPDemo // // Created by chenzm on 2018/8/24. // Copyright © 2018年 chenzm. All rights reserved. // #import <Foundation/Foundation.h> #import <Aspects.h> #define ZMLoggingPageImpression @"ZMLoggingPageImpression" #define ZMLoggingTrackedEvents @"ZMLoggingTrackedEvents" #define ZMLoggingEventName @"ZMLoggingEventName" #define ZMLoggingEventSelectorName @"ZMLoggingEventSelectorName" #define ZMLoggingEventHandlerBlock @"ZMLoggingEventHandlerBlock" @interface ZMLogging : NSObject + (void)setupWithConfiguration:(NSDictionary *)configs; @end
/** + (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; 1、aspect_hookSelector:表示要拦截指定对象的方法。 2、withOptions:是一个枚举类型,AspectPositionAfter表示viewDidLoad方法执行后会触发usingBlock:的代码。 3、usingBlock:就是拦截事件后执行的自定义方法。我们可以在这个block里面添加我们要执行的代码。 */ // // ZMLogging.m // AOPDemo // // Created by chenzm on 2018/8/24. // Copyright © 2018年 chenzm. All rights reserved. // #import "ZMLogging.h" @import UIKit; @implementation ZMLogging typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo); + (void)setupWithConfiguration:(NSDictionary *)configs{ // 页面统计 [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *className = NSStringFromClass([[aspectInfo instance] class]); NSString *pageImp = configs[className][ZMLoggingPageImpression]; if (pageImp) { //监听对象处理 NSLog(@"%@", pageImp); } }); } error:NULL]; // 事件处理 for (NSString *className in configs) { Class clazz = NSClassFromString(className); NSDictionary *config = configs[className]; if (config[ZMLoggingTrackedEvents]) { for (NSDictionary *event in config[ZMLoggingTrackedEvents]) { SEL selekor = NSSelectorFromString(event[ZMLoggingEventSelectorName]); AspectHandlerBlock block = event[ZMLoggingEventHandlerBlock]; [clazz aspect_hookSelector:selekor withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //代码块事件处理 block(aspectInfo); }); } error:NULL]; } } } } @end
// // AppDelegate+Logging.h // AOPDemo // // Created by chenzm on 2018/8/24. // Copyright © 2018年 chenzm. All rights reserved. // #import "AppDelegate.h" @interface AppDelegate (Logging) - (void)setupLogging; @end
日志信息配置:
// // AppDelegate+Logging.m // AOPDemo // // Created by chenzm on 2018/8/24. // Copyright © 2018年 chenzm. All rights reserved. // #import "AppDelegate+Logging.h" #import "ZMLogging.h" @implementation AppDelegate (Logging) - (void)setupLogging{ NSDictionary *config = @{ @"ZMSecVC": @{ ZMLoggingPageImpression: @"page imp - ZMSecVC page", ZMLoggingTrackedEvents: @[ @{ ZMLoggingEventName: @"button 1 clicked", ZMLoggingEventSelectorName: @"btnAction1:", ZMLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) { NSLog(@"btnAction1"); }, }, @{ ZMLoggingEventName: @"button 2 clicked", ZMLoggingEventSelectorName: @"btnAction2:", ZMLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) { NSLog(@"btnAction2"); }, }, ], }, @"ZMThiVC": @{ ZMLoggingPageImpression: @"page imp - ZMThiVC page", }, @"ZMMenuView":@{ ZMLoggingTrackedEvents: @[ @{ ZMLoggingEventName: @"ZMMenuView", ZMLoggingEventSelectorName: @"menuButtonClick:", ZMLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) { NSLog(@"menuButtonClick"); }, }, ], } }; [ZMLogging setupWithConfiguration:config]; } @end
方法实现调用
#import "AppDelegate.h" #import "AppDelegate+Logging.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self setupLogging]; return YES; }
4、事务拦截,安全可变容
iOS中有各类容器的概念,容器分可变容器和非可变容器(如可变数组、不可变数组),可变容器一般内部在实现上是一个链表,在进行各类(insert 、remove、 delete、 update )难免有空操作、指针越界的问题。
最粗暴的方式就是在使用可变容器的时间,每次操作都必须手动做空判断、索引比较这些操作:
NSMutableDictionary *dic = [[NSMutableDictionary alloc] init]; if (obj) { [dic setObject:obj forKey:@"key"]; } NSMutableArray *array = [[NSMutableArray alloc] init]; if (index < array.count) { NSLog(@"%@",[array objectAtIndex:index]); }
或者简单点的,用宏定义做判空处理,:
// 字符串 #define kIsEmptyStr(str) ([str isKindOfClass:[NSNull class]] || str == nil || [str length] < 1 ? YES : NO ) //长整型转字符串 #define kStrFromInteger(p) ([NSString stringWithFormat:@"%ld",(long)p]) //字符串判空值保护 #define kStrEmpDef(p) (!kIsEmptyStr(p)?[NSString stringWithFormat:@"%@",p]:@"") // 数组 #define kIsEmptyArr(array) (array == nil || [array isKindOfClass:[NSNull class]] || array.count == 0) // 字典 #define kIsEmptyDic(dic) (dic == nil || [dic isKindOfClass:[NSNull class]] || dic.allKeys == 0) // 对象 #define kIsEmptyObj(_object) (_object == nil \ || [_object isKindOfClass:[NSNull class]] \ || ([_object respondsToSelector:@selector(length)] && [(NSData *)_object length] == 0) \ || ([_object respondsToSelector:@selector(count)] && [(NSArray *)_object count] == 0))
但是这些还是需要使用大量的代码操作,这时候就会想到从可变容器本身下手,采用【Method Swizzling】方法实现方法替代重写。当然了,刚开始要重写这些容器会比较麻烦,但是可以一劳永逸(懒人的最高境界)啊。直接上代码:
这里使用NSMutableArray 做实例,为NSMutableArray追加一个新的方法,借用一下别人的Demo:
@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<[self count]){ return [self safeObjectAtIndex:index]; }else{ NSLog(@"index is beyond bounds "); } return nil; }
- (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、iOS中利用AOP(面向切面)原理实现拦截者功能 超详细过程
3、Aspects