[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分以下几个步骤。
JSPatch 修改 ViewController 的 viewWillAppear 的方法,在方法里修改背景色为
黑色
屏蔽 JSPatch,Aspects单独修改的 viewWillAppear 的方法,在方法里修改背景色为
黄色
JSPatch 和 Aspects,都替换 viewWillAppear 的方法,颜色仍修改为各自颜色
JSPatch 和 Aspects,替换不同方法
JSPatch 替换类的实例方法,Aspects 替换实例对象方法
步骤一(JSPatch修改背景色)
首先我们创建一个新的工程,Xcode -> File -> New -> Project -> Single view, 并在工程目录下创建 Podfile。
Podfile 内容如下, 将JSPatch和Aspects两个库引入进来
本地添加一个demo.js
的文件,js代码替换viewWillAppear
, 设置背景色为黑色
接下来Appdelegate
里加入
运行...
修改完成!
步骤二(Aspects修改背景色)
屏蔽JSPatch,将demo.js重命名为demo1.js
加入Aspects代码
步骤三(JSPatch和Aspects同时修改)
现在将demo1.js名字改回demo.js, 恢复JSPatch修改功效。
运行...
结果是Aspects修改生效了
步骤四(JSPatch和Aspects同时修改不同方法)
我们这时要有两种不同的表现,那用JSPatch来修改背景色,Aspects来弹一个alert。看看是否都生效了吧。Aspects的修改方法,改为另一个viewDidAppear
运行...
哎呀,没有冲突啊,都生效了,是不是搞错了?注意,我们用Aspects替换的是类的实例方法,也就是ViewController创建出来对象,都会弹一个alert出来。而我们项目中遇到情况是用Aspects修改了具体实例。所以进入最后一个步骤。
步骤五(JSPatch和Aspects同时修改不同方法, Aspects修改具体实例)
修改Aspects代码, self
代替ViewController
运行...
如果你加了 exception breakpoint 的话,就会重现项目中情况了。
Demo 总结
这里先做个小总结, JSPatch 和 Aspects 实际冲突的表现
JSPatch 与 Aspects 在修改同一个类的同一方法时, 是可行的,但只有一个生效,取决于先后顺序
步骤三演示中,我只验证了JSPatch在前的情况,有兴趣的同学可以试验一下反过来的情况
JSPatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个类的实例方法,而非具体实例,两者可以同时生效
JSPatch 与 Aspects 在修改同一个类的不同方法时,如果Aspects修改的是一个具体类的实例, Aspects assert,结果是Aspects生效
因此针对步骤五的情况是需要我们注意的。
现象研究
由表及里,先研究下我们最关心的步骤五。
forwardInvocation
冲突的原由是 JSPatch 和 Aspects 都使用了 forwardInvocation 来重写消息转发。
我们知道对oc对象的方法调用,是通过发送消息的方式进行的。任何调用到 runtime 层,都会被转化为objc_msgSend
族函数的调用。这些族函数会在对象所属类的方法列表
中搜索对应的实现,如果没找到就去父类的方法列表
中去寻找,如果找不到就走消息转发
流程。
如上图所示,对象在收到未定义的方法的时候,会首先走 +(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 了。工作原理如下图所示。
现象分析之步骤三
现在可以思考下步骤三,当替换了同一个方法时,方法首先被 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
肯定保留了原有的实现,找一下它是怎么来的。
很明显,Aspects 在替换forwardInvocation的时候,把原来的实现保存在AspectsForwardInvocationSelectorName
,这是一个新增方法。用图表示下。
现象分析之步骤五
看样子,Aspects处理还是很完美,但是往往是看上去完美的地方,还是会出现问题,该来的终究会来。
步骤五与步骤四不同的地方在于,Aspects替换的是具体对象的方法。
经过调试,如上图所示,Aspects 获取 JSPatch 失败!
经过查证,我们发现问题出在了class_replaceMethod
这个方法上。
文档上表明,这个函数的返回值是一个类已经定义了的方法的实现,它并不会去找super class。
然而在当前这个场景下,class_replaceMethod
的第一个参数klass
是 Aspects 动态创建的一个ViewController的子类,所以根本无法替换成功。假如你现在在调试器中查看klass,会输出ViewController_Aspects_
,与定义forwardInvocation
的类(ViewController
)并不相符。
解决方案
到目前为止,原因已经全部查明,接下来看看有什么办法来解决步骤五遇到的问题。
我们首先任务,就是要解决获取 JSPatch 原有实现为 nil 的问题,既然class_replaceMethod
不会上诉 super class 去获取实现,我们可以自行打上补丁:使用另一个获取实现的方法class_getInstanceMethod
(可以获取父类实现)。
修改代码
这样修改完毕,JSPatch就会找到正确的实现了。
不过当再次运行的时候,assert还是会跳到,警报还没有完全解除。respondsToSelector 仍不能认识AspectsForwardInvocationSelectorName
这个方法。
经过查证,我们发现 respondsToSelector 实质会去调用 [obj class] 从而来获取对象所对应的类,但这种情况下我们获取到的是ViewController
(并不是被 Aspects 修改过的子类 ViewController_Aspects_
),难怪找不到了!
修复方案我们仍然使用class_getInstanceMethod
运行...
冲突解决了!
总结
JSPatch 和 Aspects 冲突问题和解决方案调查完毕,如果你有遇到和我们一样的问题,可以考虑本文所列的修复方案。
以上是关于[iOS] JSPatch 和 Aspects 兼容问题研究的主要内容,如果未能解决你的问题,请参考以下文章
[iOS] JSPatch 和 Aspects 兼容问题研究