iOS/flutter动态化杂谈

2,661 阅读6分钟

iOS/flutter动态化杂谈

为什么需要动态化?实际上运营需求倒是其次,更主要的是修复线上严重问题。

那么是否一定要动态化呢?倒也不一定,国外开发者似乎更倾向于通过更好的机制来避免问题产生,搞更完善的CR/CI/单元测试之类的,而国内开发者更倾向于用动态化手段进行兜底。

这种差异是客观存在的,可能跟思维方式和技术发展甚至市场环境都有一定关系。这里我们不去深究,作为一个国内的iOS开发者,动态化是不得不关注技术问题。


iOS平台在面对动态化技术时是特殊的。因为它是唯一一个从系统层面禁止你下发执行动态库的平台,它只给安装包中带的可执行文件以执行权限,这在APP层面是无法绕过的限制。

因此其他平台只需要替换相关可执行文件即可。如果做了插件化就更容易了。

所幸,Objc是个比较动态的语言,虽然没有js等脚本语言那么动态,但是它仍然可以:

  • 运行时构造类和方法

  • 运行时替换方法的实现

这意味着我们可以做很多很多事情,离任意修改程序、执行任意逻辑已经很接近了,只差:实现新方法,也就是具体逻辑的实现。

方法很多,最简单的就是搞个脚本语言的解释器,在新方法里调用这个解释器做逻辑。早期有个Wax的项目,用lua来搞。不过后来iOS自带了JSCore,并且js语言受众也比lua多很多,JSPatch逐渐替代了Wax。

在这种JS动态化的基础上,衍生出了更多的方案。一类是RN/Weex,逻辑全用JS,渲染用Native组件来做;一类是小程序,逻辑也是用JS,渲染目前是Web+有限的Native组件,但要注意的是小程序的开发规范是不限制底层渲染的具体实现的,以后完全可以无感知地切换到别的方式进行渲染(某些平台已经在尝试了)。

这类基于JS的动态化方案有一些缺点:

  1. JS跟native相互调用的代价是比较大的。因此RN/Weex在某些场景下有无法避免的性能瓶颈。

  2. 使用JSPatch时,由于JS和Objc的语义是天然不完全一致的,一些类型互转、内存管理也可能需要注意。

  3. Native开发者不一定熟悉JS

由此衍生出两套方案,一个是滴滴的,让开发者写Objc代码,通过工具编译成JS,再做下发,通过这种方式来减少业务开发者的成本;另一个是腾讯手机QQ的,通过实现了一个Objc的解释器来解决这个问题。

手机QQ团队这套激进的动态化方案名为OCS,参考OCS ——史上最疯狂的 iOS 动态化方案

我们前面提到,Objc本身比较动态,但是纯逻辑的执行还是需要一个载体的,前面讲的是通过脚本语言来做,而OCS则是自己搞了一个虚拟机来做。当然这个虚拟机不需要实现完整的解释执行Objc的能力,解析代码并生成AST的过程可以离线做,也就是写好Patch代码直接通过基于clang的工具编个AST出来打包进行下发;实际执行中Objc相关的部分可以直接调用Runtime实现,这样的话VM真正需要实现的主要就是些简单的C语法。实际看起来效果,还行。因为避免了跨语言开销,性能比起JSPatch有比较大的优势。

如果只是热补丁的话,这样的性能优势意义并不大,因此OCS的主要出发点可能是安装包瘦身,通过它实现部分功能,对性能的要求就比较苛刻了。

这个方案可以对照OCEval-动态执行ObjectiveC的热修复方案这个项目看看,思路类似不过OCEval看起来是把语法分析部分也放在VM里做的,目前好像还不是非常完善,但有非常好的参考价值。


总的来说,Objc由于语言本身的动态性,虽然iOS官方不允许动态执行代码,但还是可以做很多动态化的事情的。而Swift就不一样了,由于Swift是静态语言(这里暂不考虑Swift使用Objc runtime的情况,毕竟不太可能对整个项目做这样的限制),前面这些思路在Swift上几乎无法使用。即使我们搞了一个完善的Swift的解释器出来,也很难桥接原有代码。

我是不知道Swift项目有什么好的方案了。老老实实完善CR/CI流程,提高代码质量吧。


再说一下Flutter的动态化,同样,Dart作为静态语言,也是不太好做动态化的。

在其它平台可以直接替换产物的,算个Diff打个Patch就好,思路还是比较简单的,iOS是行不通的,毕竟Dart的AOT产物是可执行的机器码。

闲鱼给出了一种模板解析的方案,这个基本上就是实现一下动态的UI下发,也就能支持一下运营活动,对Hotfix是无能为力的。

腾讯看点团队搞了个MXFlutter方案,基本思路是用JS写UI层逻辑,JS的逻辑反正是可以动态替换的。先上结论,这就是个玩具,几乎没有落地的意义。想想这里的调用路径,Dart - Native - JS的两层跨语言调用,整个UI层都这么搞,性能会很差;而且Widget tree由JS侧生成,映射成JSON传给Dart再解析回Widget Tree,这个性能消耗也蛮大的,而声明式UI意味着这个build过程必然是频繁调用的,看点给的解决方案...哈哈哈原谅我不厚道地笑了。

不过就算这些思路都不行,我们还有最后的办法:在Release时引入完整的DartVM。差不多就是发布Flutter的Debug产物,这样Engine大约要40M安装包(含Dart VM),而Dart部分可以动态下发。缺点一方面是占安装包比较大,另一方面是Dart的JIT性能可能不如AOT,尤其是刚启动的阶段。不过在网上找了个benchmark,看起来差得不多甚至很多方面JIT更好,参考Dart vs Dart aot

DartVM方案可能是Flutter动态化的终极解决方案。


多BB两句语言性能的问题,虽然理论上c这类直接编译到机器码的语言性能是真正的天花板,但实际上,基于VM的语言,由于可以得到一些运行时信息,并做一些运行时编译,实际性能并不会差很多,某些场景反而更容易优化。

比如Dart的这个issue:AOT code is 65% slower than JIT on dart_style benchmark,就是正则表达式的JIT优化更好。