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去實現。

相關文章
相關標籤/搜索