返回介绍

9.8 竞品技术六瞥:热修补

发布于 2024-08-17 23:46:11 字数 6200 浏览 0 评论 0 收藏 0

9.8.1 Native页面和HTML5页面的相互切换

Native页面和HTML5页面的相互切换是最激动人心的技术,比我一直在研究的App插件化技术还要震撼。因为插件化技术只能适用于Android,对iOS无能为力。即使如此,搞Android插件化技术需要投入大量的人力物力,如果团队不够大是不建议搞插件化编程的。记得两年前我去一家公司面试,他们当时就在搞App插件化,面试时问我这方面的东西,被我当场泼了一头冷水,然后就没有然后了。

我们知道,Android插件化更多是为了解决线上严重的崩溃或者bug,有时也可以紧急上线一个新功能,而不用等到新版本发布。但问题恰恰出在这里,真正需要紧急修复的是iOS,因为每次审核都要1~2周的时间,而Android可以随时发版到国内各大市场。我们不能做亏本的买卖,费了巨大人力结果发现并没有解决主要矛盾。

于是我们会选择HTML5,如果发现App出事了,就把那个模块临时切换到HTML5网站。但注意,我们通常是把整个模块切换为HTML5站点,这个模块再也不会有Native页面了。这种做法有些得不偿失。于是我开始思考,能否只修改有问题的那个页面,将其临时换成HTML5,而这个模块的其他页面仍然使用Native的?

我仔细研究了一个页面——无论是Android还是iOS,所必备的几个要素,列举如下:

首先是入口和出口,把入口和出口控制住了,尤其是传进来的参数和传出去的参数,我们就能做到随时在Native和HTML5之间切换。我们不能再随意的在A页面中实例化B页面了,我们应该使用9.7.1节介绍的页面跳转器,来解耦各个页面之间的依赖,才能把任何Native页面切换为HTML5。

注意,直接使用9.7.1节的Navigator是有问题的。我们在BaseActivity和BaseViewController中定义的字典,用来在页面间传递参数。但是HTML5可不认这一套机制。所以有必要定义一套新的协议,同时适用于Android、iOS和HTML5,pagenamek1=v1&k2=v2是一种比较合适的协议。比如说,从HTML5跳转到Android或iOS页面,协议如下所示,其中单引号中的内容是协议,由3部分组成,Android页面名称,iOS页面名称,参数键值对,分别用逗号和分号分隔开。

<a onclick="baobao.gotoAnyWhere(
  'com.example.youngheart.MovieDetailActivity,
  iOS.MovieDetailViewController:movieId=(int)123')">
  gotoAnyWhere</a>

其次是状态,这其中包括全局变量、本地存储。一个Native页面通常要读写全局变量和本地存储,如果切换成HTML5页面,就不能干这些事情了,因此,我们要提供Native和HTML5之间的交互方法,以便于HTML5页面能读写Native中的全局变量和本地存储。

最后是公共组件,比如说网络请求和打点统计。这些要在Native中封装成公用方法,以便于HTML5回调这些方法。

如果把以上三点都做到了,就可以随时更换线上的某个页面了,我们只要在App启动的时候调用一个MobileAPI接口,获取一份页面清单,指定哪些页面是Native的哪些页面是HTML5的即可。

9.8.2 在iOS中使用脚本编程

1.寻找快速修复App线上bug的办法

我们前面提到了在App中使用HTML5,这其实就是脚本编程的一种,只不过要在WebView中展现。

我见过有些App通过返回XML格式或者JSON格式的数据,通知App绘制UI。这其实也是一门脚本语言,但这么做只能把UI绘制出来,并不能动态返回一个Native的方法,比如,点击按钮该做些什么事情。

我接下来要介绍的脚本编程,是指在iOS使用Lua或JavaScript这样的脚本语言。对于应用类App而言,也确实需要脚本语言介入了,尤其是那些对转化率要求很高的电商App,线上一旦有致命的bug或者Crash,可以迅速用脚本语言改好。这就好比身体受伤了,帖一个创可贴,等伤口愈合了(下次发新版本),再把创可贴摘掉。

