一、引言
之前的文章客户端Hybrid架构设计之——WebView方案的实现,已经介绍了如何用WebView来实现Hybrid架构。当时提到了这种方案的几个缺陷:
- H5页面的部分用户体验不如native
- H5也需适配各平台机型
- 原生端基础框架搭建费时费力,有时一些特殊功能需要ios、Android、H5三方联合开发,增加了沟通协调成本
所以在这套架构基本开发完善之后,项目组马不停蹄的开始研究相对更先进、更优的解决方案——ReactNative/Weex。他们相比WebView方案的区别就在于,js代码最终会被翻译成原生代码,呈现在用户面前的也是原生组件,所以性能、体验也是无限趋近于原生级(之所以只能是趋近,因为还是有js和原生通信所产生的性能损耗)。
经过近半年的调研、学习、实战,项目最新版本的主要页面已经全部由ReactNative来实现,既然有了些成果,这次就来跟大家做个简单的分享。本文为了方便,主要还是从Android的角度出发,iOS的思路也是一样的。
二、选择RN还是Weex
结果很明显,我们选择了RN,但不得不说,从一开始,烫爷一直是更倾向于Weex的,因为当时觉得Weex相比RN有以下几点优势:
- Weex开发者自身就是RN早期使用者,他们遇到的很多RN问题,都在Weex中设法解决了
- Weex号称“一次编写,多端运行”,比RN的“Learn Once Write Anywhere”强不少
- Weex对新人非常友好,上手简单,这一点能得到很高的印象分
- Weex使用的Vue相比于ReactNative使用的React,更加轻量级,对于无前端经验的客户端开发人员来说更友好
- Weex是中国人自己的框架,支持国货,人人有责。
简单说,Weex更新更性感更中国。那么大家要问了,Weex那么棒,最后怎么还是选了RN?理由也不复杂:
- RN比Weex成熟太多了,业界已经有不少成功案例,而Weex在当时只有几个小demo,如果项目开发中遇到框架本身绕不过去的坑,很容易两眼抓瞎
- RN虽然不标榜“一次编写,多端运行”,但是大多数的组件,包括社区提供的第三方,还是可以在iOS、Android两个平台通用,不能通用的也可以通过js中间层去屏蔽对业务代码的影响,毕竟这些都是一劳永逸的活
- RN的文档小烂,但是Weex几乎没有文档
- Weex是中国人自己的框架,还他喵的是阿里的框架(大雾)
技术选型,可能还是要稳妥一点,不能一味追求新潮,毕竟支撑业务才是第一位的。
三、ReactNative实现核心模块简介
RN方案其实和WebView方案非常相似,实现起来也并没有更麻烦,主要还是以下几个模块
- RN框架
RN框架本身已经是一个相当成熟的框架了,前期的配置看一些官方文档已经足够,可以较方便的把RN项目跑起来。 - 路由模块
其实一个纯RN应用只要一个Activity就足够了,但大多数项目都有历史包袱,比如烫爷的项目,又有原生页面,又有H5页面,现在又多了RN页面,怎么在这些页面之间自由跳转,就得依赖路由模块了,这个模块的介绍同样可以参考之前的文章客户端Hybrid架构设计之——WebView方案的实现。 - 更新模块
RN的一大好处是可以不用发客户端版本,动态更新,所以更新模块尤为关键。 - 统计模块
由于市面上大多数统计sdk不会统计RN数据,所以我们很有必要自己实现相应的统计模块,不然我们对RN页面性能到底如何,完全不能做到门清,那后期优化就会比较的茫然和无的放矢。
同时还有一些需要额外学习的前端知识
- ES6语法和React
这是基础中的基础,写RN代码必须的,建议可以看看阮一峰相关的教程。 - Flexbox布局
在ReactNative中我们使用flexbox规则来指定某个组件的子元素的布局,这个不难,看看官方文档就足够了。 - Redux、saga等配合React使用的框架
这次折腾,一大感慨就是前端社区确实比客户端社区热闹好多,各种各样各种功能的前端框架层出不穷。不过烫爷建议,一开始只要使用React已经足够了,只有当你的应用足够复杂以后,才有必要去使用Redux等第三方框架,一开始千万不要贪多嚼不烂,那样很容易由于学习曲线过于陡峭,而打击积极性。
四、原生改造
-
Bundle路径修改
RN会把所有的RN代码打包成一个Bundle,框架读取这个Bundle就可以加载相应的RN页面了。框架默认是读取预置在本地比如Assets里的bundle文件,如果要修改路径,就需要修改相关的RN代码。拿Android举例,就需要写个类继承ReactNativeHost,并修改getJSBundleFile()方法,并且为了能应用这个自定义的Host,还需要修改或者覆盖对应的一系列文件,如ReactActivityDelegate、ReactActivity等。
-
Activity和原生模块关联
有些RN无法完成的功能,需要靠自定义原生模块来实现,但是由于RN框架没考虑多Activity的情况,那样在原生和RN之间的交互就可能引发混乱。举个例子,RN页面需要通过接口请求结果来通过原生模块设置title,但是在结果返回之前,我们可能已经跳到一个新Activity了,这个时候就不应该由新的Activity去设置title,而应该找到之前的Activity去做这件事。所以我们要做的就是把Activity和RN页面,以及在这个页面上发出的原生模块方法做一个绑定。
具体的做法是每个RNActivity都使用自己实例的hashcode作为自己的id,在创建RN页面时把这个id传给RN,而RN页面在调用原生模块方法时,则把这个id作为一个参数再传回来。同时我们维护一个RNActivityPool,用RNActivity的id作key,来保存RNActivity,这个时候在调用原生模块方法时,就可以通过id来找到需要对这个方法负责的RNActivity了。
-
Launch页面和预加载
无论是RN框架的启动还是RNBundle的加载、RN页面的实际渲染,都需要一定时间,而当这些因素叠加在一起,也即启动第一个RN页面时,白屏时间会长的有点让人难以接受(5s左右甚至更长)。解决的办法是Launch页面完全用原生做,并且在Launch页面预加载bundle。一般我们的Launch页面都会做一些别的初始化工作,或者广告加载等,这些时间足够完成RN的预加载了,这样一来打开第一个RN页面就和后面打开其他RN页面没有明显的差距了。预加载的方法也有现成的
ReactInstanceManager reactInstanceManager =
((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager();
if (!reactInstanceManager.hasStartedCreatingInitialContext()) { // 这句必须加,不然容易重复create,造成RN抛出异常
reactInstanceManager.createReactContextInBackground();
}
五、更新模块
首先,当然可以使用Microsoft/code-push这类第三方更新组件,但是为了更好的把控这一核心功能,包括未来对多Bundle的处理优化等,有余力的同学,最好还是自己实现更新模块。
1、差分更新技术
更新模块中的一个核心技术点就是差分更新技术,差分更新技术主要依赖文件的开源二进制比较工具bsdiff。我们用apk更新的例子来简单介绍下差分更新技术:
自从 Android 4.1 开始,Google Play 引入了应用程序的增量更新功能,App使用该升级方式,可节省约2/3的流量。现在国内主流的应用市场也都支持应用的增量更新了。
增量更新的原理,就是将手机上已安装apk与服务器端最新apk进行二进制对比,得到差分包,用户更新程序时,只需要下载差分包,并在本地使用差分包与已安装apk,合成新版apk。
例如,当前手机中已安装微博V1,大小为12.8MB,现在微博发布了最新版V2,大小为15.4MB,我们对两个版本的apk文件差分比对之后,发现差异只有3M,那么用户就只需要要下载一个3M的差分包,使用旧版apk与这个差分包,合成得到一个新版本apk,提醒用户安装即可,不需要整包下载15.4M的微博V2版apk。
以上内容摘自 cundong/SmartAppUpdates
如上所述,差分更新技术可以有效减小更新包的大小,让更新更有效率。
2、具体流程
服务器端
每次RN代码打包时,都自动生成当前最新版本的全量包,以及新版本和最近5个旧版本的差分包,保存在服务器端。客户端发起更新检查接口时上报自己当前的RN版本号,如果版本差小于等于5,服务器端就同时下发差分包和全量包的下载地址,反之如果版本差大于5,则只下发全量包地址。-
客户端
客户端每次启动时同步做两件事:
a. 发起更新检查接口,询问服务端是否需要更新,如需要,是差分更新还是全量更新。更新时,优先进行差分更新,具体步骤为下载差分包——与旧包合成——验证合成包的MD5值是否与服务器先前下发的一致,这些都完成后就可以把合成包保存好,等待下次重启时应用了。这一过程中如果出现任何问题,则放弃差分更新,去下载全量包进行全量更新。b.应用当前本地保存的最新的RN包,并删除旧包。如果既没有新包,也没有旧包,则从本地Assets中拉取打包时预置的包。
注意,之所以下载完新包不马上应用,而是要等到下次重启时才生效,主要是担心在用户使用过程中突然重新加载RN页面有可能改变页面UI甚至是业务流程,这会导致用户体验不佳或者操作失败。
六、统计模块
高级程序员应该树立一个意识,开发并不只是开发完功能就结束了,事实上功能开发可能只占了整体工作量的一半,设计、测试、统计、监控等则占了另外一半。你开发的系统不应该仅仅满足于跑起来,还应该有一套成熟的统计系统去分析系统运转状态,有一套监控系统在出现故障时能自动采取措施或至少发出预警。由于RN是一个新框架,无论是框架本身,还是我们的应用技巧都还不够成熟,所以非常有必要去对RN的性能、错误等进行统计,并以此作为未来优化的根据。
-
性能统计
性能统计主要是统计RN页面的白屏时间,具体方法是把一个页面创建时各个生命周期的时间戳记录下来,再统一上传到服务器做分析。生命周期从前往后依次是onCreate(原生)、componentWillMount、componentDidMount、componentWillUpdate、componentDidUpdate可以简单的认为
RN初始化时间 = componentWillMount - onCreate
静态UI加载时间 = componentDidMount - onCreate
服务器数据返回页面刷新时间 = componentDidUpdate - onCreate(ps:因为只要改变state就会触发componentWillUpdate和componentDidUpdate,所以需要做个标志位,只记录关键接口第一次返回后触发的componentWillUpdate和componentDidUpdate)
因为绝大多数页面都需要调用服务器接口获取数据,之后再刷新页面,一般也只有在此时用户才能看到有意义的信息,所以我们更关注第三个指标,也可以直接把第三个时间当做是广义的白屏时间。
以下是烫爷的项目收集到的数据,基本是未经过什么特别优化的RN表现,可以看出白屏时间基本在1s以内,明显还是比Hybrid的表现好不少的,而且比较让人意外的是,Android平台的性能并没有比iOS差多少,这一点比WebView方案强上很多。
埋点统计
RN的埋点比较简单的方法是把RN自带的几个可点击组件再封装一遍,包括TouchableHighlight、TouchableNativeFeedback、TouchableOpacity、TouchableWithoutFeedback,在其中添加统计功能,这样开发业务时只要用封装好的组件,再添加对应的统计id就可以了。
七、其他
-
屏幕适配
RN框架默认的长度单位是dp,但是在某些场景用dp并不能很好的适配所有屏幕大小,比如有些屏幕宽320dp,有些宽360dp,这就给我们适配造成了麻烦。这里推荐一个三方库vitalets/react-native-extended-stylesheet,这个库有不少功能,其中一个就是可以适配iOS和Android的不同屏幕。比如我们可以在框架中设定屏幕宽度为750px,设计图全部按750px宽来出,之后开发设置具体组件的长宽时,就可以完全按照设计图上的尺寸来,随后框架会根据设备的长宽去做等比缩放以此达到适配效果,原理和安卓原生的UI适配框架hongyangAndroid/AndroidAutoLayout很像。
其实关于ReactNative的内容还有很多,这篇文章就先总结到这里,相信把上面提到的那些做好,再结合下官网文档,已经足够做出一个靠谱的商业级app了。等未来烫爷研究的再深入些,再找些别的干货来给大家作汇报,我们到时再会!