全面談談iOS中的Aspects和JSPatch兼容問題

pplingling 8年前發布 | 13K 次閱讀 JSPatch iOS開發 移動開發

1. 背景

AspectsJSPatch 是 iOS 開發中非常常見的兩個庫。Aspects 提供了方便簡單的方法進行面向切片編程(AOP),JSPatch可以讓你用 JavaScript 書寫原生 iOS APP 和進行熱修復。關于實現原理可以參考 面向切面編程之 Aspects 源碼解析及應用JSPatch wiki 。簡單地概括就是將原方法實現替換為 _objc_msgForward (或 _objc_msgForward_stret ),當執行這個方法是直接進入消息轉發過程,最后到達替換后的 -forwardInvocation: ,在 -forwardInvocation: 內執行新的方法,這是兩者的共同原理。最近項目開發中需要用 JSPatch 替換方法修復一個 bug ,然而這個方法已經使用 Aspects 進行 hook 過了,那么兩者同時使用會不會有問題呢?關于這個問題,網上介紹比較詳細的是 面向切面編程之 Aspects 源碼解析及應用有關Swizzling的一個問題 ,深入研究后發現這兩篇文章講得都不夠全面。本文基于 Aspects 1.4.1 和 JSPatch 1.1 介紹幾種測試結果和原因。

2. 測試

2.0. 源碼

這是本文使用的測試代碼 ,你可以clone下來,泡杯咖啡,找個安靜的地方跟著本文一步一步實踐。

2.1. 代碼說明

ViewController.m 中首先定義一個簡單類 MyClass ,只有 -test 和 -test2 方法,方法內打印log

@interface MyClass : NSObject

  • (void)test;
  • (void)test2; @end

