全面谈谈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>