良好解决多终端开发问题是提升团队开发效率的有效方法,本文全面解析了京东 JDReact 三端融合平台,一起来看看京东的解决方案吧!
React Native 最近两三年之内整个框架在业界应该说是非常热门,很多团队、大公司都在做 RN 的一些研究开发工作。先一起回想下在 React Native 框架出现之前,互联网 APP 开发是一种什么样的模式。最初,大多数同学应该是用原生开发 Android 或者 iOS,再加上 HTML5 内嵌的方式,即 Web APP。之后又衍生出了 Hybrid APP,基于 PhoneGap/Cordova 框架实现了 WebView 的能力强化。不知道大家在做这种开发的时候,有没有遇到过一些瓶颈或者一些痛点,反正我们的团队是遇到了很多。这里总结一下之前传统的方式有哪些问题。
第一,效率低下。
因为无论是 Android 还是 iOS,使用传统的原生开发都有一定的开发门槛。而且代码上不能复用,这意味着任何一个业务要在 Android 和 iOS 各做一次开发,测试和业务开发工作都不能复用。
第二,性能比较差。
用传统的 H5 开发方式,受限于 WebView 容器的一些瓶颈,导致无论在页面加载还是用户体验上,相比原生应用有比较大的差距。
第三,灵活性不够。
因为传统原生开发意味着任何改动都需要发版,在 Android 上因为像国内应用商店非常多,而且涉及到各种不同的渠道包,所以发版成本很大;在 iOS 则受限于苹果的审核机制。对我们来讲,任何这种线上问题处理起来都非常痛苦。
最后,接入困难。
因为 Android 和 iOS 平台有差异,所以任何一种垂直业务接入 APP 的成本非常高,很多业务代码和业务流程并不能复用,造成业务团队的开发、接入成本非常高。
说了这么多痛点,我们也在反思到底需要一种什么样的框架来解决这些问题。非常幸运,我们在 2015 年的时候注意到 Facebook 发布了非常具有颠覆性的 RN 框架。简单来说,这是一种跨平台的移动应用开发框架。在当时它非常有颠覆性,因为它最大的特点就是完全用 JavaScript 进行应用的开发,但是最终会渲染成原生的组件。对开发者来说,这意味着你拥有了 Web 开发的效率,同时兼顾了原生的性能。这对我们当时业务的吸引力非常大,这个框架一经推出,国内外很多公司都在用,像 Facebook 自己也在用。在国内,手机百度、手机 QQ、京东 APP 也很早就进行了开发。RN 对我们团队来讲都有哪些优点,或者说为什么要用它,这里大概总结了以下四个原因。
第一,学习成本低。
因为它的开发基于 JavaScript,JS 语言本身在开发者当中有非常良好的群众基础,任何一个有经验的前端团队可以快速地上手 RN 开发。
第二,多端代码复用。
因为所有的业务都用 JavaScript 开发完之后只有一份代码,然后通过编译打包机制直接部署到不同的平台,如 Android、iOS 甚至 Windows 平台。
第三,接近原生的性能。
开发者使用 JavaScript 进行 RN 框架开发,开发完之后在再通过中间的虚拟 DOM。这个实际上是它核心所在,传统的 H5 的应用是跑在 Web View 的容器当中,容器中需要维护一个真实的 DOM,而真实的 DOM 上每一次操作都会有回流 (reflow) 和重绘 (repaint),效率并不高。Facebook 最有颠覆性的一点就是提出了一个虚拟 DOM 的概念,把整个 DOM 放在内存当中,然后通过高效的 diff 算法来计算比较哪些 UI 组件需要更新,最终只对这些需要更新的组件进行真实操作。经过测试,采用 RN 框架,无论是加载性能还是页面滑动性的用户体验上,都比原来 H5 的方式要好很多。
最后,社区活跃。
除了 Facebook 之外,GitHub 上有很多第三方的团队、个人、公司开发贡献了很多非常优秀的第三方组件,它的社区是非常健康、非常活跃的。
不过现实是残酷的,即便确定了用 RN 框架做业务开发,在实际的开发当中也发现了 RN 的一些不足。对我们的业务来讲,最不能接受的主要是以下四个方面。
第一点,RN 框架原生并不支持 Web 端。
这意味着如果一个业务需要同时上 Android、iOS 和 H5 页面的话,那除了用 RN 之外,还需要用传统的 H5 或用 ReactJS 框架再做一次开发,这样效率是非常低的。
第二点,RN 框架官方并不支持热更新。
虽然现在有很多第三方方案,比如微软的 CodePush,但是官方并不原生支持热更新,而热更新对我们的业务来说也是非常重要。
第三点,Facebook 给出的官方 RN API 不能完全满足业务快速的发展。
它只给了一些很基础的 API,但业务中经常会用到的一些多媒体,比如录音、录像、视频播放文件以及文件上传、压缩、加密等等,这些都没有提供。
最后
,前面提到 RN 框架性能非常不错,比 H5 好很多。实际上经过真正的业务开发后,发现 90% 的场景下 RN 的性能非常棒,可以满足我们的业务需求;但是在另外的
10% 的场景下,特别是一些交互非常复杂、页面非常复杂、需要频繁的更新、需要一些手势交互的场景,RN 仍有些内存跟性能的瓶颈。
既然 RN 有优点也有缺点,那怎么办?
我们的解决方案是
基于 RN 框架进行了深度定制和二次开发,逐步打造了符合京东业务的 JDReact 三端融合平台
,主要的工作是以下四大方面:
第一,把 RN 的核心 Base 库拿来做裁剪和二次开发
,把不需要的功能删减掉,把性能、兼容性、稳定性的问题修复,包括也支持了拆分打包。
第二,在后端搭建了一个功能支撑平台
,帮 RN 框架增加了灰度更新升级、数据监控以及降级容灾功能,这些对业务来说是非常重要的。
第三,基于整个 RN 框架,结合京东的一些业务特点,封装了一套自己的业务组件,包括 UI 公共组件库。
目的是为了让垂直业务开发者可以很快地使用框架进行业务开发,完全不用关心设计的样式跟交互,可以快速接入业务。
第四,打通 Web 端,实现了一套 RN 框架向 ReactJS 转换的工具。
可以做到一次代码编写,直接部署到 Android、iOS 跟 Web 三端。
下图所示就是整个 JDReact 三端融合平台的架构图。
最下面是一个后端接入平台,包含刚刚提到的灰度更新、降级容灾、数据采集和持续集成,这些是由服务端提供的一套服务。中间这一层是提供给内部开发者的一套完整的 SDK 开发工具,里面除了一些 API 之外,也封装了大量的京东定制功能组件,包括 UI 公共组件。其中还有一块是 Web 转换工具,提供了一套 RN 转换的脚本。业务开发者完全不需要关注这些细节,只要关心他自己的业务逻辑,就可以直接开发出覆盖三端的应用。最上面的业务层就是京东 APP 所有使用三端融合平台开发的业务,这些都可以直接部署到 Android、iOS 和 Web。
前面主要介绍了整体的平台架构,现在开始来分享一些干货,就是我们在开发过程当中团队遇到的 RN 的一些问题,包括如何改进跟优化的一些实践。我列了一些功能点跟大家一起分享。
有同学抱怨过 RN 库太大了,所以拿到 RN 的第一件事就是裁剪。对 Android 平台来讲,除了把 RN 的基础库裁剪以外,很重要一点就是要把方法数减少。因为 Android 平台 dex 有方法数限制,一旦超过 65K 就需要拆分成多个 dex,整个应用的安装跟加载都会有性能问题。所以,要对 Android 方法数进行严格控制,我们的做法就是根据业务情况,把一些用不到的组件方案中的功能组件删除。其中最重要改动就是把 Android 中 support-v7 和 stetho 库依赖给去掉,去掉之后不仅大小减小了很多,而且方法数减少了将近 7000。除了移除这个功能库,很重要一点,因为不是一个全新的 RN 应用,需要跟现有的体量很大的 APP 做集成整合,所以尽量让一些依赖库复用主站中依赖库,比如 fresco、okhttp 等。一来缩减包的大小,二来避免包的冲突。但是主站中的版本很可能跟 RN 中引用的版本有差异,需要中间做一层适配层,把这些差异尽量抹平,保证这些功能和方法都能工作。
虽然说 RN 框架号称比 H5 的加载性能快很多,但实际开发中发现在 Android 的一些低端机型上,加载速度还是达不到原生体验,极端情况下甚至会出现白屏。主要原因是业务 jsbundle 比较大,RN 框架在加载 jsbundle 和通过 JSCore 解析 jsbundle 时耗时太长。当用户看到真正业务页面之前会出现长时间的空白页面。
当时提出了两个解决方案,第一种方式是实现一套预加载机制。预加载机制就是在用户真正进入业务之前,把 jsbundle 提前加载解析,提前把 RootView 生成。简单来说就是用空间换时间。但这样做并不是所有的业务都适合,因为会带来一些内存增长,所以一般在很核心很重要的业务采取预加载机制。第二种方式是修改了 RN 框架底层库,在 RN 框架开始加载 jsbundle 文件时,显示一个 loading 的进度提示用户正在做加载的动作。当 JS 文件加载并且解析渲染完成之后,把进度条去掉,最终被页面展现给用户。这样虽然等待时间并没有减少,但是用户体验会好很多,整体的时间从收到的反馈来看还是比 H5 要好很多,这是我们做的一个优化点。
我们还做了一件很重要的事情,就是内存优化。在 RN 框架开发中碰到的最大的坑就是内存这块,因为业务中会经常碰到 ListView 的使用,根据这些业务的需要,可能要加载很多页,两页、三页、甚至可能会无限加载。这种方式在早期的 RN 版本当中肯定会引起 OOM(OutOfMemory)崩溃,原因是在 RN 的早期版本当中并没有对 ListView 做内存复用。这意味着 ListView 滚多少,图片都会在内存中,当页面加载地越多,出现 OOM 崩溃的几率也越大,这是一个非常不能接受的问题。
在 RN 的早期版本,我们团队在 JS 层实现了一套内存回收。它的原理跟原生当中的原理也差不多,就是当页面划出两个屏幕之后,会强制把图片和内容进行回收,用一个空白的 View 替换。当内容划到用户可见的屏幕范围之后,再把图片给加载出来,这也是原生常用的一种内存回收的方式。修改后的效果很好,无论页面加载再多,都不会出现卡顿和 OOM 崩溃。在 RN 的新版本(0.43 之后),引入了一个新的 FlatList 组件。这个组件完全解决了 ListView 的内存回收问题。它的实现机制和我们的方案类似也是在 JS 层中做内存回收的动作。所以给大家建议,如果开发中碰到类似的问题,完全可以升级到最新的 RN Base 0.43 以上使用 FlatList 组件。如果版本比较低的话,那就需要自己实现这套机制。
第二个比较大的内存问题就是图片,iOS 平台可能相对好一些,在 Android 问题会相对多一些。RN 的底层图片框架库用的是 Fresco,而我们主 App 中用的也是 Fresco 底层库,这里就会有些问题。第一个就是重复初始化,这也是当时业务开发当中碰到的问题。当主 App 中的 Fresco 进行初始化之后,如果 RN 中也进行一次初始化,实际上之前那部分内存并没有被释放,会出现内存泄漏。我们做了专门的检测,避免 RN 重复初始化的问题。第二个也是跟 RN 框架里面的实现有关系,因为它采用的图片编解码用的是 ARGB_8888,这种方式支持 Alpha 通道。但实际上大部分情况下可以采用 RGB_565 编码,虽然丢失了 Alpha 通道,但是图片在内存当中的大小可以减少 50%。不过有些业务可能也真的需要一些透明的背景,需要 Alpha 通道,所以也提供了一些 API 来针对特殊图片,让它采用 ARGB_8888 进行编解码。这样既解决内存问题,也满足了业务的需求。
最后一个经验就是在所有的 RN 页面退出之前,建议强制调用 Fresco 框架的 clearMemoryCache 方法,通知 Fresco 清除内存缓存。可以保证 GC 及时地把这些图片内存给回收掉,避免整个 APP 的内存占用过高,经过实践验证这也很有效地解决了内存问题。