@implementation MyClass

  • (void)test { NSLog(@"MyClass origin log"); }
  • (void)test2 { NSLog(@"MyClass test2 origin log"); } @end</code></pre>

    接著是三個hook方法,分別是對 -test 進行 hook 的 -jp_hook 、 -aspects_hook 和對 -test2 進行 hook 的 -aspects_hook_test2

    - (void)jp_hook {
      [JPEngine startEngine];
      NSString sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
      NSString script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
      [JPEngine evaluateScript:script];
    }
  • (void)aspects_hook { [MyClass aspect_hookSelector:@selector(test) withOptions:AspectPositionAfter usingBlock:^(id aspects) {
      NSLog(@"aspects log");
    
    } error:nil]; }
  • (void)aspects_hook_test2 { [MyClass aspect_hookSelector:@selector(test2) withOptions:AspectPositionInstead usingBlock:^(id aspects) {
      NSLog(@"aspects test2 log");
    
    } error:nil]; }</code></pre>

    demo.js 代碼也非常簡單,對 MyClass 的 -test 進行替換

    require('MyClass')
    defineClass('MyClass', {
      test: function() {
    //        self.ORIGtest();
    
      console.log("jspatch log")
    
    } });</code></pre>

    2.2. 具體測試

    2.2.1. JSPatch 先 hook 、Aspects 采用 AspectPositionInstead (替換) hook

    那么代碼就是下面這樣,注意把 -aspects_hook 方法設置為 AspectPositionInstead

    // ViewController.m
  • (void)viewDidLoad { [super viewDidLoad]; [self jp_hook]; [self aspects_hook]; MyClass a = [[MyClass alloc] init]; [a test]; }</code></pre>

    執行結果:

    JPAndAspects[2092:1554779] aspects log

    結果是 Aspects 正確替換了方法

    2.2.2. Aspects 先采用隨便一種Position hook,JSPatch再hook

    那么代碼就是下面這樣

    - (void)viewDidLoad {
      [super viewDidLoad];
      [self aspects_hook];
      [self jp_hook];
      MyClass a = [[MyClass alloc] init];
      [a test];
    }</code></pre> 
    

    執行結果:

    JPAndAspects[2774:1565702] JSPatch.log: jspatch log

    結果是 JSPatch 正確替換了方法

    Why?

    前面說到, hook 會替換該方法和 -forwardInvocation: ,我們先看看方法被 hook 前后的變化

    原方法對應關系

    方法替換后原方法指向了 _objc_msgForward ,同時添加一個方法 PREFIXtest ( JSPatch 是 ORIGtest , Aspects 是 aspects_test )指向了原來的實現。 JSPatch 新增了一個方法指向 IMP(NEWtest) , Aspects 則保存block為關聯屬性

    -test 變化

    -forwardInvocation: 的變化也相似,原來的 -forwardInvocation: 沒實現是這樣的

    -forwardInvocation: 變化

    如果原來的 -forwardInvocation: 有實現,就新加一個 -ORIGforwardInvocation: 指向原 IMP(forwardInvocation:)

    -forwardInvocation: 變化

    由于 -test 方法指向了 _objc_msgForward ,這時調用 -test 方法就會進入消息轉發,消息轉發的第三步進入 -forwardInvocation: 執行新的 IMP(NEWforwardInvocation) ,拿到 invocation , invocation.selector 拼上前綴,然后拼上其他信息直接invoke,最終執行 IMP(NEWtest) ( Aspects 是執行替換的 block )。

    以上是只有一次hook的情況,我們看看兩者都hook的變化

    JSPatch先hook, -test 變化

    JSPatch先hook, -forwardInvocation: 變化

    這時調用 -test 同樣發生消息轉發,進入 -forwardInvocation: 執行 Aspects 的 IMP(AspectsforwardInvocation) ,上文提到 Aspects 把替換的 block 保存為關聯屬性了,到了 -forwardInvocation: 直接拿出來執行,和原來的實現沒有任何關系,所以有了2.2.1 正確的結果。

    Aspects先hook, -test 變化

    Aspects先hook, -forwardInvocation: 變化

    這時調用 -test 同樣發生消息轉發,進入 -forwardInvocation: 執行 JSPatch 的 IMP(JSPatchforwardInvocation) ,執行 _JPtest ,和原來的實現

    沒有任何關系,所以有了2.2.2 正確的結果。

    看到這里,如果細心的話會發現 ORIGtest 指向了 _objc_msgForward ,如果我們在 JSPatch 代碼里調用 self.ORIGtest() 會怎么樣呢?

    2.2.3. Aspects 先采用隨便一種Position hook,JSPatch再hook,JSPatch代碼里調用self.ORIGtest()

    代碼是下面這樣的

    // demo.js
    require('MyClass')
    defineClass('MyClass', {
      test: function() {
    
      self.ORIGtest();
      console.log("jspatch log")
    
    } }); // ViewController.m
  • (void)viewDidLoad { [super viewDidLoad]; [self aspects_hook]; [self jp_hook]; MyClass a = [[MyClass alloc] init]; [a test]; }</code></pre>

    執行結果:

    JPAndAspects[8668:1705052] -[MyClass ORIGtest]: unrecognized selector sent to instance 0x7ff592421a30

    Why?

    -test 和 -forwardInvocation: 的變化同上一步 Aspects 先 hook 。

    由于 -ORIGtest 指向了 _objc_msgForward ,調用方法時進入 -forwardInvocation: 執行 IMP(JSPatchforwardInvocation) , JSPatchforwardInvocation 中有這樣一段代碼

    static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation invocation)
    {
    ...
    JSValue *jsFunc = getJSFunctionInObjectHierachy(slf, JPSelectorName);
    if (!jsFunc) {
    
    JPExecuteORIGForwardInvocation(slf, selector, invocation);
    return;
    
    } ... }</code></pre>

    這個 -ORIGtest 在對象中找不到具體的實現,因此轉發給了 -ORIGINforwardInvocation: 。 注意:這里直接把 -ORIGtest 轉發出去了 ,很顯然 IMP(AspectsforwardInvocation) 也是處理不了這個消息的。因此,出現了 unrecognized selector 異常。

    這里是兩者兼容出現的最大問題,如果 JSPatch 在轉發前判斷一下這個方法是自己添加的 -ORIGxxx ,把前綴 ORIG 去掉再轉發,這個問題就解決了。

    2.2.4. JSPatch先hook, Aspects 再采用AspectPositionInstead(替換)hook,JSPatch代碼里調用self.ORIGtest()

    和2.2.1 相同,不管 JSPatch hook 之后是什么樣的,都只執行 Aspects 的 block

    2.2.5. JSPatch先hook, Aspects 再采用AspectPositionBefore(替換)hook

    代碼如下, 注意把 AspectPositionInstead 替換為 AspectPositionBefore

    // demo.js
    require('MyClass')
    defineClass('MyClass', {
      test: function() {
    
      console.log("jspatch log")
    
    } }); // ViewController.m
  • (void)viewDidLoad { [super viewDidLoad]; [self jp_hook]; [self aspects_hook]; MyClass a = [[MyClass alloc] init]; [a test]; }</code></pre>

    執行結果:

    JPAndAspects[10943:1756624] aspects log
    JPAndAspects[10943:1756624] JSPatch.log: jspatch log

    執行結果如期是正確的。

    IMP(AspectsforwardInvocation) 的部分代碼如下

    SEL originalSelector = invocation.selector;
      SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
      invocation.selector = aliasSelector;
      AspectsContainer objectContainer = objc_getAssociatedObject(self, aliasSelector);
      AspectsContainer classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
      AspectInfo info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];

    // Before hooks. aspect_invoke(classContainer.beforeAspects, info); aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks. BOOL respondsToAlias = YES; if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {

      aspect_invoke(classContainer.insteadAspects, info);
      aspect_invoke(objectContainer.insteadAspects, info);
    

    }else {

      Class klass = object_getClass(invocation.target);
      do {
          if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
              [invocation invoke];
              break;
          }
      }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    

    }

    // After hooks. aspect_invoke(classContainer.afterAspects, info); aspect_invoke(objectContainer.afterAspects, info);

    // If no hooks are installed, call original implementation (usually to throw an exception) if (!respondsToAlias) {

      invocation.selector = originalSelector;
      SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
      if ([self respondsToSelector:originalForwardInvocationSEL]) {
          ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
      }else {
          [self doesNotRecognizeSelector:invocation.selector];
      }
    

    }</code></pre>

    首先執行 Before hooks ;接著查找是否有 Instead hooks ,如果有就執行,如果沒有就在類繼承鏈中查找父類能否響應 -aspects_test ,如果可以就invoke這個invocation,否則把 respondsToAlias 置為 NO ;接著執行 After hooks ;接著 if (!respondsToAlias) 把這個 -test 轉發給 ORIGINforwardInvocation 即 IMP(JSPatchforwardInvocation) 處理了這個消息。 注意這里是把 -test 轉發

    2.2.6. JSPatch先hook, Aspects 再采用AspectPositionAfter hook

    代碼同2.2.5, 注意把 AspectPositionBefore 替換為 AspectPositionAfter

    JPAndAspects[11706:1776713] aspects log
    JPAndAspects[11706:1776713] JSPatch.log: jspatch log

    結果都輸出了,但是順序不對。

    從 IMP(AspectsforwardInvocation) 代碼中不難看出, After hooks 先執行了,再將這個消息轉發。這也可以說是 Aspects 的不足。

    2.2.7. Aspects隨便一種Position hook方法-test2,JSPatch再hook -test,JSPatch代碼里調用self.ORIGtest(), Aspects 以隨便一種Position hook方法-test

    同2.2.5和2.2.6很像,不過前面多了對 -test2 的hook,代碼如下:

    // demo.js
    require('MyClass')
    defineClass('MyClass', {
      test: function() {

      self.ORIGtest();
      console.log("jspatch log")
    

    } }); // ViewController.m

  • (void)viewDidLoad { [super viewDidLoad]; [self aspects_hook_test2]; [self jp_hook]; [self aspects_hook]; MyClass a = [[MyClass alloc] init]; [a test]; }</code></pre>

    代碼執行結果:

    JPAndAspects[12597:1797663] MyClass origin log
    JPAndAspects[12597:1797663] JSPatch.log: jspatch log

    結果是Aspects對 -test 的hook沒有生效。

    Why?

    不廢話,直接看 Aspects 代碼:

    static Class aspect_hookClass(NSObject self, NSError *error) {
      NSCParameterAssert(self);
      Class statedClass = self.class;
      Class baseClass = object_getClass(self);
      NSString className = NSStringFromClass(baseClass);

    // Already subclassed if ([className hasSuffix:AspectsSubclassSuffix]) {

      return baseClass;
    
      // We swizzle a class object, not a single object.
    

    }else if (class_isMetaClass(baseClass)) {

      return aspect_swizzleClassInPlace((Class)self);
      // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
    

    }else if (statedClass != baseClass) {

      return aspect_swizzleClassInPlace(baseClass);
    

    }

    // Default case. Create dynamic subclass. const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; Class subclass = objc_getClass(subclassName);

    if (subclass == nil) {

      subclass = objc_allocateClassPair(baseClass, subclassName, 0);
      if (subclass == nil) {
          NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
          AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
          return nil;
      }
    
      aspect_swizzleForwardInvocation(subclass);
      aspect_hookedGetClass(subclass, statedClass);
      aspect_hookedGetClass(object_getClass(subclass), statedClass);
      objc_registerClassPair(subclass);
    

    }

    objectsetClass(self, subclass); return subclass; }</code></pre>

    這段代碼的作用是區分 self 的類型,進行不同的 swizzleForwardInvocation 。 self 本身可能是一個 Class ;或者self通過 -class 方法返回的self真正的 Class 不同,最典型的 KVO ,會創建一個子類加上 NSKVONotify 前綴,然后重寫class方法,看不懂的可以參考 Objective-C 對象模型 。這兩種情況都對self真正的Class進行 aspect_swizzleClassInPlace ;如果self是一個普通對象,則模仿KVO的實現方式,創建一個子類, swizzle 子類的 -forwardInvocation: ,通過 object_setClass 強行設置 Class 。</p>

    再看 aspect_swizzleClassInPlace

    static Class aspect_swizzleClassInPlace(Class klass) {
      ...

      if (![swizzledClasses containsObject:className]) {
          aspect_swizzleForwardInvocation(klass);
          [swizzledClasses addObject:className];
      }
    

    ... }</code></pre>

    問題就出在這個 aspect_swizzleClassInPlace ,它會判斷如果這個類的 -forwardInvocation: swizzle 過,就什么都不做,但是通過數組這種方式是會出問題,第二次 hook 的時候就不會 -forwardInvocation: 替換成 IMP(AspectsforwardInvocation) ,所以第二次 hook 不生效。相比, JSPatch 的實現就比較合理,判斷兩個IMP是否相等。

    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {

}</code></pre>