在手机游戏领域,已经广泛采用Lua进行编程了。这样的好处是,每天都能通过Lua修改代码,增加个新的地图或者道具,然后通过MobileAPI把Lua脚本返回给App,达到新功能迅速上线的效果,而不用受发版上线的制约。接下来我们看iOS中是如何植入Lua或JavaScript脚本的。

2.在iOS中使用脚本语言的八卦史

首先隆重介绍Wax这个第三方开源库。Wax是使用Lua脚本语言来编写iOS原生应用的一个框架,它建立了iOS原生Objective-C语言和Lua脚本语言之间的映射关系。

但是发明Wax的这哥们从2013年开始就不维护这个框架了,导致了Wax中的很多遗留问题没有得到解决,比如说不支持自定义的结构体和结构体指针,不支持多线程等等。

后来,2013年年底,屠毅敏在Wax的基础上开发出WaxPatch,这也是GitHub上的一个开源项目,它的神奇之处就在于,在App启动时会加载服务器上的zip包,zip包中是用Lua脚本编写的补丁,在App运行期间,这些补丁文件中的方法能替换iOS中的任何一个类的任何一个方法的实现。它的实现原理是重写了运行时的class_replaceMethod方法。 [1]

就在我们庆幸iOS找到了快速修复线上bug的解决方案,再不用因为线上有bug而要忍受老板能杀死你的眼神时,苹果在2015年2月强制要求所有新提交的应用必须兼容64位,但原来使用Lua的框架Wax是不支持64位的。

人生不如意事,十有八九。

于是又等了几个月,开源社区给出了Wax的64位版本,在此基础上,我们把WaxPatch的改动也移植过去,就有了WaxPatch的64位版本。 [2]

2015年5月,JSPatch面世。它的原理和WaxPatch一样,都是在App运行期间替换iOS中的任何一个类的任何一个方法的实现,只是它是基于JavaScript来实现的。估计是JSPatch的作者等不及Wax和WaxPatch迟迟不更新所以才另起炉灶了吧。与此同时,JSPatch的作者还提供了大量的实例来帮助我们理解这个开源项目。 [3]

Wax和WaxPatch毕竟很久不维护了,它不支持iOS的多线程语法以及自定义结构体和结构体指针,而JSPatch是支持这些iOS特性的,所以建议大家使用JSPatch。本书即将出版的时候,JSPatch已经比较成熟了,而且还在持续更新,优化因反射而带来的性能问题。让我们拭目以待。

本书不打算过多介绍如何把Objective-C代码转换为Lua或者JavaScript,官方文档已经讲得很清楚了。下面我将以WaxPatch为例,介绍一下它的使用策略。JSPatch的使用思路也是一样的。

3.Zip包下载策略

接下来介绍WaxPatch中压缩包的下载规则。压缩包中的内容就是用于热修补的Lua脚本。

首先返回Lua下载地址的MobileAPI接口,要区分App的版本。比如当前版本有一个严重的bug,为了修复它引入了lua001.zip,而我们在下一个版本修复了这个bug,就不需要lua001.zip包,或者说等下个版本上线后又发现了新的bug,这时候要引入lua002.zip。所以这个MobileAPI接口应该根据版本号返回不同的Lua压缩包下载地址。

如何控制App不重复下载相同的Lua压缩包呢?每次调用MobileAPI接口获取到Lua压缩包的地址,比如说lua001.zip,我们在解压lua001.zip这个压缩包到本地lua001这个目录下的同时会把lua001这个值存到本地文件的变量luaVer中。下次再调用MobileAPI接口,就会根据返回的Lua压缩包的地址进行判断:

如果值为空,说明不需要Lua脚本来修复bug,那么就把luaVer设置为空。

如果值仍然是lua001.zip没有变化,就什么都不做。

如果值是一个新的Lua压缩包的地址,比如lua002.zip,那么就下载这个压缩包,将其解压到lua002这个新的目录,并把luaVer这个值设置为lua002。

按照上述策略,我们就可以根据luaVer的值,来控制App能加载到最新的lua压缩包,而且避免重复下载。

4.调试策略

我们的策略是依赖MobileAPI返回的Lua压缩包的下载地址,但是不可能每次开发调试时,都把一个用于测试Lua压缩包发布到服务器上,因为我们在调试期间会频繁地修改Lua压缩包中的文件。

