[iOS] JSPatch 和 Aspects 兼容问题研究

Posted 易动开发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[iOS] JSPatch 和 Aspects 兼容问题研究相关的知识,希望对你有一定的参考价值。

背景

ios开发中,我们使用到了JSPatch和Aspects这两个第三方库。

先说下这两个是什么东西,能用来做什么以及我们为何要引入这两个库。

  • Aspects 源于切面编程的概念,可以在一个类的 selector 的前后添加代码,或替换一个 selector 的实现。应用场景主要是处理UI表现,修改系统控件满足我们的需求。

  • JSPatch 在后期添加,App Store发布周期长,有线上问题需要即时修复,等不及发版本,需要做hotfix。到目前为止也帮助我们解决了很多线上问题,贡献很大。

之前两者从未遇到过冲突问题,但有一次用JSPatch在做hotfix的时候就遇到了问题:我们的目标是修改 UIWebview 的一个selector A,但此时Aspects已经修改了 UIWebview 的另一个 selector B。

结果代码运行起来,挂在Aspects的__Aspects_ARE_BEING_CALLED__方法里,见下图。

为了找寻其原因,我们来脱离项目,做个demo,来完整看看 JSPatch 和 Aspects 的兼容问题。

Demo

我们做的这个Demo分以下几个步骤。

  1. JSPatch 修改 ViewController 的 viewWillAppear 的方法,在方法里修改背景色为黑色

  2. 屏蔽 JSPatch,Aspects单独修改的 viewWillAppear 的方法,在方法里修改背景色为黄色

  3. JSPatch 和 Aspects,都替换 viewWillAppear 的方法,颜色仍修改为各自颜色

  4. JSPatch 和 Aspects,替换不同方法

  5. JSPatch 替换类的实例方法,Aspects 替换实例对象方法

步骤一(JSPatch修改背景色)

首先我们创建一个新的工程,Xcode -> File -> New -> Project -> Single view, 并在工程目录下创建 Podfile。

Podfile 内容如下, 将JSPatch和Aspects两个库引入进来

[iOS] JSPatch 和 Aspects 兼容问题研究

本地添加一个demo.js的文件,js代码替换viewWillAppear, 设置背景色为黑色

[iOS] JSPatch 和 Aspects 兼容问题研究
接下来Appdelegate里加入

[iOS] JSPatch 和 Aspects 兼容问题研究

运行...

[iOS] JSPatch 和 Aspects 兼容问题研究

修改完成!

步骤二(Aspects修改背景色)

屏蔽JSPatch,将demo.js重命名为demo1.js

加入Aspects代码

[iOS] JSPatch 和 Aspects 兼容问题研究

[iOS] JSPatch 和 Aspects 兼容问题研究

步骤三(JSPatch和Aspects同时修改)

现在将demo1.js名字改回demo.js, 恢复JSPatch修改功效。

运行...

[iOS] JSPatch 和 Aspects 兼容问题研究

结果是Aspects修改生效了

步骤四(JSPatch和Aspects同时修改不同方法)

我们这时要有两种不同的表现,那用JSPatch来修改背景色,Aspects来弹一个alert。看看是否都生效了吧。Aspects的修改方法,改为另一个viewDidAppear

[iOS] JSPatch 和 Aspects 兼容问题研究

运行...

[iOS] JSPatch 和 Aspects 兼容问题研究

哎呀,没有冲突啊,都生效了,是不是搞错了?注意,我们用Aspects替换的是类的实例方法,也就是ViewController创建出来对象,都会弹一个alert出来。而我们项目中遇到情况是用Aspects修改了具体实例。所以进入最后一个步骤。

步骤五(JSPatch和Aspects同时修改不同方法, Aspects修改具体实例)

修改Aspects代码, self代替ViewController

[iOS] JSPatch 和 Aspects 兼容问题研究

运行...

如果你加了 exception breakpoint 的话,就会重现项目中情况了。

[iOS] JSPatch 和 Aspects 兼容问题研究

Demo 总结

这里先做个小总结, JSPatch 和 Aspects 实际冲突的表现

  • JSPatch 与 Aspects 在修改同一个类的同一方法时, 是可行的,但只有一个生效,取决于先后顺序

步骤三演示中,我只验证了JSPatch在前的情况,有兴趣的同学可以试验一下反过来的情况

  • JSPatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个类的实例方法,而非具体实例,两者可以同时生效

  • JSPatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个具体类的实例, Aspects assert,结果是Aspects生效

因此针对步骤五的情况是需要我们注意的。

现象研究

由表及里,先研究下我们最关心的步骤五。

forwardInvocation

冲突的原由是 JSPatch 和 Aspects 都使用了 forwardInvocation 来重写消息转发。

我们知道对oc对象的方法调用,是通过发送消息的方式进行的。任何调用到 runtime 层,都会被转化为objc_msgSend族函数的调用。这些族函数会在对象所属类的方法列表中搜索对应的实现,如果没找到就去父类的方法列表中去寻找,如果找不到就走消息转发流程。

[iOS] JSPatch 和 Aspects 兼容问题研究

如上图所示,对象在收到未定义的方法的时候,会首先走 +(BOOL)resolveInstanceMethod:(SEL)selector, 这里给你一个动态添加方法的机会。

如果这里未处理,走到-(id)forwardingTargetForSelector:(SEL)selector, 返回一个能处理此消息的对象。

