Android插件化更多是为了解决线上严重的崩溃或者bug,有时也可以紧急上线一个新功能,而不用等到新版本发布。但问题恰恰出在这里,真正需要紧急修复的是iOS,因为每次审核都要1~2周的时间,而Android可以随时发版到国内各大市场。我们不能做亏本的买卖,费了巨大人力结果发现并没有解决主要矛盾。
于是我们会选择HTML5,如果发现App出事了,就把那个模块临时切换到HTML5网站。但注意,我们通常是把整个模块切换为HTML5站点,这个模块再也不会有Native页面了。这种做法有些得不偿失。于是我开始思考,能否只修改有问题的那个页面,将其临时换成HTML5,而这个模块的其他页面仍然使用Native的?
Android和IOS页面所必备的要素:
首先是入口和出口,把入口和出口控制住了,尤其是传进来的参数和传出去的参数,我们就能做到随时在Native和HTML5之间切换。我们不能再随意的在A页面中实例化B页面了,我们应该使用9.7.1节介绍的页面跳转器,来解耦各个页面之间的依赖,才能把任何Native页面切换为HTML5。
我们在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的即可。
1.寻找快速修复App线上bug的办法
在手机游戏领域,已经广泛采用Lua进行编程了。这样的好处是,每天都能通过Lua修改代码,增加个新的地图或者道具,然后通过MobileAPI把Lua脚本返回给App,达到新功能迅速上线的效果,而不用受发版上线的制约。接下来我们看iOS中是如何植入Lua或JavaScript脚本的。
2.在iOS中使用脚本语言的八卦史
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不支持的场景及解决方案
1、 如果变量或属性声明错了呢?
我们知道WaxPacth编程的思想是在iOS运行时注入,动态修改任何一个类的任何一个方法的实现。也就是说任何一个方法体都可以替换为Lua脚本,但就是不能修改方法的签名。但这还好,遇到这种情况,我们在Lua中重写一个方法,简单地包装一下Objective-C中不符合我们要去的方法即可。
但是如果是一个属性或类级别的变量的类型声明错了,我们就真的没办法了。仔细检查WaxPacth这个框架,还真没有定义一个属性或变量的地方。遇到这种情况,我们的解决方案是,在项目中增加一个LuaClass类,里面只有在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方法都有成百上千行代码,这就给改写带来了很大的工作量。这就又回到了编码规范的层面,尽量把方法写的短小。每个方法只做一件事情。
iOS因为有了WaxPatch而重新焕发了活力,而Android在Lua方向的进展却不温不火。
Android因为可以使用插件化编程,而且即使线上有了严重的bug,到各大市场发一次新版本就解决了,所以,相比iOS,Android有更多的选择。