iOS面向切面编程笔记:UIButton按钮防连点、NSArray数组越界、数据打点

2,634 阅读8分钟

面向切面编程参考:React Native面向切面编程

iOS中的实现方式:

ObjC 中实现 AOP 最直接的方法就是使用 Runtime 中的 Method Swizzling。使用Aspects, 可以不需要繁琐的手工调用 Method Swizzling

iOS中的应用场景一:数据统计

所谓 AOP 其实就是给你的程序提供一个可拆卸的组件化能力。比如你的 APP 需要用到事件统计功能, 无论你是用 UMeng, Google Analytics, 还是其他的统计平台等等, 你应该都会写过类似的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [Logger log:@"View Did Load"];
    // 下面初始化数据
}

在视图控制器开始加载的时候,用 Logger 类记录一个统计事件。 其实 viewDidLoad 方法本身的逻辑并不是为了完成统计,而是进行一些初始化操作。这就导致了一个设计上的瑕疵, 数据统计的代码和我们实际的业务逻辑代码混杂在一起了。随着业务逻辑代码不断增多,类似的混杂也会越来越多,这样的耦合势必会增加维护的成本。AOP 其实就是在不影响程序整体功能的情况下,将 Logger 这样的逻辑,从主业务逻辑中抽离出来的能力。有了 AOP 之后, 我们的业务逻辑代码就变成了这样:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 下面初始化数据
}

这里不再会出现 Logger 的统计逻辑的代码,但是统计功能依然是生效的。 当然,不出现在主业务代码中,不代表统计代码就消失了。 而是用 AOP 模式 hook 到别的地方去了。

优点:

  • 1、业务隔离 ,解耦。剥离开主业务和统计业务。
  • 2、即插即用。在预发布环境和发布环境测试的时候,不想记录统计数据,只需要把统计业务逻辑模块去掉即可。
  • 3、如果你在哪一天想换一个统计平台, 那么你不需要到处改代码了, 只需要把统计层面的代码修改一下就可以。

缺点:

  • 1、代码不够直观
  • 2、使用不当,出现Bug比较难于调试

iOS中的应用场景二:防止按钮连续点击

网上有一篇文章iOS---防止UIButton重复点击的三种实现方式,经过实践发现文章可以作为一个demo来演示,在真实的项目开发中是不实用的。因为sendAction:to:forEvent:方法是UIControl的方法,所有继承自UIControl的类的这个方法都会被替换,比如UISwitch。下面是针对这篇文章的改进版,确保只有UIButton的改方法被HOOK:

#import <UIKit/UIKit.h>
@interface UIButton (FixMultiClick)
@property (nonatomic, assign) NSTimeInterval clickInterval;
@end

