阅读 3467

小程序白屏问题和内存研究

在开发小程序应用中,QA发现过几次页面白屏的情况,苦于难易复现和调试,故想对小程序白屏问题进行一番探究。

从小程序官方开发者文档得知,微信小程序运行在三端:iOS(iPhone/iPad)、Android和用于调试的开发者工具。三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的[1]:

  1. 在 iOS 上,小程序逻辑层的 javascript 代码运行在 JavaScriptCore 中,视图层是由 WKWebView 来渲染的,环境有 iOS8、iOS9、iOS10;
  2. 在 Android 上,旧版本,小程序逻辑层的 javascript 代码运行中 X5 JSCore 中,视图层是由 X5 基于 Mobile Chrome 53/57 内核来渲染的;
  3. 新版本,小程序逻辑层的 javascript 代码运行在 V8 中,视图层是由自研 XWeb 引擎基于 Mobile Chrome 53 内核来渲染的;
  4. 在 开发工具上,小程序逻辑层的 javascript 代码是运行在 NW.js 中,视图层是由 Chromium 60 Webview 来渲染的。

下面说说WKWebView、Mobile Chrome 53/57、Mobile Chrome 53是什么。

在Apple公司的开发者文档网站上,有对WKWebView进行介绍,简单来说,WKWebView是一个为app内置浏览器渲染交互式网页内容的组件,用于替换老版本的UIWebView组件[2]。不管是UIWebView,还是WKWebView,它们都属于IOS WebView。我们可以把WebView理解为手机操作系统的一个系统级的组件。不管是手机内置的浏览器,还是其他app,比如微信等,只要你想呈现交互式的网页内容,都可以调用WebView去完成这件事情。Android WebView亦是如此[3]。

现在我们可以把WKWebView称为IOS端的WebView,那么Android端的Mobile Chrome 53/57,或者Mobile Chrome 53又是什么,这两个跟WebView又是什么关系呢? 我们可以把Mobile Chrome 53/57理解为Chrome for Android 537版本,这里的537是指Chrome的排版引擎(layout engine)采用的WebKit内核版本,具体参考Google Chrome version history[4]。需要指出的是,53/57是不是就是537,这里存疑,没有查到有效的参考资料,但是这个对我们的研究应该没有什么影响,可以不予考虑。到这里,又引入了两个概念:layout engine、WebKit内核。接下来简单介绍一下layout engine和WebKit内核。

我们都知道浏览器有两个重要的引擎:渲染引擎(rendering engine,也称layout engine,即上面提到的排版引擎,后续为了方便,统一描述为渲染引擎)和JS引擎。其中渲染引擎负责解析网页内容,计算显示方式,输出至显示设备。JS引擎则负责解析JavaScript语言,实现网页的动态交互效果。最开始时渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎,即浏览器内核就是该浏览器采用的渲染引擎,主要参考X5内核调研报告[5]。在后续的讨论中,浏览器内核就单指渲染引擎。

那WebKit内核又是什么?这个不得不追溯WebKit的历史了。1998,自由软件社区KDE开发了HTML排版引擎KHTML和JavaScript解析引擎KJS,也就是现代浏览器两个重要的引擎。Apple公司的开发者Don Melton于2001年在KDE的基础之上开始了WebKit项目。刚开始时,WebKit仅为KDE的复刻,我们可以理解为WebKit是KDE基础上fork出来的分支。后来,在WebKit项目中,KHTML被命名为WebCore,KJS被命名为JavaScriptCore,主要参考维基百科[6]。至此,我们可以回答,至少针对Apple的产品来说,浏览器内核就是WebKit,即渲染引擎采用的是WebKit内核。

webkit项目是Apple公司发展自家浏览器启动的项目。Google公司在发展Chrome浏览器也成立了Chromium项目。在Chromium项目中,JavaScript解析引擎采用Google自己开发的大名鼎鼎的V8引擎,渲染引擎采用的是WebKit内核。到2013年7月份,Chromium项目将渲染引擎替换为Blink引擎,并在Chrome28及后续的版本上采用[4][7]。Blink引擎是Google在WebKit项目中的WebCore基础上fork出来的一个分支[8][9]。我们可以用一幅图把KDE、WebKit和Chromium串联起来:

现在,我们再回过头来看一下Mobile Chrome 53/57,或者Mobile Chrome 53,其实它的内核还是从WebKit上演化而来。绕了这么远,只为一句话:小程序就是运行在WebView之上。那么我们的初衷,研究小程序白屏问题,其实就是在探究WebView白屏问题。如果要更详细一点,那就是WKWebview、Android WebView白屏的原因。

关于WKWebview白屏,网上罗列的常见原因大致有以下几种:

  1. 内存占用比较大时,WebContent Process 会 crash,从而出现白屏现象。
  2. URL网址无效或者含有中文字符。
  3. WKWebview刚推出时,在IOS8.0~8.2会偶尔出现白屏
  4. 由于滚动组件嵌套的结构,不刷新的问题。

