AOP(Aspect Oriented Programming)面向切面编程
相比传统的OOP来说,OOP的特点在于它可以很好的将系统横向分为很多个模块(比如通讯录模块,聊天模块,发现模块),每个子模块可以横向的衍生出更多的模块,用于更好的区分业务逻辑。而AOP其实相当于是纵向的分割系统模块,将每个模块里的公共部分提取出来(即那些与业务逻辑不相关的部分,如日志,用户行为等等),与业务逻辑相分离开来,从而降低代码的耦合度。
AOP主要是被使用在日志记录,性能统计,安全控制,事务处理,异常处理几个方面。由于本人并不是位大神(正在成长中),所以目前只在项目里用到了日志记录和事物处理这两个方面,另外几个方面会在以后陆陆续续更新。
注意:AOP和OOP一样,并不是一种技术,而是一种编程思想,所有实现了AOP编程思想的都算是AOP的实现。
在iOS中我们通常使用Method Swizzling(俗称iOS黑魔法)来实现AOP,Method Swizzling其实就是一种在Runtime的时候把一个方法的实现与另一个方法的实现互相替换。具体详见Method Swizzling
Aspects 一个基于Objective-C的AOP开发框架,封装了 Runtime ,是我们平时比较常用到实现Method Swizzling的一个类库,它提供了如下两个API:
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// Called after the original implementation (default)
AspectPositionInstead = 1, /// Will replace the original implementation.
AspectPositionBefore = 2, /// Called before the original implementation.
AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};
/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
AOP iOS中的事务处理之版本控制
我之前维护过一个项目Toast系列(Mac下一个比较有名的刻盘软件),它有很多的版本比如Pro版本,LT(lite)版本。顾名思义LT其实就是关闭了很多功能的一个版本,很多对应的事件在LT不会被触发,所以为了阻止事件的触发,每个用户事件中都会出现一段如下的宏判断
- (void)detailBottomBtnEvent:(id)sender {
//if we not use AOP, we must write this code in project
#ifdef LITE_VERSION
//do nothing
#else
//do all thing
#endif
}
这显然不是我们想要看到的结果,每个用户事件里都会去判断LT_VERSION的宏,然后在做对应的事件处理。LT的版本不是一个主版本,我们的业务逻辑里主要还是需要触发用户对应的事件,所以这个时候我们就可以用到AOP的思想
//
// AppDelegate+LiteEvent.m
// AOPTransactionIntactDemo
//
// Created by wuyike on 16/5/19.
// Copyright © 2016年 bongmi. All rights reserved.
//
#import "AppLiteDelegate+LiteEvent.h"
#import "Aspects.h"
typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);
@implementation AppLiteDelegate (LiteEvent)
- (void)setLiteEvent {
#ifdef LITE_VERSION
NSDictionary *configs = @{
@"AOPTopViewController": @{
UserTrackedEvents: @[
@{
UserEventName: @"detailBtn",
UserEventSelectorName: @"detailTopBtnEvent:",
UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
NSLog(@"Top detailBtn clicked, this is lite version");
},
},
],
},
@"AOPBottomViewController": @{
UserTrackedEvents: @[
@{
UserEventName: @"detailBtn",
UserEventSelectorName: @"detailBottomBtnEvent:",
UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
NSLog(@"Bottom detailBtn clicked this is lite version");
},
},
],
},
@"AOPLeftViewController": @{
UserTrackedEvents: @[
@{
UserEventName: @"detailBtn",
UserEventSelectorName: @"detailLeftBtnEvent:",
UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
NSLog(@"Left detailBtn clicked this is lite version");
},
},
],
},
@"AOPRightViewController": @{
UserTrackedEvents: @[
@{
UserEventName: @"detailBtn",
UserEventSelectorName: @"detailRightBtnEvent:",
UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
NSLog(@"Right detailBtn clicked this is lite version");
},
},
],
},
};
for (NSString *className in configs) {
Class clazz = NSClassFromString(className);
NSDictionary *config = configs[className];
if (config[UserTrackedEvents]) {
for (NSDictionary *event in config[UserTrackedEvents]) {
SEL selekor = NSSelectorFromString(event[UserEventSelectorName]);
AspectHandlerBlock block = event[UserEventHandlerBlock];
[clazz aspect_hookSelector:selekor
withOptions:AspectPositionInstead
usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(aspectInfo);
});
} error:NULL];
}
}
}
#endif
}
我们有Top,Buttom,Left,Right四个ViewController,每个ViewController中都有一个对应的用户触发事件,我们只需要在LT版本下替换对应的事件就可以,每个模块的业务逻辑不需要任何改动。
AOP iOS中的事务处理之安全可变容器
OC中任何以NSMutable开头的类都是可变容器,它们一般都具有(insert 、remove、replace)等操作,所以我们经常需要判断容器是否为空,以及指针越界等问题。为了避免我们在每次操作这些容器的时候都去判断,一般有以下几种解决方法:
派生类
Category
Method Swizzling
使用派生类肯定不是好的方法,Category可以解决我们的问题,但是导致项目中所有用到容器操作的地方都需要显示的调用我们新加的方法,所以也不是很优雅。所以这个时候用Method Swizzling就是一个不错的选择。
#import "NSMutableArray+SafeArray.h"
#import <objc/runtime.h>
@implementation NSMutableArray (SafeArray)
+ (void)load {
[[self class] swizzleMethod:@selector(addObject:) withMethod:@selector(safeAddObject:)];
[[self class] swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex:)];
[[self class] swizzleMethod:@selector(insertObject:atIndex:) withMethod:@selector(safeInsertObject:atIndex:)];
[[self class] swizzleMethod:@selector(removeObjectAtIndex:) withMethod:@selector(safeRemoveObjectAtIndex:)];
[[self class] swizzleMethod:@selector(replaceObjectAtIndex:withObject:) withMethod:@selector(safeReplaceObjectAtIndex:withObject:)];
NSLog(@"%@ %@", @"SafeArray", [self class]);
}
#pragma mark - magic
- (void)safeAddObject:(id)anObject {
//do safe operate
if (anObject) {
[self safeAddObject:anObject];
} else {
NSLog(@"safeAddObject: anObject is nil");
}
}
- (id)safeObjectAtIndex:(NSInteger)index {
//do safe operate
if (index >= 0 && index <= self.count) {
return [self safeObjectAtIndex:index];
}
NSLog(@"safeObjectAtIndex: index is invalid");
return nil;
}
- (void)safeInsertObject:(id)anObject
atIndex:(NSUInteger)index {
//do safe operate
if (anObject && index >= 0 && index <= self.count) {
[self safeInsertObject:anObject atIndex:index];
} else {
NSLog(@"safeInsertObject:atIndex: anObject or index is invalid");
}
}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
//do safe operate
if (index >= 0 && index <= self.count) {
[self safeRemoveObjectAtIndex:index];
} else {
NSLog(@"safeRemoveObjectAtIndex: index is invalid");
}
}
- (void)safeReplaceObjectAtIndex:(NSUInteger)index
withObject:(id)anObject {
//do safe operate
if (anObject && index >= 0 && index <= self.count) {
[self safeReplaceObjectAtIndex:index withObject:anObject];
} else {
NSLog(@"safeReplaceObjectAtIndex:withObject: anObject or index is invalid");
}
}
- (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);
}
}
以上就是我用AOP思想在事件处理方面的两个具体的应用。
AOP iOS中的日志记录
通常我们会在项目中收集用户的日志,以及用户行为,以用来分析Bug,以及提升产品质量。项目往往包含很多的模块,以及下面会有更多的子模块,所以如果把这些操作具体加载每个事件中,显然这种做法是不可取的,第一所有收集用户行为的操作不属于业务逻辑范畴,我们不需要分散到各个业务中。第二这种方式的添加不利于后期维护,而且改动量是巨大的。所以这里使用上面提到的版本控制事件处理相同的方式,这里抄袭一个Demo
- (void)setupLogging
{
NSDictionary *config = @{
@"MainViewController": @{
GLLoggingPageImpression: @"page imp - main page",
GLLoggingTrackedEvents: @[
@{
GLLoggingEventName: @"button one clicked",
GLLoggingEventSelectorName: @"buttonOneClicked:",
GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
NSLog(@"button one clicked");
},
},
@{
GLLoggingEventName: @"button two clicked",
GLLoggingEventSelectorName: @"buttonTwoClicked:",
GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
NSLog(@"button two clicked");
},
},
],
},
@"DetailViewController": @{
GLLoggingPageImpression: @"page imp - detail page",
}
};
[GLLogging setupWithConfiguration:config];
}
typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);
+ (void)setupWithConfiguration:(NSDictionary *)configs
{
// Hook Page Impression
[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][GLLoggingPageImpression];
if (pageImp) {
NSLog(@"%@", pageImp);
}
});
} error:NULL];
// Hook Events
for (NSString *className in configs) {
Class clazz = NSClassFromString(className);
NSDictionary *config = configs[className];
if (config[GLLoggingTrackedEvents]) {
for (NSDictionary *event in config[GLLoggingTrackedEvents]) {
SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);
AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];
[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];
}
}
}
}
我们通过上述的操作可以在每个用户事件触发后都追加一个用户的行为记录,同时又不需要修改业务逻辑。
总结
这里反馈一个关于Aspect的问题,貌似Aspect不支持不同类中含有相同名称的方法时,会出现不能正确替换方法的情况,详细可以参见https://github.com/steipete/Aspects/issues/48 https://github.com/steipete/Aspects/issues/24
作者:北辰明
链接://www.greatytc.com/p/f58a9e4be184
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
统计打点是 App 开发里很重要的一个环节,App 的运行状态、用户的各种行为等都需要打点,有不少关于统计的第三方库(如友盟统计)。但是,如果要求在整个项目的所有button里统计用户的点击事件,假如一个项目里面有1000个button,你就要设1000个地方设置统计代码,显然不科学。
有没有一种办法在一个地方进行统计打点,检测整个应用的点击事件?
方案一:使用Runtime的方式追踪点击的按钮
特点:需要对每个button进行tag编号,对手势点击、tableView的点击要单独配置,比较繁琐
方案二:使用面向切面编程AOP对按钮或者页面进行追踪(无需在任何详情页面中做相应配置)
特点:
1、在不修改源代码的情况下,通过运行时给程序添加统一功能的技术,可以用作日志记录,性能统计等
2、无需对每个button进行tag编号,创建button后只需在新建的plist中配置button对应的方法名和对应的事件 ID就行
3、适用于Tap点击手势,使用时设置事件ID,和button的使用方法一样
4、button不支持直接在block里面写事件的方式,但可以在block里面调用方法或者需要统一写成下面的方式
[button addTarget:self action:@selector(click)forControlEvents:UIControlEventTouchUpInside];
5、适用于tableview的didSelectRowAtIndexPath点击事件,可获取tableView对应的类名、section和row
6、如果是统计tableview的点击事件,根据需要在获取到section和row后加个判断埋点统计
if (section == 0 && row == 1) {
[MobClick event:eventID];
}
7、如果有特殊需求:某个按钮登录前和登录后记录的事件不一样,需要加判断
if ([eventID isEqualToString:@"xxx"]) {
[EJServiceUserInfo isLogin]?[MobClick event:eventID]:[MobClick event:@"???"];
}else{
[MobClick event:eventID];
}
以下是具体代码:
(不得不吐槽一下,网上很多博客文章都是转载的,很少有能直接运行的,研究了一整天才弄出来)
这里用到了第三方库:Aspects,用cocoaPods进行集成 pod 'Aspects'
1、创建一个继承与NSObject的EJAspectManager类
EJAspectManager.h
@interface EJAspectManager : NSObject
+(void)trackAspectHooks;
@end
EJAspectManager.m
#import "EJAspectManager.h"
#import "Aspects/Aspects.h"
@implementation EJAspectManager
+(void)trackAspectHooks{
[EJAspectManager trackViewAppear];
[EJAspectManager trackBttonEvent];
}
#pragma mark -- 监控统计用户进入此界面的时长,频率等信息
+ (void)trackViewAppear{
[UIViewController aspect_hookSelector:@selector(viewWillAppear:)
withOptions:AspectPositionBefore
usingBlock:^(id<AspectInfo> info){
//用户统计代码写在此处
DDLogDebug(@"[打点统计]:%@ viewWillAppear",NSStringFromClass([info.instance class]));
NSString *className = NSStringFromClass([info.instance class]);
DLog(@"className-->%@",className);
[MobClick beginLogPageView:className];//(className为页面名称
}
error:NULL];
[UIViewController aspect_hookSelector:@selector(viewWillDisappear:)
withOptions:AspectPositionBefore
usingBlock:^(id<AspectInfo> info){
//用户统计代码写在此处
DDLogDebug(@"[打点统计]:%@ viewWillDisappear",NSStringFromClass([info.instance class]));
NSString *className = NSStringFromClass([info.instance class]);
DLog(@"className-->%@",className);
[MobClick endLogPageView:className];
}
error:NULL];
//other hooks ... goes here
//...
}
#pragma mark --- 监控button的点击事件
+ (void)trackBttonEvent{
__weak typeof(self) ws = self;
//设置事件统计
//放到异步线程去执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//读取配置文件,获取需要统计的事件列表
NSString *path = [[NSBundle mainBundle] pathForResource:@"EventList" ofType:@"plist"];
NSDictionary *eventStatisticsDict = [[NSDictionary alloc] initWithContentsOfFile:path];
for (NSString *classNameString in eventStatisticsDict.allKeys) {
//使用运行时创建类对象
const char * className = [classNameString UTF8String];
//从一个字串返回一个类
Class newClass = objc_getClass(className);
NSArray *pageEventList = [eventStatisticsDict objectForKey:classNameString];
for (NSDictionary *eventDict in pageEventList) {
//事件方法名称
NSString *eventMethodName = eventDict[@"MethodName"];
SEL seletor = NSSelectorFromString(eventMethodName);
NSString *eventId = eventDict[@"EventId"];
[ws trackEventWithClass:object_getClass(newClass) selector:seletor eventID:eventId];
[ws trackTableViewEventWithClass:object_getClass(newClass) selector:seletor eventID:eventId];
[ws trackParameterEventWithClass:object_getClass(newClass) selector:seletor eventID:eventId];
}
}
});
}
#pragma mark -- 监控button和tap点击事件(不带参数)
+ (void)trackEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{
[klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
NSString *className = NSStringFromClass([aspectInfo.instance class]);
NSLog(@"className--->%@",className);
NSLog(@"event----->%@",eventID);
if ([eventID isEqualToString:@"xxx"]) {
[EJServiceUserInfo isLogin]?[MobClick event:eventID]:[MobClick event:@"???"];
}else{
[MobClick event:eventID];
}
} error:NULL];
}
#pragma mark -- 监控button和tap点击事件(带参数)
+ (void)trackParameterEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{
[klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo,UIButton *button) {
NSLog(@"button---->%@",button);
NSString *className = NSStringFromClass([aspectInfo.instance class]);
NSLog(@"className--->%@",className);
NSLog(@"event----->%@",eventID);
} error:NULL];
}
#pragma mark -- 监控tableView的点击事件
+ (void)trackTableViewEventWithClass:(Class)klass selector:(SEL)selector eventID:(NSString*)eventID{
[klass aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo,NSSet *touches, UIEvent *event) {
NSString *className = NSStringFromClass([aspectInfo.instance class]);
NSLog(@"className--->%@",className);
NSLog(@"event----->%@",eventID);
NSLog(@"section---->%@",[event valueForKeyPath:@"section"]);
NSLog(@"row---->%@",[event valueForKeyPath:@"row"]);
NSInteger section = [[event valueForKeyPath:@"section"]integerValue];
NSInteger row = [[event valueForKeyPath:@"row"]integerValue];
//统计事件
if (section == 0 && row == 1) {
[MobClick event:eventID];
}
} error:NULL];
}
@end
2、这样我们在appDelegate里面直接调用下面的代码就可以达到全局统计的目的了!
[EJAspectManager trackAspectHooks];
3、上面涉及到的EventPlist是新创建的plist文件,设置方式如下:
作者:来宝
链接://www.greatytc.com/p/6211704a4b22
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。