iOS中的AOP(1)-介绍及应用

AOP是什么?

AOP,也就是面向切面编程,能够经过预编译方式或运行期动态代理实如今不修改源代码的状况下给程序动态统一添加功能的一种技术。git

在不修改源代码的状况下给程序动态添加功能,咱们通常称之为hook,在iOS中有几种方案能够去实现github

  • Method Swizzling
  • 基于消息转发的实现,表明Aspects
  • 基于libffi的实现,如针对block的BlockHook和饿了么针对函数调用的Stiger

在这系列文章里面将会探讨我所了解的基于Method Swizzling和消息转发的hook。编程


OC中如何实现AOP

其实在服务端开发中,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去实现的。框架

基于Method Swizzling实现

咱们以前对一个方法进行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

  1. 对于每一个不一样类的Hook,都要去写一个category,load和替换的方法
  2. 因为load方法的执行顺序依赖于文件的编译顺序,对于同一个类的Hook,若是须要屡次HOOk,切面(也就是Hook执行的方法)的执行顺序不可控
  3. 因为是在load方法是在编译的时候就执行,因此Hook方法之后及其不方便,不具备动态性

另外关于Method Swizzling的弊端iOS界的毒瘤-MethodSwizzling


基于消息转发

iOS中有一个老牌的基于消息转发的AOP框架Aspects,可是本文所讲述和使用的是本人本身写的一个AOP工具,SFAspect。SFAspect核心的原理借鉴了Aspects,都是经过消息转发去实现Hook。

为何重复的去造一个轮子呢?由于基于我对AOP的理解以及iOS开发的一些习惯,我去了作了一些功能上的补充,如

  • 切面执行应该有明确的执行顺序,能够随意控制每个Hook的执行顺序,使Hook执行顺序不受制于声明顺序和建立顺序
  • 切面能够灵活的移除,不受制于Hook的声明空间
  • 切面能够中能够中止切面后代码的执行
  • 切面能够独立出来,供多个切点使用

前面两点其实很好理解,主要是为了提升Hook的灵活性和准确性,那为何要中止切面后的代码的执行呢?其实这一点我认为很重要,尤为对于验证的需求来讲。举个例子,假设登录服务类B登录操做须要接收帐号和密码参数,咱们能够利用Hook对B的登录操做进行参数校验,对B类的登录操做进行一个前置的Hook,若是帐号或密码为空,则在Hook中中止后续操做,以防没必要要的调用。

接下来简单说一下基于消息转发的Hook(在另一篇文章会详细讲述)

  1. 将被hook的方法实现另存起来,而后再将被hook方法的imp设置为msg_forward,使被hook的方法调用时进入消息转发流程

  1. 在消息转发的流程中,hook类的methodSignatureForSelector和forwardInvocation方法,在forwardInvocation中执行hook的操做

相对基于Method Swizzling实现的实现,基于消息转发的便捷性和动态性更强,可是有一点,基于消息转发的hook的速度是慢于Method Swizzling的,Method Swizzling是直接交换方法的实现地址,而消息转发的方案每一次调用方法都须要进入到消息转发流程,对于被hook的方法,在被hook期间,方法缓存也至关于失效状态。

SFAspect的实现原理在下一篇文章详细描述


SFAspect的使用

安装

pod 'SFAspect'
复制代码

使用

  • hook单个对象实例方法
[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的用法
          
       }];
复制代码
  • hook单个对象的类方法
[self.vc hookSel:@selector(sayHiTo:withVCTitle:) withIdentify:@"3" withPriority:0 withHookOption:(HookOptionPre) withBlock:^(SFAspectModel *aspectModel, HookState state) {
        NSLog(@"hook单个对象的类方法");
    }];
复制代码
  • 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);
        
    }];
复制代码
  • hook类全部对象的类方法
[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全部对象的类方法");
        
    }];
复制代码
  • 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);
                
        
    }];
复制代码
  • 移除hook
[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)];
复制代码
  • hook中 pre,after,around的区别
[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];

复制代码

iOS中AOP的应用例子

  • 埋点
  • 简单的线上控制页面跳转
  • 特殊的链式调用
  • 控制函数执行的间隔
  • 更多

集中埋点

这是一个很简单的应用,新建一个专门的埋点的类,在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去实现。

相关文章
相关标签/搜索