针对原因3,解决的方案是判断IOS系统版本,小于8.2的使用UIWebView。如果站在小程序开发者的角度,这个跟我们好像没有关系。小程序是个平台,我们在这个平台上开发我们的小程序应用,如果小程序也有这个问题,那只能由小程序团队去解决这件事情。还有,比如原因4,我们该嵌套还是得嵌套,有问题也是小程序团队去解决。至于原因2,如果是小程序原生开发的话,页面间的跳转URL包含中文也是能正常跳转的,这个应该是小程序内部兼容了。但是原因1,这个跟我们就有很大的关系了,比如我们定义了大量的变量,使用完了却没有释放,那么这部分内存在小程序销毁之前会被一直占用。再比如我们在某一刻操作了某个比较大的变量,可能在短时间内,内存使用量也会飙升。同样的,对于导致Android WebView白屏的问题,绝大部分也只能由小程序团队去解决。

这样一来,从开发小程序应用的前端角度来说,我们能够把握的是尽量避免由于内存使用紧张导致的部分WebView被回收而出现的白屏问题。至此,我们研究的小程序白屏问题,可以转向对小程序内存优化的研究。

下面总结一下平时开发过程中可能会导致内存警告的操作:

  1. 使用大图片和长列表图片。根据小程序团队分析过的大部分案例,大图片和长列表图片的使用,都会引起WKWebview被回收[10]。其中长列表页图片是指页面包含数目较大的列表,每个列表里面又引用了图片。

  2. 随意定义变量,由于小程序的机制而又没有得到释放。以下四种场景下定义的变量,即使离开当前页面,变量也不会被回收:

    • 定义在Page构造器外层的全局变量。

    • 定义在data内部的数据。

    • 定义在Page内部,类data数据。

    • 挂载到getApp().globalData上的数据。

    假如我们在testvar页面定义了上述变量,由testvar通过navigateTo跳转到下一个页面otherpage,在页面otherpage里面我们可以通过getCurrentPages()获取页面testvar的引用,进而获取里面的变量。通过navigateTo打开新页面,上一个页面进入页面栈,并且该页面只是hide,并不是unload[11]。小程序框架的页面栈最多可支持10层页面。设想一下,那些具有复杂交互的页面,每层页面都附带了众多的数据,甚至包含很多图片,再考虑多层页面并存的问题,那内存使用量将是很可观的。在页面栈里面的页面unload之前,都会造成持续的内存占用。

  3. 短时间内大数据操作。假设在某个时间点,我们需要对接口返回的大量数据进行操作,可能会造成瞬时的内存占用。

  4. 列表数据的持续累加,导致某个数据异常大。设想一下,假如我们的列表页有很多条数据,每经过一次分页请求,我们就把新的数据concat到已有的数据之上,久而久之,这条数据可能会变成巨无霸,逐渐侵蚀我们的内存。

所幸的是,上述这些可能造成内存大量占用的操作,我们是可以避免或者优化的。

  1. 针对原因1中的大图片,我们就可以适当压缩压缩。如果不能再压了,或者图片必须这么大,还有单个图片本来都不大,但是列表太多造成引用的图片太多怎么办呢?好,这个可以暂时先放下,在后续的讨论中再提对应的解决方案。
  2. 针对原因2,我们需要结合实际的业务场景,对那些用完就可以丢弃的,不需要伴随页面整个生存周期存在的变量,就不要用那四种方式去定义数据。
  3. 针对原因3,我们可以尽量和接口开发方协商,通过分页或其他方式来避免接口一次返回大量的数据。
  4. 针对原因4,本质的原因是持续的分页请求导致新的数据不断追加到已有的数据之上,那么这种场景,我们就需要对已有的部分数据进行舍弃。舍弃哪些已有的数据,需要一个原则。设想一下有这样一个场景,我们进入列表页list,我们定义了listData用来存放每次分页请求过来的数据。第一页的数据过来了,listData仅仅包含第一页数据。第二页数据过来了,我们把新数据concat到第一页上,此时,listData就包含了第一、第二两个页面的数据。第三页的数据过来了,listData就包含前三个页面的数据。现在我们不妨停下来想想,目前我们给用户呈现的是第三页的数据,第一页的数据处于不可见的状态,既然不可见,为何不把它丢弃?如果用户往上滑动,需要呈现第一页的数据时,我们可以再请求第一页的数据。listData丢弃部分数据,会及时反馈到view层,view层部分节点也会随之销毁,这样App Service层和view层占用的部分内存都会得到释放。当然,我们提出的这种方案,就是为了解决持续的分页请求导致新的数据不断追加到已有的数据之上的问题,至于要不要采用,采用了什么场景下进行已有数据丢弃,丢弃哪些数据,这些都要结合实际的业务进行评估和权衡。

希望大家进行批评和指正!

参考文献: [1]: developers.weixin.qq.com/miniprogram… [2]: developer.apple.com/documentati… [3]: developer.android.com/reference/a… [4]: en.wikipedia.org/wiki/Google… [5]: juejin.im/post/684490… [6]: zh.wikipedia.org/wiki/WebKit [7]: zh.wikipedia.org/wiki/Google… [8]: zh.wikipedia.org/wiki/Chromi… [9]: zh.wikipedia.org/wiki/Blink [10]:developers.weixin.qq.com/miniprogram… [11]: developers.weixin.qq.com/miniprogram…