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

2,764 阅读8分钟

AOP是什么?

AOP,也就是面向切面编程,可以通过预编译方式或运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。

在不修改源代码的情况下给程序动态添加功能,我们一般称之为hook,在iOS中有几种方案可以去实现

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

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


OC中如何实现AOP

其实在服务端开发中,Spring以及Spring家族产品早已大杀四方,名扬天下。作为Spring 基石之一的AOP思想更是发光发热,在各种语言,各种平台上,AOP编程思想都是做出了不可磨灭的贡献。

像在Java的后台开发中,如日志输出,Spring Security OAuth2 的鉴权控制,请求拦截等都是AOP的经典应用,像这些与业务无关,但是又散布在各个业务的需求,都是比较适合用AOP解决的。

但话说回来,对于iOS中的OC开发者,AOP的实现方式有哪些呢?

从语言特性上,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的交换,所以方法的执行速度快 但是从上面的代码可知,这个方案有一下几个弊端

  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去实现。