App竞品技术分析 (6)热修复

13 篇文章 0 订阅
13 篇文章 0 订阅
1 Native页面和Html5页面的相互切换

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

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

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

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

  首先是入口和出口,也就是复旦正门前的保安老哥每天都要问的那几条哲学命题,“你从哪里来,要到哪里去”。把入口和出口控制住了,尤其是传进来的参数和传出去的参数,我们就能做到随时在Native和Html5之间切换。我们不能再随意的在A页面中实例化B页面了,我们应该使用7.1介绍的页面跳转器,来解耦各个页面之间的依赖,才能把任何Native页面切换为Html5。

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

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

  图9-14从Html5跳转到Android或iOS页面的协议。

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

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

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

2 在iOS中使用脚本编程
2.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.2 在iOS中使用脚本语言的八卦史

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

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

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

  就在我们庆幸iOS找到了快速修复线上bug的解决方案,再不用因为线上有bug而要忍受老板能杀死你的眼神时,苹果在15年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的使用思路也是一样的。

2.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压缩包,而且避免重复下载。

2.4 调试策略

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

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

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

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

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

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

  我们知道WaxPatch编程的思想是在iOS运行时注入,动态修改任何一个类的任何一个方法的实现。

  也就是说任何一个方法体都可以替换为Lua脚本,但就是不能修改方法的签名。但这还好,遇到这种情况,我们在Lua中重写一个方法,简单的包装一下Objective-c中不符合我们要去的方法即可。

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

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

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

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

2.6 如果zip包被劫持了呢?

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

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

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

2.7 Lua对iOS的深远影响

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

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


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

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

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

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值