全面谈谈iOS中的Aspects和JSPatch兼容问题

pplingling 8年前
   <h2><strong>1. 背景</strong></h2>    <p><a href="/misc/goto?guid=4959630207627933220" rel="nofollow,noindex">Aspects</a> 和 <a href="/misc/goto?guid=4958875629367274198" rel="nofollow,noindex">JSPatch</a> 是 iOS 开发中非常常见的两个库。Aspects 提供了方便简单的方法进行面向切片编程(AOP),JSPatch可以让你用 JavaScript 书写原生 iOS APP 和进行热修复。关于实现原理可以参考 <a href="/misc/goto?guid=4959714764269601822" rel="nofollow,noindex">面向切面编程之 Aspects 源码解析及应用</a> 和 <a href="/misc/goto?guid=4959649889825816944" rel="nofollow,noindex">JSPatch wiki</a> 。简单地概括就是将原方法实现替换为 _objc_msgForward (或 _objc_msgForward_stret ),当执行这个方法是直接进入消息转发过程,最后到达替换后的 -forwardInvocation: ,在 -forwardInvocation: 内执行新的方法,这是两者的共同原理。最近项目开发中需要用 JSPatch 替换方法修复一个 bug ,然而这个方法已经使用 Aspects 进行 hook 过了,那么两者同时使用会不会有问题呢?关于这个问题,网上介绍比较详细的是 <a href="/misc/goto?guid=4959714764269601822" rel="nofollow,noindex">面向切面编程之 Aspects 源码解析及应用</a> 和 <a href="/misc/goto?guid=4959714764395446615" rel="nofollow,noindex">有关Swizzling的一个问题</a> ,深入研究后发现这两篇文章讲得都不够全面。本文基于 Aspects 1.4.1 和 JSPatch 1.1 介绍几种测试结果和原因。</p>    <h2><strong>2. 测试</strong></h2>    <p><strong>2.0. 源码</strong></p>    <p><a href="/misc/goto?guid=4959714764479834450" rel="nofollow,noindex">这是本文使用的测试代码</a> ,你可以clone下来,泡杯咖啡,找个安静的地方跟着本文一步一步实践。</p>    <p><strong>2.1. 代码说明</strong></p>    <p>ViewController.m 中首先定义一个简单类 MyClass ,只有 -test 和 -test2 方法,方法内打印log</p>    <pre>  <code class="language-objectivec">@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>    <p>接着是三个hook方法,分别是对 -test 进行 hook 的 -jp_hook 、 -aspects_hook 和对 -test2 进行 hook 的 -aspects_hook_test2</p>    <pre>  <code class="language-objectivec">- (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>    <p>demo.js 代码也非常简单,对 MyClass 的 -test 进行替换</p>    <pre>  <code class="language-objectivec">require('MyClass')  defineClass('MyClass', {      test: function() {  //        self.ORIGtest();          console.log("jspatch log")      }  });</code></pre>    <p><strong>2.2. 具体测试</strong></p>    <p><strong>2.2.1. JSPatch 先 hook 、Aspects 采用 AspectPositionInstead (替换) hook</strong></p>    <p>那么代码就是下面这样,注意把 -aspects_hook 方法设置为 AspectPositionInstead</p>    <pre>  <code class="language-objectivec">// ViewController.m  - (void)viewDidLoad {      [super viewDidLoad];      [self jp_hook];      [self aspects_hook];      MyClass *a = [[MyClass alloc] init];      [a test];  }</code></pre>    <p>执行结果:</p>    <pre>  <code class="language-objectivec">JPAndAspects[2092:1554779] aspects log</code></pre>    <p>结果是 Aspects 正确替换了方法</p>    <p>2.2.2. Aspects 先采用随便一种Position hook,JSPatch再hook</p>    <p>那么代码就是下面这样</p>    <pre>  <code class="language-objectivec">- (void)viewDidLoad {      [super viewDidLoad];      [self aspects_hook];      [self jp_hook];      MyClass *a = [[MyClass alloc] init];      [a test];  }</code></pre>    <p>执行结果:</p>    <pre>  <code class="language-objectivec">JPAndAspects[2774:1565702] JSPatch.log: jspatch log</code></pre>    <p>结果是 JSPatch 正确替换了方法</p>    <p>Why?</p>    <p>前面说到, hook 会替换该方法和 -forwardInvocation: ,我们先看看方法被 hook 前后的变化</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b3b934be290ad56212723fe23a620366.png"></p>    <p style="text-align:center">原方法对应关系</p>    <p>方法替换后原方法指向了 _objc_msgForward ,同时添加一个方法 PREFIXtest ( JSPatch 是 ORIGtest , Aspects 是 aspects_test )指向了原来的实现。 JSPatch 新增了一个方法指向 IMP(NEWtest) , Aspects 则保存block为关联属性</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e634df9ace2a6c9dcb4771c0b074b3f9.png"></p>    <p style="text-align:center">-test 变化</p>    <p>-forwardInvocation: 的变化也相似,原来的 -forwardInvocation: 没实现是这样的</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3927c3b5f623fc0d2e404de40bcace80.png">-forwardInvocation: 变化</p>    <p>如果原来的 -forwardInvocation: 有实现,就新加一个 -ORIGforwardInvocation: 指向原 IMP(forwardInvocation:)</p>    <p><img src="https://simg.open-open.com/show/bb4f8f39406555b88576e144f5144aa5.png"></p>    <p>-forwardInvocation: 变化</p>    <p>由于 -test 方法指向了 _objc_msgForward ,这时调用 -test 方法就会进入消息转发,消息转发的第三步进入 -forwardInvocation: 执行新的 IMP(NEWforwardInvocation) ,拿到 invocation , invocation.selector 拼上前缀,然后拼上其他信息直接invoke,最终执行 IMP(NEWtest) ( Aspects 是执行替换的 block )。</p>    <p>以上是只有一次hook的情况,我们看看两者都hook的变化</p>    <p><img src="https://simg.open-open.com/show/3bfbfebfe0143dcca9b3d5ef821aa5d1.png"></p>    <p style="text-align:center">JSPatch先hook, -test 变化</p>    <p><img src="https://simg.open-open.com/show/140371b4e2a9a26ebdc27cb2176d2399.png"></p>    <p style="text-align:center">JSPatch先hook, -forwardInvocation: 变化</p>    <p>这时调用 -test 同样发生消息转发,进入 -forwardInvocation: 执行 Aspects 的 IMP(AspectsforwardInvocation) ,上文提到 Aspects 把替换的 block 保存为关联属性了,到了 -forwardInvocation: 直接拿出来执行,和原来的实现没有任何关系,所以有了2.2.1 正确的结果。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d543d6cc95f7320db1b40b5da0a9cd95.png"></p>    <p style="text-align:center">Aspects先hook, -test 变化</p>    <p><img src="https://simg.open-open.com/show/d34fc658c7fd016ed5dcb4c3d76d4c12.png"></p>    <p style="text-align:center">Aspects先hook, -forwardInvocation: 变化</p>    <p>这时调用 -test 同样发生消息转发,进入 -forwardInvocation: 执行 JSPatch 的 IMP(JSPatchforwardInvocation) ,执行 _JPtest ,和原来的实现</p>    <p>没有任何关系,所以有了2.2.2 正确的结果。</p>    <p>看到这里,如果细心的话会发现 ORIGtest 指向了 _objc_msgForward ,如果我们在 JSPatch 代码里调用 self.ORIGtest() 会怎么样呢?</p>    <p><strong>2.2.3. Aspects 先采用随便一种Position hook,JSPatch再hook,JSPatch代码里调用self.ORIGtest()</strong></p>    <p>代码是下面这样的</p>    <pre>  <code class="language-objectivec">// 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>    <p>执行结果:</p>    <pre>  <code class="language-objectivec">JPAndAspects[8668:1705052] -[MyClass ORIGtest]: unrecognized selector sent to instance 0x7ff592421a30</code></pre>    <p><strong>Why?</strong></p>    <p>-test 和 -forwardInvocation: 的变化同上一步 Aspects 先 hook 。</p>    <p>由于 -ORIGtest 指向了 _objc_msgForward ,调用方法时进入 -forwardInvocation: 执行 IMP(JSPatchforwardInvocation) , JSPatchforwardInvocation 中有这样一段代码</p>    <pre>  <code class="language-objectivec">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>    <p>这个 -ORIGtest 在对象中找不到具体的实现,因此转发给了 -ORIGINforwardInvocation: 。 <strong> 注意:这里直接把 -ORIGtest 转发出去了 </strong> ,很显然 IMP(AspectsforwardInvocation) 也是处理不了这个消息的。因此,出现了 unrecognized selector 异常。</p>    <p>这里是两者兼容出现的最大问题,如果 JSPatch 在转发前判断一下这个方法是自己添加的 -ORIGxxx ,把前缀 ORIG 去掉再转发,这个问题就解决了。</p>    <p><strong>2.2.4. JSPatch先hook, Aspects 再采用AspectPositionInstead(替换)hook,JSPatch代码里调用self.ORIGtest()</strong></p>    <p>和2.2.1 相同,不管 JSPatch hook 之后是什么样的,都只执行 Aspects 的 block</p>    <p><strong>2.2.5. JSPatch先hook, Aspects 再采用AspectPositionBefore(替换)hook</strong></p>    <p>代码如下, <strong>注意把</strong> AspectPositionInstead <strong>替换为</strong> AspectPositionBefore</p>    <pre>  <code class="language-objectivec">// 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>    <p>执行结果:</p>    <pre>  <code class="language-objectivec">JPAndAspects[10943:1756624] aspects log  JPAndAspects[10943:1756624] JSPatch.log: jspatch log</code></pre>    <p>执行结果如期是正确的。</p>    <p>IMP(AspectsforwardInvocation) 的部分代码如下</p>    <pre>  <code class="language-objectivec">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>    <p>首先执行 Before hooks ;接着查找是否有 Instead hooks ,如果有就执行,如果没有就在类继承链中查找父类能否响应 -aspects_test ,如果可以就invoke这个invocation,否则把 respondsToAlias 置为 NO ;接着执行 After hooks ;接着 if (!respondsToAlias) 把这个 -test 转发给 ORIGINforwardInvocation 即 IMP(JSPatchforwardInvocation) 处理了这个消息。 <strong> 注意这里是把 -test 转发 </strong></p>    <p><strong>2.2.6. JSPatch先hook, Aspects 再采用AspectPositionAfter hook</strong></p>    <p>代码同2.2.5, <strong>注意把</strong> AspectPositionBefore <strong>替换为</strong> AspectPositionAfter</p>    <pre>  <code class="language-objectivec">JPAndAspects[11706:1776713] aspects log  JPAndAspects[11706:1776713] JSPatch.log: jspatch log</code></pre>    <p>结果都输出了,但是顺序不对。</p>    <p>从 IMP(AspectsforwardInvocation) 代码中不难看出, After hooks 先执行了,再将这个消息转发。这也可以说是 Aspects 的不足。</p>    <p><strong>2.2.7. Aspects随便一种Position hook方法-test2,JSPatch再hook -test,JSPatch代码里调用self.ORIGtest(), Aspects 以随便一种Position hook方法-test</strong></p>    <p>同2.2.5和2.2.6很像,不过前面多了对 -test2 的hook,代码如下:</p>    <pre>  <code class="language-objectivec">// 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>    <p>代码执行结果:</p>    <pre>  <code class="language-objectivec">JPAndAspects[12597:1797663] MyClass origin log  JPAndAspects[12597:1797663] JSPatch.log: jspatch log</code></pre>    <p>结果是Aspects对 -test 的hook没有生效。</p>    <p>Why?</p>    <p>不废话,直接看 Aspects 代码:</p>    <pre>  <code class="language-objectivec">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);      }        object_setClass(self, subclass);      return subclass;  }</code></pre>    <p>这段代码的作用是区分 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>    <p>再看 aspect_swizzleClassInPlace</p>    <pre>  <code class="language-objectivec">static Class aspect_swizzleClassInPlace(Class klass) {      ...          if (![swizzledClasses containsObject:className]) {              aspect_swizzleForwardInvocation(klass);              [swizzledClasses addObject:className];          }      ...  }</code></pre>    <p>问题就出在这个 aspect_swizzleClassInPlace ,它会判断如果这个类的 -forwardInvocation: swizzle 过,就什么都不做,但是通过数组这种方式是会出问题,第二次 hook 的时候就不会 -forwardInvocation: 替换成 IMP(AspectsforwardInvocation) ,所以第二次 hook 不生效。相比, JSPatch 的实现就比较合理,判断两个IMP是否相等。</p>    <pre>  <code class="language-objectivec">if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {    }</code></pre>    <p><strong>2.2.8. Aspects 先采用随便一种Position hook父类,JSPatch再hook子类,JSPatch代码里调用self.super().xxx()</strong></p>    <p>代码是下面这样的</p>    <pre>  <code class="language-objectivec">// 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>    <p>执行结果:</p>    <pre>  <code class="language-objectivec">JPAndAspects[89642:1600226] -[MySubClass SUPER_test]: unrecognized selector sent to instance 0x7fa4cadabc70</code></pre>    <p><strong>Why?</strong></p>    <p>父类 MyClass 的 -test 和 -forwardInvocation: 的变化同2.2.1中原 -forwardInvocation 没有实现的情况。</p>    <p>JSPatch 中 super 的实现是新增加一个方法 -SUPER_test ,IMP指向了父类的IMP,由于 -test 指向了 _objc_msgForward ,调用方法时进入 -forwardInvocation: 执行 IMP(JSPatchforwardInvocation) ,执行 self.super().test() 时,实际执行了 -SUPER_test ,这个 -SUPER_test 在对象中找不到具体的实现,发生了 -ORIGtest 一样的异常 <strong>。</strong> 这里是两者兼容出现的第二个比较严重的问题。**</p>    <p><strong>2.3 总结</strong></p>    <p>写到这里,除了 Aspects 对对象的 hook (这种情况很少见,你可以自己测试),可能已经解答了两者兼容的大部分问题。通过以上分析,得出不兼容的四种情况:</p>    <ul>     <li> <p>Aspects 先 hook 某一方法, JSPatch 再 hook 同一方法且 JSPatch 调用了 self.ORIGxxx() ,结果是异常崩溃。</p> </li>     <li> <p>Aspects 先 hook 父类某一方法, JSPatch 再 hook 子类同一方法且 JSPatch 调用了 self.super().xxx() ,结果是异常崩溃。</p> </li>     <li> <p>JSPatch 先 hook 某一方法, Aspects 以 After 的方式 hook 同一方法,结果是执行顺序不对</p> </li>     <li> <p>Aspects 先 hook 任何方法, JSPatch 再 hook 另一方法, Aspects 再 hook 和 JSPatch 相同的方法,结果是最后一次 hook 不生效</p> </li>    </ul>    <p> </p>    <p> </p>    <p>参考</p>    <ul>     <li><a href="/misc/goto?guid=4959714764269601822" rel="nofollow,noindex">http://wereadteam.github.io/2016/06/30/Aspects/</a></li>     <li><a href="/misc/goto?guid=4959714764395446615" rel="nofollow,noindex">http://www.jianshu.com/p/d5c3c2f236b8</a></li>    </ul>    <p> </p>    <p>来自:http://www.jianshu.com/p/dc1deaa1b28e</p>    <p> </p>