基于此,在调试期间,我们绕开从服务器下载Lua压缩包并比较版本的做法,改为把Lua压缩包中的文件直接复制到本地目录的方式,比如,lua001.zip包中有2个Lua文件,我们把这两个文件集成到App项目中,在App每次启动的时候,就把这两个Lua文件复制到本地,然后就可以直接使用了。

在全部调试完成,就把代码切回到仍然从服务器下载Lua压缩包的模式。

5.Lua不支持的场景及解决方案

并不是所有的iOS代码都能转换为Lua脚本。以下是我遇到的情况以及相应的解决方案。

1)如果变量或属性声明错了呢?

我们知道WaxPacth编程的思想是在iOS运行时注入,动态修改任何一个类的任何一个方法的实现。也就是说任何一个方法体都可以替换为Lua脚本,但就是不能修改方法的签名。但这还好,遇到这种情况,我们在Lua中重写一个方法,简单地包装一下Objective-C中不符合我们要去的方法即可。

但是如果是一个属性或类级别的变量的类型声明错了,我们就真的没办法了。仔细检查WaxPacth这个框架,还真没有定义一个属性或变量的地方。遇到这种情况,我们的解决方案是,在项目中增加一个LuaClass类,里面只有一个字典属性dicLuaObject。

在Lua脚本中,我们把错误类型的属性或者变量所出现的任何地方替换为正确类型的变量,而这个变量则定义在LuaClass类的dicLuaObject字典属性中。

2)对于block块该如何处理呢?

Lua-Wax不支持block块。因此一旦block块内的代码有问题,就要重写这个block块所在的方法,同时将block块中的代码封装成另成一个方法,也在Lua脚本中重写。

6.如果zip包被劫持了呢?

不要以为MobileAPI返回了Lua压缩包下载的地址,就可以直接下载并使用了。经常有恶意攻击者劫持了服务器返回给我们的下载地址,而让我们去下载一个恶意的压缩包。我们一旦下载并解压缩这个恶意的包,接下来可能发生各种意想不到的事情。

为此,我们不能认为网上下载的任何压缩包都是安全的。我们需要一套校验机制,来保证这个下载到的压缩包是我们自己提供的,如果验证不过,就删除或者隔离这个文件。

SSH是最简单的解决方案,但就是HTTPS协议访问起来太慢了,能否做成HTTP的呢?可以,我们需要准备一对公钥和私钥:把zip包使用私钥进行签名后再放到服务器提供下载:而App下载这个zip包到本地,则使用保存在App中的公钥进行校验。我们要对私钥进行严格的保密,不能泄漏给他人,这样即使有人在App中取到了公钥,因为没有配套的私钥,也没办法生成一个符合我们要取的zip包。

7.Lua对iOS的深远影响

有了Lua这个利器,线上的任何bug或者Crash都能以最快的速度修复,而不需要重新提交审核新的版本并等待超长的时间。比如,我们最苦恼的是页面打点经常发现打错了或者漏打了,为了能不影响数据的采集,使用Lua能及时缝补这个漏洞。

最后需要补充的是,虽然Lua语言很简单,尤其是WaxPacth这个框架的支持,使得我们可以改写任何方法都很容易。但是我经常看到的是很多Objective-C方法都有成百上千行代码,这就给改写带来了很大的工作量。这就又回到了编码规范的层面,尽量把方法写的短小。每个方法只做一件事情。

提示  在Android中使用Lua

iOS因为有了WaxPatch而重新焕发了活力,而Android在Lua方向的进展却不温不火。

Android因为可以使用插件化编程,而且即使线上有了严重的bug,到各大市场发一次新版本就解决了,所以,相比iOS,Android有更多的选择。

其实Android也可以使用Lua脚本语言编程,业界比较公认的技术是AndroLua这个开源项目。我对AndroLua的研究还在进行中。也请越来越多的人关注这个项目。

本书临近出版的时候,听说淘宝有个团队推出一个名为Dexposed的开源项目,它是基于AOP思想来设计的,能解决性能监控、在线热修复等问题。这个开源项目还很年轻,但是我非常看好它。

[1] WaxPatch的源码地址:https://github.com/mmin18/WaxPatch

[2] WaxPatch的64位版本,参见https://github.com/felipejfc/n-wax

[3] JSPatch的下载地址,参见https://github.com/bang590/JSPatch

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文