2.2.8. Aspects 先采用隨便一種Position hook父類,JSPatch再hook子類,JSPatch代碼里調用self.super().xxx()

代碼是下面這樣的

// demo.js
require('MySubClass')
defineClass('MySubClass', {
    test: function() {
        self.super().test();
        console.log("jspatch log")
    }
});
// ViewController.m

// 增加一個子類 @interface MySubClass : MyClass @end

@implementation MySubClass

  • (void)test { NSLog(@"MySubClass origin log"); } @end

  • (void)viewDidLoad { [super viewDidLoad]; [self aspects_hook]; [self jp_hook]; MySubClass *a = [[MySubClass alloc] init]; [a test]; }</code></pre>

    執行結果:

    JPAndAspects[89642:1600226] -[MySubClass SUPER_test]: unrecognized selector sent to instance 0x7fa4cadabc70

    Why?

    父類 MyClass 的 -test 和 -forwardInvocation: 的變化同2.2.1中原 -forwardInvocation 沒有實現的情況。

    JSPatch 中 super 的實現是新增加一個方法 -SUPER_test ,IMP指向了父類的IMP,由于 -test 指向了 _objc_msgForward ,調用方法時進入 -forwardInvocation: 執行 IMP(JSPatchforwardInvocation) ,執行 self.super().test() 時,實際執行了 -SUPER_test ,這個 -SUPER_test 在對象中找不到具體的實現,發生了 -ORIGtest 一樣的異常 這里是兩者兼容出現的第二個比較嚴重的問題。**

    2.3 總結

    寫到這里,除了 Aspects 對對象的 hook (這種情況很少見,你可以自己測試),可能已經解答了兩者兼容的大部分問題。通過以上分析,得出不兼容的四種情況:

    • Aspects 先 hook 某一方法, JSPatch 再 hook 同一方法且 JSPatch 調用了 self.ORIGxxx() ,結果是異常崩潰。

    • Aspects 先 hook 父類某一方法, JSPatch 再 hook 子類同一方法且 JSPatch 調用了 self.super().xxx() ,結果是異常崩潰。

    • JSPatch 先 hook 某一方法, Aspects 以 After 的方式 hook 同一方法,結果是執行順序不對

    • Aspects 先 hook 任何方法, JSPatch 再 hook 另一方法, Aspects 再 hook 和 JSPatch 相同的方法,結果是最后一次 hook 不生效

     

     

    參考

     

    來自:http://www.jianshu.com/p/dc1deaa1b28e

     

 本文由用戶 pplingling 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!