# AOPDemo **Repository Path**: chenzm_186/AOPDemo ## Basic Information - **Project Name**: AOPDemo - **Description**: ios面向切面编程:强大的AOP 实现方法:日志记录、性能统计、安全控制(这里主要指可变容器的安全处理)、事务处理、异常处理等等。 - **Primary Language**: Objective-C - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2018-08-31 - **Last Updated**: 2023-08-03 ## Categories & Tags **Categories**: ioc-framework, ios-modules **Tags**: None ## README # ios面向切面编程:强大的AOP ### 一、 AOP简介 在了解AOP的同时,最好把OOP的概念也一起了解一下: * [AOP](https://baike.baidu.com/item/AOP/1332219?fr=aladdin): Aspect Oriented Programming 面向切面编程。 * [OOP](https://baike.baidu.com/item/OOP/1152915?fr=aladdin): Object Oriented Programming 面向对象编程。 我喜欢一个关于AOP与OOP这两种思想编程的比喻,形象而生动,容易理解:假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。由此可见,AOP是OOP的一个有效补充。 **注意:** AOP不是一种技术,实际上是编程思想。凡是符合AOP思想的技术,都可以看成是AOP的实现 ### 二、AOP的功能和用途 主要的功能是:[日志记录](https://blog.csdn.net/chaoyuan899/article/details/38639829)、[性能统计](https://www.cnblogs.com/tanzhenblog/p/5001344.html)、[安全控制](https://blog.csdn.net/sdlg2015/article/details/50198219)(这里主要指可变容器的安全处理)、事务处理、[异常处理](https://blog.csdn.net/jianandjan/article/details/50240111)等等。 主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。 ### 三、优点 可以通过预编译方式和运行期动态代理实现**在不修改源代码的情况下给程序动态统一添加功能的一种技术**。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。 ### 四、iOS中的AOP 利用 Obj-C 的 Runtime 特性(运行时编程),给语言做扩展,帮助解决项目开发中的一些设计和技术问题。 * AOP的优势: 1. 减少切面业务的开发量,“一次开发终生使用”,比如日志 2. 减少代码耦合,方便复用。切面业务的代码可以独立出来,方便其他应用使用 3. 提高代码review的质量,比如我可以规定某些类的某些方法才用特定的命名规范,这样review的时候就可以发现一些问题 * AOP的弊端: 1. 它破坏了代码的干净整洁。(因为 AOP部分 的代码本身并不属于 ViewController 里的主要逻辑。随着项目扩大、代码量增加,你的 ViewController 里会到处散布着 Logging 的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码) 2. 由于改模块本身的独立性,在修改代码的时候,容易遗忘对该部分代码的调整。 ### 五、Aspects[应用广泛的AOP开发]框架的应用 Aspects是一个很不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API ```oc /** 隐藏导航栏 @param selector:表示要拦截指定对象的方法 @param selector:options是一个枚举类型: AspectPositionAfter表示方法执行后会触发usingBlock:的代码; AspectPositionBefore表示方法执行前会触发usingBlock:的代码; AspectPositionInstead表示替代方法执行直接触发usingBlock:的代码 @param options :就是拦截事件后执行的自定义方法。我们可以在这个block里面添加我们要执行的代码。 */ + (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; ``` **注:** Aspect不支持不同类中含有相同名称的方法时,也不支持重复调用同类的同一方法,会出现不能正确替换或业务处理的情况 ##### 1、事务处理 在不影响整个APP项目的情况下,将一些独立的小功能集成到项目,比如大转盘功能,搜索更能,日志写入,第三方物流信息请求列表实现、二维码扫描等一些可以独立于项目的小功能。以下为实现页面跳转的功能 1、将Aspects导入项目,可以手动导入,也可以使用Pod导入项目 ```oc platform :ios, '8.0' target '项目名称' do #AOP的库 pod 'Aspects', '1.4.1',:inhibit_warnings => true end ``` 2、创建一个扩展类并添加【Aspects】类目,然后在这里实现切入的位置并实现方法 ```oc #import "AppDelegate.h" @interface AppDelegate (SmallFeature) ///小功能添加 -(void)setSmallFeature; @end ``` ```oc #import "AppDelegate+SmallFeature.h" #import @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,UIButton *btn){ Class snatchTreasure = NSClassFromString(@"ZMSecVC"); [weakSelf.navigationController pushViewController:[snatchTreasure new] animated:YES]; } error:NULL]; } ``` 3、调用方法并实现 ```oc #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) UINavigationController * navigationController; @end ``` ```oc #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](https://gitee.com/chenzm_186/AOPDemo)】查看。 ##### 3、日志记录 我们在项目中收集用户的日志,以及用户行为,以用来分析Bug,以及提升产品质量。稍微大一点的项目往往包含很多的模块,以及下面会有更多的子模块,所以如果把收集日志的操作具体加载到每个事件中,显然这种做法是不可取的。其原因有二: > 第一:所有收集用户行为的操作不属于业务逻辑范畴,我们不需要分散到各个业务中。 > 第二:这种方式的添加不利于后期维护,而且改动量是巨大的。 方法实现: ```oc // // ZMLogging.h // AOPDemo // // Created by chenzm on 2018/8/24. // Copyright © 2018年 chenzm. All rights reserved. // #import #import #define ZMLoggingPageImpression @"ZMLoggingPageImpression" #define ZMLoggingTrackedEvents @"ZMLoggingTrackedEvents" #define ZMLoggingEventName @"ZMLoggingEventName" #define ZMLoggingEventSelectorName @"ZMLoggingEventSelectorName" #define ZMLoggingEventHandlerBlock @"ZMLoggingEventHandlerBlock" @interface ZMLogging : NSObject + (void)setupWithConfiguration:(NSDictionary *)configs; @end ``` ```oc /** + (id)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); + (void)setupWithConfiguration:(NSDictionary *)configs{ // 页面统计 [UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id 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) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //代码块事件处理 block(aspectInfo); }); } error:NULL]; } } } } @end ``` ```oc // // 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 ``` 日志信息配置: ```oc // // 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) { NSLog(@"btnAction1"); }, }, @{ ZMLoggingEventName: @"button 2 clicked", ZMLoggingEventSelectorName: @"btnAction2:", ZMLoggingEventHandlerBlock: ^(id aspectInfo) { NSLog(@"btnAction2"); }, }, ], }, @"ZMThiVC": @{ ZMLoggingPageImpression: @"page imp - ZMThiVC page", }, @"ZMMenuView":@{ ZMLoggingTrackedEvents: @[ @{ ZMLoggingEventName: @"ZMMenuView", ZMLoggingEventSelectorName: @"menuButtonClick:", ZMLoggingEventHandlerBlock: ^(id aspectInfo) { NSLog(@"menuButtonClick"); }, }, ], } }; [ZMLogging setupWithConfiguration:config]; } @end ``` 方法实现调用 ```oc #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 )难免有空操作、指针越界的问题。 最粗暴的方式就是在使用可变容器的时间,每次操作都必须手动做空判断、索引比较这些操作: ```oc 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]); } ``` 或者简单点的,用宏定义做判空处理,: ```oc // 字符串 #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: ```oc @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; } ``` ```oc - (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: 。 [下载示例源码地址](https://gitee.com/chenzm_186/AOPDemo) ##### 参考链接: 1、[iOS面向切面编程-AOP](https://blog.csdn.net/yidu_blog/article/details/53125994) 2、[iOS中利用AOP(面向切面)原理实现拦截者功能 超详细过程](https://www.jianshu.com/p/97017f88f584) 3、[Aspects](https://github.com/steipete/Aspects/issues/48)