面向切面编程参考: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
@interface UIButton (FixMultiClick)
@property (nonatomic, assign) NSTimeInterval clickInterval;
@end
#import "UIButton+FixMultiClick.h"
#import
#import
@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 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
@interface NSArray (Aspect)
@end
#import "NSArray+Aspect.h"
#import
@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
复制代码
对外的两个重要接口
声明如下:
第一个:HOOK一个类的所有实例的指定方法
/// 为一个指定的类的某个方法执行前/替换/后,添加一段代码块.对这个类的所有对象都会起作用.
///
/// @param block 方法被添加钩子时,Aspectes会拷贝方法的签名信息.
/// 第一个参数将会是 `id`,余下的参数是此被调用的方法的参数.
/// 这些参数是可选的,并将被用于传递给block代码块对应位置的参数.
/// 你甚至使用一个没有任何参数或只有一个`id`参数的block代码块.
///
/// @注意 不支持给静态方法添加钩子.
/// @return 返回一个唯一值,用于取消此钩子.
+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
复制代码
第二个:HOOK一个类实例的指定方法
/// 为一个指定的对象的某个方法执行前/替换/后,添加一段代码块.只作用于当前对象.
- (id)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 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 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 info) {
NSLog(@"%@", HOOK类方法);
} error:NULL];
}
复制代码
注意:
Aspects
对类族无效,比如NSArray
需要使用系统方法对每个子类单独hook
。- 所有的调用,都会是线程安全的。
Aspects
使用了Objective-C
的消息转发机会,会有一定的性能消耗.所有对于过于频繁的调用,不建议使用Aspects
。Aspects
更适用于视图/控制器相关的等每秒调用不超过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)