ios面向切面编程:强大的AOP

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 利用 Obj-C 的 Runtime 特性(运行时编程),给语言做扩展,帮助解决项目开发中的一些设计和技术问题。

一、 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: 。


下载示例源码地址


参考链接:

1、iOS面向切面编程-AOP

2、iOS中利用AOP(面向切面)原理实现拦截者功能 超详细过程

3、Aspects


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
84 1
|
2月前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
2月前
|
安全 Java 编译器
什么是AOP面向切面编程?怎么简单理解?
本文介绍了面向切面编程(AOP)的基本概念和原理,解释了如何通过分离横切关注点(如日志、事务管理等)来增强代码的模块化和可维护性。AOP的核心概念包括切面、连接点、切入点、通知和织入。文章还提供了一个使用Spring AOP的简单示例,展示了如何定义和应用切面。
192 1
什么是AOP面向切面编程?怎么简单理解?
|
2月前
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
72 5
|
4月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
【9月更文挑战第9天】AOP(面向切面编程)通过分离横切关注点提高模块化程度,如日志记录、事务管理等。Micronaut AOP基于动态代理机制,在应用启动时为带有特定注解的类生成代理对象,实现在运行时拦截方法调用并执行额外逻辑。通过简单示例展示了如何在不修改 `CalculatorService` 类的情况下记录 `add` 方法的参数和结果,仅需添加 `@Loggable` 注解即可。这不仅提高了代码的可维护性和可扩展性,还降低了引入新错误的风险。
54 13
|
3月前
|
Java 容器
AOP面向切面编程
AOP面向切面编程
52 0
|
4月前
|
Swift iOS开发 UED
揭秘一款iOS应用中令人惊叹的自定义动画效果,带你领略编程艺术的魅力所在!
【9月更文挑战第5天】本文通过具体案例介绍如何在iOS应用中使用Swift与UIKit实现自定义按钮动画,当用户点击按钮时,按钮将从圆形变为椭圆形并从蓝色渐变到绿色,释放后恢复原状。文中详细展示了代码实现过程及动画平滑过渡的技巧,帮助读者提升应用的视觉体验与特色。
75 11
|
5月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
5月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
74 0
|
5月前
|
监控 安全 数据库
面向方面编程(AOP)的概念
【8月更文挑战第22天】
91 0