AOP,也就是面向切面编程,能够经过预编译方式或运行期动态代理实如今不修改源代码的状况下给程序动态统一添加功能的一种技术。git
在不修改源代码的状况下给程序动态添加功能,咱们通常称之为hook,在iOS中有几种方案能够去实现github
在这系列文章里面将会探讨我所了解的基于Method Swizzling和消息转发的hook。编程
其实在服务端开发中,Spring以及Spring家族产品早已大杀四方,名扬天下。做为Spring 基石之一的AOP思想更是发光发热,在各类语言,各类平台上,AOP编程思想都是作出了不可磨灭的贡献。缓存
像在Java的后台开发中,如日志输出,Spring Security OAuth2 的鉴权控制,请求拦截等都是AOP的经典应用,像这些与业务无关,可是又散布在各个业务的需求,都是比较适合用AOP解决的。ruby
但话说回来,对于iOS中的OC开发者,AOP的实现方式有哪些呢?markdown
从语言特性上,OC没有像JAVA那样的语言特性,没有注解。不能便捷且无侵入的去添加切面和起点。可是,OC有Runtime!有Runtime!有Runtime! 经过Runtime,咱们也能够实现AOP编程。前面提到的Method Swizzling和基于消息转发的实现Hook都是经过Rumtime去实现的。框架
咱们以前对一个方法进行hook,通常都是写一个Category,而后在Category写以下代码(以hook viewDidAppear为例)函数
+ (void)load {
Class class = [self class];
SEL originSEL = @selector(viewDidAppear:);
SEL swizzleSEL = @selector(swizzleViewDidAppear:);
Method originMethod = class_getInstanceMethod(class, originSEL);
Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL);
BOOL didAddMethod = class_addMethod(class, originSEL,
method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzleSEL,
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod,
swizzleMethod);
}
}
// 咱们本身实现的方法,也就是和self的swizzleViewDidAppear方法进行交换的方法。
- (void)swizzleViewDidAppear:(BOOL)animated {
[self swizzleViewDidAppear:animated];
//埋点操做
//...........
}
复制代码
其实这个的实现思路很简单,就是交换两个方法的实现地址(在上面就是viewDidAppear和swizzleViewDidAppear),而后在新的方法调用原有的方法,这样就能够在不修改原来的方法的代码的状况下动态添加内容,如图所示工具
利用Method Swizzling,能够实现Hook,并且是由于基于imp的交换,因此方法的执行速度快 可是从上面的代码可知,这个方案有一下几个弊端oop
另外关于Method Swizzling的弊端iOS界的毒瘤-MethodSwizzling
iOS中有一个老牌的基于消息转发的AOP框架Aspects,可是本文所讲述和使用的是本人本身写的一个AOP工具,SFAspect。SFAspect核心的原理借鉴了Aspects,都是经过消息转发去实现Hook。
为何重复的去造一个轮子呢?由于基于我对AOP的理解以及iOS开发的一些习惯,我去了作了一些功能上的补充,如
前面两点其实很好理解,主要是为了提升Hook的灵活性和准确性,那为何要中止切面后的代码的执行呢?其实这一点我认为很重要,尤为对于验证的需求来讲。举个例子,假设登录服务类B登录操做须要接收帐号和密码参数,咱们能够利用Hook对B的登录操做进行参数校验,对B类的登录操做进行一个前置的Hook,若是帐号或密码为空,则在Hook中中止后续操做,以防没必要要的调用。
接下来简单说一下基于消息转发的Hook(在另一篇文章会详细讲述)
相对基于Method Swizzling实现的实现,基于消息转发的便捷性和动态性更强,可是有一点,基于消息转发的hook的速度是慢于Method Swizzling的,Method Swizzling是直接交换方法的实现地址,而消息转发的方案每一次调用方法都须要进入到消息转发流程,对于被hook的方法,在被hook期间,方法缓存也至关于失效状态。
SFAspect的实现原理在下一篇文章详细描述
pod 'SFAspect'
复制代码
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
//参数从2开始,由于方法执行的时候隐式携带了两个参数:self 和 _cmd,self是方法调用者,_cmd是被调用f方法的sel
[invocation getArgument:&animated atIndex:2];
NSLog(@"准备执行viewWillAppear,参数animated的值为%d",animated);
//改变参数
animated = NO;
[invocation setArgument:&animated atIndex:2];
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"2" withPriority:0 withHookOption:(HookOptionAfter) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
//参数从2开始,由于方法执行的时候隐式携带了两个参数:self 和 _cmd,self是方法调用者,_cmd是被调用f方法的sel
[invocation getArgument:&animated atIndex:2];
NSLog(@"执行viewWillAppear后,参数animated的值为%d",animated);
//也能够经过invocation获取返回值,详情参考消息转发过程当中NSInvocation的用法
}];
复制代码
[self.vc hookSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"3" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"hook单个对象的类方法");
}];
复制代码
[SFHookViewController hookAllClassSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
[invocation getArgument:&animated atIndex:2];
NSLog(@"准备执行viewWillAppear,参数animated的值为%d",animated);
}];
复制代码
[SFHookViewController hookAllClassSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
BOOL animated = NO;
NSInvocation *invocation = aspectModel.originalInvocation;
[invocation getArgument:&animated atIndex:2];
NSLog(@"hook全部对象的类方法");
}];
复制代码
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"准备执行viewWillAppear,执行的优先级是%d",aspectModel.priority);
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"2" withPriority:1 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"准备执行viewWillAppear,执行的优先级是%d",aspectModel.priority);
}];
复制代码
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
NSLog(@"准备执行viewWillAppear,执行的优先级是%d",aspectModel.priority);
}];
//移除hook后hook里面的block不执行
[self.vc removeHook:@selector(viewWillAppear:) withIdentify:@"1" withHookOption:(HookOptionPre)];
复制代码
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
//pre是在方法前执行
NSLog(@"pre-准备执行viewWillAppear");
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"2" withPriority:0 withHookOption:(HookOptionAfter) withBlock:^(SFAspectModel *aspectModel, HookState state) {
//after是在方法前执行
NSLog(@"after-执行viewWillAppear后");
}];
[self.vc hookSel:@selector(viewWillAppear:) withIdentify:@"3" withPriority:0 withHookOption:(HookOptionAround) withBlock:^(SFAspectModel *aspectModel, HookState state) {
//around是在方法先后执行
if(state == HookStatePre){
NSLog(@"around准备执行viewWillAppear");
}
if (state == HookStateAfter) {
NSLog(@"around-准备执行viewWillAppear");
}
}];
复制代码
__block CFAbsoluteTime startTime;
HookBLock block = ^(SFAspectModel *aspectModel, HookState state) {
//控制两秒内不可再次点击button
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
if (linkTime< 2) {
[aspectModel stop];//中止操做
// [aspectModel stopWithBlock:^{
// //中止并抛出异常
// }];
}else{
startTime = CFAbsoluteTimeGetCurrent();
}
};
[UIButton hookSel:@selector(sendAction:to:forEvent:) withIdentify:@"22" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
复制代码
这是一个很简单的应用,新建一个专门的埋点的类,在load方法中对须要被埋点的方法进行hook便可
+(void)load{
HookBLock block = ^(SFAspectModel *aspectModel, HookState state) {
//埋点操做
NSLog(@"//埋点操做");
};
[SFViewController1 hookAllClassSel:@selector(sayGoodDayTo:withVCTitle:) withIdentify:@"33" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
[SFHookViewController hookAllClassSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"33" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
[SFViewController1 sayGoodDayTo:@"1" withVCTitle:@"1"];
[SFHookViewController sayHiTo:@"2" withVCTitle:@"2"];
}
复制代码
当咱们的正式环境某个页面出现崩溃的错误时,或是提交给苹果审核的时候,咱们能够经过对页面跳转进行Hook,实现阻止用户进入到某个页面的需求。就拿hook方法pushViewController举例 假设SFHookViewController出现了问题,要替换成SFViewController页面
-(void)hookErrorPage{
//假设这里是从线上获取到出问题的页面和替换的页面
NSMutableDictionary *errorPageInfoDic = [NSMutableDictionary dictionary];
[errorPageInfoDic setObject:@"SFHookViewController" forKey:@"page_key"];//有问题的页面
[errorPageInfoDic setObject:@"SFViewController1" forKey:@"jump_router"];//替换的页面
__block NSMutableArray<NSMutableDictionary *> *errorPageList = [NSMutableArray array];
[errorPageList addObject:errorPageInfoDic];
if(errorPageList.count > 0){
//注意要使用block,由于在hook的block里面对invocation的操做须要捕获
__block UIViewController *vc = nil;
__block UIViewController *maintainVC = nil;
__block NSString *vcName;
//hook pushViewController,控制跳转行为
[UINavigationController hookSel:@selector(pushViewController:animated:) withIdentify:@"1" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
__block NSInvocation *invocation = aspectModel.originalInvocation;
//参数从2开始,由于方法执行的时候隐式携带了两个参数:self 和 _cmd,self是方法调用者,_cmd是被调用f方法的sel
[invocation getArgument:&vc atIndex:2];
for (int i = 0; i < errorPageList.count; i++) {
NSDictionary *dic = errorPageList[i];
vcName = [dic objectForKey:@"page_key"];
if ([vcName isEqualToString:NSStringFromClass([vc class])]) {
//建立替换的页面
maintainVC = [[NSClassFromString([dic valueForKey:@"jump_router"]) alloc] initWithNibName:[dic valueForKey:@"jump_router"] bundle:nil];
maintainVC.view.backgroundColor =[UIColor redColor];
if(maintainVC){
//替换页面
[invocation setArgument:&maintainVC atIndex:2];
}
}
}
}];
}
[self.navigationController pushViewController:[[SFHookViewController alloc] initWithNibName:@"SFHookViewController" bundle:nil] animated:YES];
}
复制代码
有些时候,咱们须要控制操做的间隔,举个例子,有时候咱们会防止按钮的段时间内屡次点击,这种状况也能够经过Hook去控制。由于UIController的事件都是经过sendAction:to:forEvent:去调用的,咱们能够经过hook UiButton的类去实现这种需求,以下
__block CFAbsoluteTime startTime;
HookBLock block = ^(SFAspectModel *aspectModel, HookState state) {
//控制两秒内不可再次点击button
CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
if (linkTime< 2) {
[aspectModel stop];//中止操做
}else{
startTime = CFAbsoluteTimeGetCurrent();
}
};
[UIButton hookSel:@selector(sendAction:to:forEvent:) withIdentify:@"22" withPriority:1 withHookOption:(HookOptionPre) withBlock:block];
复制代码
由于SFAspect中被hook的方法和hook里面的操做是按顺序执行,因此被hook的方法和hook里面的操做至关因而链式调用,这里不作代码展现,以下图所示
经过Hook咱们还能够实现不少的需求,只要经过在方法调用先后能够去作的事情,经过Hook都能实现
其实AOP不是必须的,可是AOP编程是一个开发利器,有不少的应用场景咱们均可以经过Aop去实现。