背景
在我们实际开发的ios项目中,使用了Jspatch和Aspect两个第三方库
先说下这两个是什么东东,能用来做什么,我们项目为何要引入这两个库
Aspect 源于切面编程的概念,可以在一个类的seletor的前后添加代码,或替换一个selector的实现。应用场景主要是处理UI表现,修改系统控件满足我们的需求。简易教程可参看aspect doc
Jspatch 在后期添加,appstore发布周期长,有线上问题需要即时修复,等不及发版本,需要做hotfix。到目前为止也帮助我们解决了很多线上问题,贡献很大
之前两者从未遇到过冲突问题,但在有一次用Jspatch在做hotfix的时候,修改了UIWebview的一个selector A,可偏偏Aspect也修改了UIWebview的一个selector B, 就是两者修改了同一个类的不同方法!
代码运行起来
必挂在aspect的__ASPECTS_ARE_BEING_CALLED__
方法里,具体位置在
但当时我们仍需要做hotfix,只能绕路了,不能hook UIWebview了!找其他方法。
为了找寻其原因,我们来脱离项目,做个demo,来完整看看Jspatch和Aspect的兼容问题。
Demo
我们做的这个Demo分以下几个步骤,正好不熟悉Jspatch的同学和Aspect的同学也可以熟悉一下。
- Jspatch修改Viewcontroller的viewWillAppear的方法,在方法里修改背景色为
黑色
- 屏蔽Jspatch,Aspect单独修改的viewWillAppear的方法,在方法里修改背景色为
黄色
- Jspatch和Aspect,都替换viewWillAppear的方法,颜色仍修改为各自颜色
- Jspatch和Aspect,替换不同方法
- Jspatch替换类的实例方法,Aspect替换实例对象方法
步骤一(Jspatch修改背景色)
首先我们创建一个新的工程,xcode -> file -> new -> project -> Signal view。工程目录下创建Podfile。
Podfile内容如下, 将jspatch和aspect两个库引入进来
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'JADemo' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for JADemo
pod 'JSPatch'
pod 'Aspects'
end
本地添加一个
demo.js
的文件,js代码替换viewWillAppear
, 设置背景色为黑色
require('UIColor')
defineClass('ViewController', {
viewWillAppear: function(animated) {
self.super().viewWillAppear(animated);
self.view().setBackgroundColor(UIColor.blackColor());
},
});
接下来Appdelegate
里加入
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[JPEngine startEngine];
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
if (sourcePath) {
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
[JPEngine evaluateScript:script];
}
return YES;
}
运行...
修改完成!
步骤二(Aspects修改背景色)
屏蔽Jspatch,将demo.js 重命名为demo1.js
加入aspects代码
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
[super viewWillAppear:animated];
self.view.backgroundColor = [UIColor yellowColor];
} error:nil];
}
步骤三(Jspatch和Aspects同时修改)
现在将demo1.js名字改回demo.js, 恢复Jspatch修改功效。
运行...
结果是Aspects修改生效了
步骤四(Jspatch和Aspects同时修改不同方法)
我们这时要有两种不同的表现,那用Jspatch来修改背景色,Aspects来弹一个alert。看看是否都生效了吧。Aspects的修改方法,改为另一个viewDidAppear
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[ViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
[super viewDidAppear:animated];
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"alert" message:@"content" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alertController animated:true completion:nil];
} error:nil];
}
运行...
哎呀,没有冲突啊,都生效了,是不是搞错了。注意,我们用Aspects替换的是类的实例方法,也就是ViewController创建出来对象,都会弹一个alert出来。而我们项目中遇到情况是用Aspects修改了具体实例。所以进入最后一个步骤。
步骤五(Jspatch和Aspects同时修改不同方法, Aspects修改具体实例)
修改Aspects代码, self
代替ViewController
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, BOOL animated) {
[super viewDidAppear:animated];
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"alert" message:@"content" preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alertController animated:true completion:nil];
} error:nil];
}
运行...
如果你加了exception断点的话,就会重现项目中情况了。
Demo总结
这里先做个小总结, Jspatch 和 Aspects 实际冲突的表现
- Jspatch 与 Aspects 在修改同一个类的同一方法时, 是可行的,但只有一个生效,取决于先后顺序
步骤三演示中,我只验证了Jspatch在前的情况,有兴趣的同学可以试验一下反过来的情况
Jspatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个类的实例方法,而非具体实例,两者可以同时生效
Jspatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个具体类的实例, Aspects assert,结果是Aspects生效
因此针对步骤五的情况是需要我们注意的。
现象研究
由表及里,先研究下我们最关心的步骤五。
forwardInvocation
冲突的原由是Jspatch和Aspects都使用了forwardInvocation,来重写消息转发。
这里做一下runtime的复习
我们知道对oc对象的方法调用,通过发送消息的方式,底层会调用void objc_msgSend(id self, SEL cmd, ...)
, 去对象的类中方法列表
中搜索,如果没找到就去父类的方法列表
中去寻找,如果找不到就走消息转发
流程
对象在收到未定义的方法的时候,会首先走 +(BOOL)resolveInstanceMethod:(SEL)selector
, 这里给你的机会去动态添加一个方法
如果这里未处理,走到-(id)forwardingTargetForSelector:(SEL)selector
, 返回一个能处理此消息的对象
如果仍未处理走到最终的-(void)forwardInvocation:(NSInvocation*)invocation
, forwardInvocation叫做完整的消息转发,之所以叫完整消息转发, 它包含一次调用的所有信息,调用对象,调用方法名,实现,参数,返回值。
Jspatch 和 Aspects 为了自定义一个方法调用,直接将方法调用,转发给forwardInvocation,在forwardInvocation中做一些复杂的工作。
如果你override一个forwardInvocation方法在一个类中,你发现你的方法并不会进入,这是因为这个方法在类中可以找到,不出触发消息转发机制,上面所讲。那如何强制进入forwardInvocation呢
Jspatch 和 Aspects 都会找到这样一个关键字,_objc_msgForward
,在Demo中搜索一下
拿Jspatch举例,Jspatch会最终对类的某个selector调用class_replaceMethodclass_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
,Aspects也同样有此操作。
这样这个在调用这个类对象的selector,就直接进入forwardInvocation了。用图表示一下,工作原理
Jspatch 和 Aspects 为了保留原来的实现,添加了自己forwardInvocation,并replace了原来的forwardInvocation,保留了原版,可恢复。
现象分析之步骤三
现在可以思考下,步骤三,当替换了同一个方法时,方法首先被Jspatch强制转发给forwardInvocation,并替换了forwaInvocation的实现,然后后来Aspects,也替换了forwardInvocation的实现,就是把jspatch的实现给替换走了。
因此当替换同一个方法时,与顺序有关。
现象分析之步骤四
回顾步骤四,替换不同方法(Jspatch替换了viewWillAppear,Aspects替换viewDidAppear),Aspects替换类的实例方法。
到这里可能有疑惑,如果按照现象分析之步骤三的图
, Jspatch的实现被Aspects替换,那么Jspatch应该没有启动作用才对,怎么会都奏效呢。
这就要进入Aspects的forwardInvocation看看它是怎么处理的。Aspects重写的forwardInvocation的函数是__ASPECTS_ARE_BEING_CALLED__
。
关注这个函数的最下面的代码
断点断住的地方,就是去执行Jspatch的地方。respondsToAlias代表,Aspects hook了这个selector(Aspects内部会记录下自己hook的所有selector)。这部分代码意思是,当Aspects没hook这个selector的时候,就调用原来的forwardInvocation实现。大家注意我用红圈圈起来的地方,就知道进来的方法就是Jspatch hook的viewWillAppear。
那么originalForwardInvocationSEL
, 肯定保留了Jspatch的实现,找一下源头。搜索AspectsForwardInvocationSelectorName
就可以找到了。
很明显,Aspects在替换原来forwardInvocation的时候,把原来的实现保存在AspectsForwardInvocationSelectorName
,这是一个新增方法。用图表示下
现象分析之步骤五
看样子,Aspects处理还是很完美,但是往往是看上去完美的地方,还是会出现问题,该来的终究回来。
最后来解释那个assert。步骤五与步骤四不同的地方在于,Aspects替换的是具体对象的方法。
经过调试,发现在获取的Jspatch原有实现是空! 注意红框里的。AspectsForwardInvocationSelectorName
也不会添加成功。
问题出在class_replaceMethod
这个方法上
文档上表明,这个函数的返回值是,一个类定义了的方法的实现,它不会去找super class。然而这个class_replaceMethod
的第一个参数klass
是Aspects动态创建的一个Viewcontroller的子类(为何动态创建子类也是为方便的回复原状)。你控制台上po klass,会输出ViewController_Aspects_
。然而定义forwardInvocation
的类是Viewcontroller( Jspatch有重新实现过Viewcontroller的forwardInvocation
)
回到那个assert
由于origin forwardInvoca 为nil,AspectsForwardInvocationSelectorName这方法也就不复存在了。因此走到了那个assert。
解决方案
到目前为止,原因已经全部查明,接下来看看有什么办法来解决步骤五遇到的问题,也就是我们项目中遇到的问题。
我们首先任务就是解决,获取的Jspatch原有实现为nil的问题,既然class_replaceMethod
不能够上诉super class去获取实现,我们使用另一个获取实现的方法class_getInstanceMethod
, class_getInstanceMethod
文档描述可以获取父类实现。
修改代码
static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
- // If there is no method, replace will act like class_addMethod.
- IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
- if (originalImplementation) {
- class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+ // get origin forwardInvocation impl, include superClass impl,not NSObject impl, and class method to kClass
+ Method originalMethod = class_getInstanceMethod(klass, @selector(forwardInvocation:));
+ if (originalMethod != class_getInstanceMethod([NSObject class], @selector(forwardInvocation:))) {
+ IMP originalImplementation = method_getImplementation(originalMethod);
+ if (originalImplementation) {
+ // If there is no method, replace will act like class_addMethod.
+ class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
+ }
}
+ class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
获取原有实现,排除[NSObject forwardInvocation], 因为搜索父类,会搜到[NSObject forwardInvocation],这个我们不需要addMethod,我们只关心,我们重写的。
这样Jspatch的实现就会找到,看图中红框,很明显名字看出来那就是Jspatch的实现,我们找到了
不过当再次运行的时候,assert还是会跳到,警报还没有完全解除。
respondsToSelector仍不能认识AspectsForwardInvocationSelectorName
这个方法。
查文档发现
respondsToSelector实质会去调用class获取类是Viewcontroller(因此子类的class方法被Aspects重写为基类的,为了动态创建子类对用户是透明的),然后查看这个类的方法列表。然而由于Aspects的动态创建子类,AspectsForwardInvocationSelectorName
添加到了Aspects的动态创建的子类ViewController_Aspects_
上!难怪找不到了!
我们仍然使用class_getInstanceMethod
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
- if ([self respondsToSelector:originalForwardInvocationSEL]) {
- ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
- }else {
+ Method method = class_getInstanceMethod(object_getClass(self), originalForwardInvocationSEL);
+ if (method) {
+ typedef void (*FuncType)(id, SEL, NSInvocation *);
+ FuncType imp = (FuncType)method_getImplementation(method);
+ imp(self, selector, invocation);
+ } else {
[self doesNotRecognizeSelector:invocation.selector];
}
运行...
冲突解决了!
总结
Jspatch和Aspects冲突问题和解决方案调查完毕,如果你有遇到和我们一样的问题,可以考虑我的修复方案。Jspatch和Aspects都是hack的方案,大家在开发过程中,还是尽量少用,我们目前项目中hack了很多系统控件,完成需求,这使得系统升级的时候,我们变得非常被动,因为我们的hack不知道会出现什么问题。
注意
我还尝试过在jspatch的js里,写aspects修改方法,这样是不起作用的,jspatch会自己修改block参数个数,Aspects发现参数不一致, 是不会执行的,因此不要试图这样做