Aspects 和 JSPatch 是 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 介绍几种测试结果和缘由。javascript
这是本文使用的测试代码,你能够clone下来,泡杯咖啡,找个安静的地方跟着本文一步一步实践。php
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
接着是三个hook方法,分别是对-test
进行hook
的-jp_hook
、-aspects_hook
和对-test2
进行hook
的-aspects_hook_test2
java
- (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]; }
demo.js
代码也很是简单,对MyClass
的-test
进行替换git
require('MyClass') defineClass('MyClass', { test: function() { // self.ORIGtest(); console.log("jspatch log") } });
那么代码就是下面这样,注意把-aspects_hook
方法设置为AspectPositionInstead
程序员
// ViewController.m - (void)viewDidLoad { [super viewDidLoad]; [self jp_hook]; [self aspects_hook]; MyClass *a = [[MyClass alloc] init]; [a test]; }
执行结果:github
JPAndAspects[2092:1554779] aspects log
结果是 Aspects
正确替换了方法编程
那么代码就是下面这样后端
- (void)viewDidLoad { [super viewDidLoad]; [self aspects_hook]; [self jp_hook]; MyClass *a = [[MyClass alloc] init]; [a test]; }
执行结果:数组
JPAndAspects[2774:1565702] JSPatch.log: jspatch log
结果是 JSPatch
正确替换了方法
前面说到,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的变化
-test
变化
-forwardInvocation:
变化
这时调用-test
一样发生消息转发,进入-forwardInvocation:
执行Aspects
的IMP(AspectsforwardInvocation)
,上文提到Aspects
把替换的block
保存为关联属性了,到了-forwardInvocation:
直接拿出来执行,和原来的实现没有任何关系,因此有了2.2.1 正确的结果。
-test
变化
-forwardInvocation:
变化
这时调用-test
一样发生消息转发,进入-forwardInvocation:
执行JSPatch
的IMP(JSPatchforwardInvocation)
,执行_JPtest
,和原来的实现
没有任何关系,因此有了2.2.2 正确的结果。
看到这里,若是细心的话会发现ORIGtest
指向了_objc_msgForward
,若是咱们在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]; }
执行结果:
JPAndAspects[8668:1705052] -[MyClass ORIGtest]: unrecognized selector sent to instance 0x7ff592421a30
-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; } ... }
这个-ORIGtest
在对象中找不到具体的实现,所以转发给了-ORIGINforwardInvocation:
。注意:这里直接把-ORIGtest
转发出去了,很显然IMP(AspectsforwardInvocation)
也是处理不了这个消息的。所以,出现了unrecognized selector
异常。
这里是二者兼容出现的最大问题,若是JSPatch
在转发前判断一下这个方法是本身添加的-ORIGxxx
,把前缀ORIG
去掉再转发,这个问题就解决了。
和2.2.1 相同,无论JSPatch
hook
以后是什么样的,都只执行Aspects
的block
代码以下,注意把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]; }
执行结果:
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]; } }
首先执行Before hooks
;接着查找是否有Instead hooks
,若是有就执行,若是没有就在类继承链中查找父类可否响应-aspects_test
,若是能够就invoke这个invocation,不然把respondsToAlias
置为NO
;接着执行After hooks
;接着if (!respondsToAlias)
把这个-test
转发给ORIGINforwardInvocation
即IMP(JSPatchforwardInvocation)
处理了这个消息。注意这里是把-test
转发
代码同2.2.5,注意把AspectPositionBefore
替换为AspectPositionAfter
JPAndAspects[11706:1776713] aspects log JPAndAspects[11706:1776713] JSPatch.log: jspatch log
结果都输出了,可是顺序不对。
从IMP(AspectsforwardInvocation)
代码中不难看出,After hooks
先执行了,再将这个消息转发。这也能够说是Aspects
的不足。
同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]; }
代码执行结果:
JPAndAspects[12597:1797663] MyClass origin log JPAndAspects[12597:1797663] JSPatch.log: jspatch log
结果是Aspects对-test
的hook没有生效。
不废话,直接看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); } object_setClass(self, subclass); return subclass; }
这段代码的做用是区分self
的类型,进行不一样的swizzleForwardInvocation
。self
自己多是一个Class
;或者self经过-class
方法返回的self真正的Class
不一样,最典型的KVO
,会建立一个子类加上NSKVONotify_
前缀,而后重写class方法,看不懂的能够参考Objective-C 对象模型
。这两种状况都对self真正的Class进行aspect_swizzleClassInPlace
;若是self是一个普通对象,则模仿KVO的实现方式,建立一个子类,swizzle
子类的-forwardInvocation:
,经过object_setClass
强行设置Class
。
再看aspect_swizzleClassInPlace
static Class aspect_swizzleClassInPlace(Class klass) { ... if (![swizzledClasses containsObject:className]) { aspect_swizzleForwardInvocation(klass); [swizzledClasses addObject:className]; } ... }
问题就出在这个aspect_swizzleClassInPlace
,它会判断若是这个类的-forwardInvocation:
swizzle
过,就什么都不作,可是经过数组这种方式是会出问题,第二次hook
的时候就不会-forwardInvocation:
替换成IMP(AspectsforwardInvocation)
,因此第二次hook
不生效。相比,JSPatch
的实现就比较合理,判断两个IMP是否相等。
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) { }
代码是下面这样的
// 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]; }
执行结果:
JPAndAspects[89642:1600226] -[MySubClass SUPER_test]: unrecognized selector sent to instance 0x7fa4cadabc70
父类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
同样的异常。
这里是二者兼容出现的第二个比较严重的问题。
写到这里,除了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
不生效简书做为一个优质原创内容社区,拥有大量优质原创内容,提供了极佳的阅读和书写体验,吸引了大量文字爱好者和程序员。简书技术团队在这里分享技术心得体会,是但愿抛砖引玉,吸引更多的程序员大神来简书记录、分享、交流本身的心得体会。这个专题之后会不按期更新简书技术团队的文章,包括Android、iOS、前端、后端等等,欢迎你们关注。
参考