#import "UIButton+FixMultiClick.h"
#import <objc/runtime.h>
#import <Aspects/Aspects.h>
@interface UIButton ()
@property (nonatomic, assign) NSTimeInterval clickTime;
@end
@implementation UIButton (FixMultiClick)
-(NSTimeInterval)clickTime {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickTime:(NSTimeInterval)clickTime {
    objc_setAssociatedObject(self, @selector(clickTime), @(clickTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)clickInterval {
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
-(void)setClickInterval:(NSTimeInterval)clickInterval {
    objc_setAssociatedObject(self, @selector(clickInterval), @(clickInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load {
    [UIButton aspect_hookSelector:@selector(sendAction:to:forEvent:)
                      withOptions:AspectPositionInstead
                       usingBlock:^(id<AspectInfo> info){
        UIButton *obj = info.instance;
        if(obj.clickInterval <= 0){
            [info.originalInvocation invoke];
        }
        else{
            if ([NSDate date].timeIntervalSince1970 - obj.clickTime < obj.clickInterval) {
                return;
            }
            obj.clickTime = [NSDate date].timeIntervalSince1970;
            [info.originalInvocation invoke];
        }
    } error:nil];
}
@end

iOS中的应用场景三:NSArray的数组越界

crash的具体几种情况

  • 取值:index超出array的索引范围
  • 添加:插入的object为nil或者Null
  • 插入:index大于count、插入的object为nil或者Null
  • 删除:index超出array的索引范围
  • 替换:index超出array的索引范围、替换的object为nil或者Null

解决思路: HOOK系统方法,替换为自定义的安全方法

#import <Foundation/Foundation.h>
@interface NSArray (Aspect)
@end

#import "NSArray+Aspect.h"
#import <objc/runtime.h>

@implementation NSArray (Aspect)
/**
 *  对系统方法进行替换
 *
 *  @param systemSelector 被替换的方法
 *  @param swizzledSelector 实际使用的方法
 *  @param error            替换过程中出现的错误消息
 *
 *  @return 是否替换成功
 */
+ (BOOL)systemSelector:(SEL)systemSelector customSelector:(SEL)swizzledSelector error:(NSError *)error{
    Method systemMethod = class_getInstanceMethod(self, systemSelector);
    if (!systemMethod) {
        return NO;
    }
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
    if (!swizzledMethod) {
        return NO;
    }
    if (class_addMethod([self class], systemSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod([self class], swizzledSelector, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
    }
    else{
        method_exchangeImplementations(systemMethod, swizzledMethod);
    }
    return YES;
}

/**
 NSArray 是一个类簇
 */
+(void)load{
    [super load];
    // 越界:初始化的空数组
    [objc_getClass("__NSArray0") systemSelector:@selector(objectAtIndex:)
                               customSelector:@selector(emptyObjectIndex:)
                                          error:nil];
    // 越界:初始化的非空不可变数组
    [objc_getClass("__NSSingleObjectArrayI") systemSelector:@selector(objectAtIndex:)
                                           customSelector:@selector(singleObjectIndex:)
                                                      error:nil];
    // 越界:初始化的非空不可变数组
    [objc_getClass("__NSArrayI") systemSelector:@selector(objectAtIndex:)
                               customSelector:@selector(safe_arrObjectIndex:)
                                          error:nil];
    // 越界:初始化的可变数组
    [objc_getClass("__NSArrayM") systemSelector:@selector(objectAtIndex:)
                               customSelector:@selector(safeObjectIndex:)
                                          error:nil];
    // 越界:未初始化的可变数组和未初始化不可变数组
    [objc_getClass("__NSPlaceholderArray") systemSelector:@selector(objectAtIndex:)
                                         customSelector:@selector(uninitIIndex:)
                                                    error:nil];
    // 越界:可变数组
    [objc_getClass("__NSArrayM") systemSelector:@selector(objectAtIndexedSubscript:)
                               customSelector:@selector(mutableArray_safe_objectAtIndexedSubscript:)
                                          error:nil];
    // 越界vs插入:可变数插入nil,或者插入的位置越界
    [objc_getClass("__NSArrayM") systemSelector:@selector(insertObject:atIndex:)
                               customSelector:@selector(safeInsertObject:atIndex:)
                                          error:nil];
    // 插入:可变数插入nil
    [objc_getClass("__NSArrayM") systemSelector:@selector(addObject:)
                               customSelector:@selector(safeAddObject:)
                                          error:nil];
}
- (id)safe_arrObjectIndex:(NSInteger)index{
    if (index >= self.count || index < 0) {
        NSLog(@"this is crash, [__NSArrayI] check index (objectAtIndex:)") ;
        return nil;
    }
    return [self safe_arrObjectIndex:index];
}
- (id)mutableArray_safe_objectAtIndexedSubscript:(NSInteger)index{
    if (index >= self.count || index < 0) {
        NSLog(@"this is crash, [__NSArrayM] check index (objectAtIndexedSubscript:)") ;
        return nil;
    }
    return [self mutableArray_safe_objectAtIndexedSubscript:index];
}
- (id)singleObjectIndex:(NSUInteger)idx{
    if (idx >= self.count) {
        NSLog(@"this is crash, [__NSSingleObjectArrayI] check index (objectAtIndex:)") ;
        return nil;
    }
    return [self singleObjectIndex:idx];
}
- (id)uninitIIndex:(NSUInteger)idx{
    if ([self isKindOfClass:objc_getClass("__NSPlaceholderArray")]) {
        NSLog(@"this is crash, [__NSPlaceholderArray] check index (objectAtIndex:)") ;
        return nil;
    }
    return [self uninitIIndex:idx];
}
- (id)safeObjectIndex:(NSInteger)index{
    if (index >= self.count || index < 0) {
        NSLog(@"this is crash, [__NSArrayM] check index (objectAtIndex:)") ;
        return nil;
    }
    return [self safeObjectIndex:index];
}
- (void)safeInsertObject:(id)object atIndex:(NSUInteger)index{
    if (index>self.count) {
        NSLog(@"this is crash, [__NSArrayM] check index (insertObject:atIndex:)") ;
        return ;
    }
    if (object == nil) {
        NSLog(@"this is crash, [__NSArrayM] check object == nil (insertObject:atIndex:)") ;
        return ;
    }
    [self safeInsertObject:object atIndex:index];
}
- (void)safeAddObject:(id)object {
    if (object == nil) {
        NSLog(@"this is crash, [__NSArrayM] check index (addObject:)") ;
        return ;
    }
    [self safeAddObject:object];
}
- (id)emptyObjectIndex:(NSInteger)index {
    NSLog(@"this is crash, [__NSArray0] check index (objectAtIndex:)") ;
    return nil;
}
@end

验证

- (void)viewDidLoad {
    [super viewDidLoad];
    NSArray *arr1 =  @[@"1",@"2"];
    NSLog(@"[arr1 objectAtIndex:9527] = %@", [arr1 objectAtIndex:9527]);
    NSLog(@"[arr1 objectAtIndexedSubscript:9527] = %@", [arr1 objectAtIndexedSubscript:9527]);

    NSArray *arr2 =  [[NSArray alloc]init];
    NSLog(@"[arr2 objectAtIndex:9527] = %@", [arr1 objectAtIndex:9527]);
    NSLog(@"[arr2 objectAtIndexedSubscript:9527] = %@", [arr1 objectAtIndexedSubscript:9527]);
    
    NSArray *arr3 =  [[NSArray alloc] initWithObjects:@"1",nil];
    NSLog(@"[arr3 objectAtIndex:9527] = %@", [arr1 objectAtIndex:9527]);
    NSLog(@"[arr3 objectAtIndexedSubscript:2] = %@", [arr3 objectAtIndexedSubscript:2]);

    NSArray *arr4 =  [NSArray alloc];
    NSLog(@"[arr4 objectAtIndex:9527] = %@", [arr4 objectAtIndex:9527]);
    NSLog(@"[arr4 objectAtIndexedSubscript:9527] = %@", [arr4 objectAtIndexedSubscript:9527]);

    NSMutableArray *arr5 =  [NSMutableArray array];
    NSLog(@"[arr5 objectAtIndex:9527] = %@", [arr4 objectAtIndex:9527]);
    NSLog(@"[arr5 objectAtIndexedSubscript:2] = %@", [arr5 objectAtIndexedSubscript:2]);

    NSMutableArray *arr6 =  [NSMutableArray array];
    [arr6 addObject:nil];
    [arr6 insertObject:nil atIndex:4];
    [arr6 insertObject:@3 atIndex:4];
}

Aspects实用介绍

Aspects是一个基于Method Swizzle的iOS函数替换的第三方库,他可以很好的实现勾取一个类或者一个对象的某个方法,支持在方法执行前(AspectPositionBefore)/执行后(AspectPositionAfter)替代原方法执行(AspectPositionInstead)

pod "Aspects"

需要导入的头文件

#import <Aspects/Aspects.h>

对外的两个重要接口声明如下:

第一个:HOOK一个类的所有实例的指定方法

/// 为一个指定的类的某个方法执行前/替换/后,添加一段代码块.对这个类的所有对象都会起作用.
///
/// @param block  方法被添加钩子时,Aspectes会拷贝方法的签名信息.
/// 第一个参数将会是 `id<AspectInfo>`,余下的参数是此被调用的方法的参数.
/// 这些参数是可选的,并将被用于传递给block代码块对应位置的参数.
/// 你甚至使用一个没有任何参数或只有一个`id<AspectInfo>`参数的block代码块.
///
/// @注意 不支持给静态方法添加钩子.
/// @return 返回一个唯一值,用于取消此钩子.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

第二个:HOOK一个类实例的指定方法

/// 为一个指定的对象的某个方法执行前/替换/后,添加一段代码块.只作用于当前对象.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

options有如下选择:

AspectPositionAfter   = 0,            // 在原方法调用完成以后进行调用
AspectPositionInstead = 1,            // 取代原方法   
AspectPositionBefore  = 2,            // 在原方法调用前执行   
AspectOptionAutomaticRemoval = 1 << 3 // 在调用了一次后清除(只能在对象方法中使用)

三个重要参数如下:

// 1、被HOOK的元类、类或者实例
@property (nonatomic, unsafe_unretained, readonly) id instance;
// 2、方法参数列表
@property (nonatomic, strong, readonly) NSArray *arguments;
// 3、原来的方法
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
// 执行原来的方法
[originalInvocation invoke];

基本使用

+(void)Aspect {
    // 在类UIViewController所有的实例执行viewWillAppear:方法完毕后做一些事情
    [UIViewController aspect_hookSelector:@selector(viewWillAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> info) {
                                   NSString *className = NSStringFromClass([[info instance] class]);
                                   NSLog(@"%@", className);
                               } error:NULL];
    
    // 在实例myVc执行viewWillAppear:方法完毕后做一些事情
    UIViewController* myVc = [[UIViewController alloc] init];
    [myVc aspect_hookSelector:@selector(viewWillAppear:)
                            withOptions:AspectPositionAfter
                             usingBlock:^(id<AspectInfo> info) {
                                 id instance = info.instance;               //调用的实例对象
                                 id invocation = info.originalInvocation;   //原始的方法
                                 id arguments = info.arguments;             //参数
                                 [invocation invoke];                       //原始的方法,再次调用
                             } error:NULL];
    // HOOK类方法
    Class metalClass = objc_getMetaClass(NSStringFromClass(UIViewController.class).UTF8String);
    [metalClass aspect_hookSelector:@selector(ClassMethod)
                        withOptions:AspectPositionAfter
                         usingBlock:^(id<AspectInfo> info) {
                             NSLog(@"%@", HOOK类方法);
                         } error:NULL];
}

注意:

  • Aspects 对类族无效,比如 NSArray 需要使用系统方法对每个子类单独 hook
  • 所有的调用,都会是线程安全的。 Aspects 使用了 Objective-C 的消息转发机会,会有一定的性能消耗.所有对于过于频繁的调用,不建议使用 AspectsAspects更适用于视图/控制器相关的等每秒调用不超过1000次的代码。
  • 当应用于某个类时(使用类方法添加钩子),不能同时hook父类和子类的同一个方法;否则会引起循环调用问题.但是,当应用于某个类的示例时(使用实例方法添加钩子),不受此限制.
  • 使用KVO时,最好在 aspect_hookSelector: 调用之后添加观察者,否则可能会引起崩溃.

参考链接

ios 针对数组越界的崩溃优化

Aspects源码解析

面向切面 Aspects 源码阅读

iOS---防止UIButton重复点击的三种实现方式

Aspects– iOS的AOP面向切面编程的库

Objc 黑科技 - Method Swizzle 的一些注意事项

Aspects– iOS的AOP面向切面编程的库

iOS 如何实现Aspect Oriented Programming (上)

iOS数据埋点统计方案选型(附Demo):运行时Method Swizzling机制与AOP编程(面向切面编程)

Aspects源码解读:动态Block调用(不定参数的Block)