如果仍未处理走到最终的-(void)forwardInvocation:(NSInvocation*)invocation, forwardInvocation 叫做完整的消息转发,之所以这样称呼, 是因为它包含了一次调用的所有信息:调用对象,调用方法名,实现,参数,返回值等。

而 JSPatch 和 Aspects 便是利用到了 forwardInvocation。

JSPatch 和 Aspects 为了自定义一个方法调用,直接将方法调用,转发给forwardInvocation,在forwardInvocation中做一些复杂的工作。

如果你做个试验:在一个类中重写forwardInvocation,你会发现这个方法并不会得到执行,这是因为当前调用的 selector 都可以直接在类中找到实现,不会触发消息转发机制。这跟两个库的目标不同,那如何做呢?

在 JSPatch 和 Aspects 中我们都可以找到这样一个函数:_objc_msgForward,这两个库便是将目标 selector 的实现直接替换为了_objc_msgForward。这样在调用发生时,就直接进入 forwardInvocation 了。工作原理如下图所示。

[iOS] JSPatch 和 Aspects 兼容问题研究

现象分析之步骤三

现在可以思考下步骤三,当替换了同一个方法时,方法首先被 JSPatch 强制转发给forwardInvocation,并替换了 forwaInvocation 的实现,接着 Aspects 也替换了forwardInvocation 的实现,这样就把 JSPatch 的实现给替换走了。

[iOS] JSPatch 和 Aspects 兼容问题研究

因此当替换同一个方法时,只会保留后替换的。

现象分析之步骤四

回顾步骤四,替换不同方法(JSPatch 替换 viewWillAppear,Aspects 替换 viewDidAppear),并且此时 Aspects 是直接替换类的实例方法。

到这里可能有疑惑,如果按照现象分析之步骤三的图, JSPatch的实现被Aspects替换,那么JSPatch应该没有启动作用才对,怎么会都奏效呢。

这就要进入 Aspects 的 forwardInvocation 看看它是怎么处理的。Aspects 重写forwardInvocation 的函数是__Aspects_ARE_BEING_CALLED__

关注这个函数的这段代码
[iOS] JSPatch 和 Aspects 兼容问题研究

显然,断点断住的地方,就是去执行 JSPatch 的地方。respondsToAlias这个变量代表了Aspects 已经hook了这个selector(Aspects内部会记录下自己hook的所有selector)。那么,这部分代码意思就是说,当Aspects没hook这个selector的时候,就调用原来的forwardInvocation实现。大家注意我标红的地方,就知道进来的方法其实就是已经被 JSPatch hook 的 viewWillAppear。

那么我们猜测originalForwardInvocationSEL肯定保留了原有的实现,找一下它是怎么来的。

[iOS] JSPatch 和 Aspects 兼容问题研究

很明显,Aspects 在替换forwardInvocation的时候,把原来的实现保存在AspectsForwardInvocationSelectorName,这是一个新增方法。用图表示下。

[iOS] JSPatch 和 Aspects 兼容问题研究

现象分析之步骤五

看样子,Aspects处理还是很完美,但是往往是看上去完美的地方,还是会出现问题,该来的终究会来。

步骤五与步骤四不同的地方在于,Aspects替换的是具体对象的方法。

[iOS] JSPatch 和 Aspects 兼容问题研究

经过调试,如上图所示,Aspects 获取 JSPatch 失败! 

经过查证,我们发现问题出在了class_replaceMethod这个方法上。

[iOS] JSPatch 和 Aspects 兼容问题研究

文档上表明,这个函数的返回值是一个类已经定义了的方法的实现,它并不会去找super class。

然而在当前这个场景下,class_replaceMethod的第一个参数klass是 Aspects 动态创建的一个ViewController的子类,所以根本无法替换成功。假如你现在在调试器中查看klass,会输出ViewController_Aspects_,与定义forwardInvocation的类(ViewController)并不相符。

解决方案

到目前为止,原因已经全部查明,接下来看看有什么办法来解决步骤五遇到的问题。

我们首先任务,就是要解决获取 JSPatch 原有实现为 nil 的问题,既然class_replaceMethod不会上诉 super class 去获取实现,我们可以自行打上补丁:使用另一个获取实现的方法class_getInstanceMethod(可以获取父类实现)。

修改代码

[iOS] JSPatch 和 Aspects 兼容问题研究
这样修改完毕,JSPatch就会找到正确的实现了。

[iOS] JSPatch 和 Aspects 兼容问题研究

不过当再次运行的时候,assert还是会跳到,警报还没有完全解除。respondsToSelector 仍不能认识AspectsForwardInvocationSelectorName这个方法。

经过查证,我们发现 respondsToSelector 实质会去调用 [obj class] 从而来获取对象所对应的类,但这种情况下我们获取到的是ViewController(并不是被 Aspects 修改过的子类 ViewController_Aspects_),难怪找不到了!

修复方案我们仍然使用class_getInstanceMethod

运行...

冲突解决了!

总结

JSPatch 和 Aspects 冲突问题和解决方案调查完毕,如果你有遇到和我们一样的问题,可以考虑本文所列的修复方案。

以上是关于[iOS] JSPatch 和 Aspects 兼容问题研究的主要内容,如果未能解决你的问题,请参考以下文章

[iOS] JSPatch 和 Aspects 兼容问题研究

JSPatch - 基本使用和学习

ios开发不能不知的动态修复bug补丁第三方库JSPatch 使用学习:JSPatch导入和使用.js文件传输加解密

JSPatch更新:完善开发功能模块的能力

JSPatch

正确姿势介入JSPatch