在開(kāi)發(fā)小程序應(yīng)用中,QA發(fā)現(xiàn)過(guò)幾次頁(yè)面白屏的情況,苦于難易復(fù)現(xiàn)和調(diào)試,故想對(duì)小程序白屏問(wèn)題進(jìn)行一番探究。
從小程序官方開(kāi)發(fā)者文檔得知,微信小程序運(yùn)行在三端:iOS(iPhone/iPad)、Android和用于調(diào)試的開(kāi)發(fā)者工具。三端的腳本執(zhí)行環(huán)境以及用于渲染非原生組件的環(huán)境是各不相同的[1]:
下面說(shuō)說(shuō)WKWebView、Mobile Chrome 53/57、Mobile Chrome 53是什么。
在Apple公司的開(kāi)發(fā)者文檔網(wǎng)站上,有對(duì)WKWebView進(jìn)行介紹,簡(jiǎn)單來(lái)說(shuō),WKWebView是一個(gè)為app內(nèi)置瀏覽器渲染交互式網(wǎng)頁(yè)內(nèi)容的組件,用于替換老版本的UIWebView組件[2]。不管是UIWebView,還是WKWebView,它們都屬于IOS WebView。我們可以把WebView理解為手機(jī)操作系統(tǒng)的一個(gè)系統(tǒng)級(jí)的組件。不管是手機(jī)內(nèi)置的瀏覽器,還是其他app,比如微信等,只要你想呈現(xiàn)交互式的網(wǎng)頁(yè)內(nèi)容,都可以調(diào)用WebView去完成這件事情。Android WebView亦是如此[3]。
現(xiàn)在我們可以把WKWebView稱為IOS端的WebView,那么Android端的Mobile Chrome 53/57,或者M(jìn)obile Chrome 53又是什么,這兩個(gè)跟WebView又是什么關(guān)系呢? 我們可以把Mobile Chrome 53/57理解為Chrome for Android 537版本,這里的537是指Chrome的排版引擎(layout engine)采用的WebKit內(nèi)核版本,具體參考Google Chrome version history[4]。需要指出的是,53/57是不是就是537,這里存疑,沒(méi)有查到有效的參考資料,但是這個(gè)對(duì)我們的研究應(yīng)該沒(méi)有什么影響,可以不予考慮。到這里,又引入了兩個(gè)概念:layout engine、WebKit內(nèi)核。接下來(lái)簡(jiǎn)單介紹一下layout engine和WebKit內(nèi)核。
我們都知道瀏覽器有兩個(gè)重要的引擎:渲染引擎(rendering engine,也稱layout engine,即上面提到的排版引擎,后續(xù)為了方便,統(tǒng)一描述為渲染引擎)和JS引擎。其中渲染引擎負(fù)責(zé)解析網(wǎng)頁(yè)內(nèi)容,計(jì)算顯示方式,輸出至顯示設(shè)備。JS引擎則負(fù)責(zé)解析JavaScript語(yǔ)言,實(shí)現(xiàn)網(wǎng)頁(yè)的動(dòng)態(tài)交互效果。最開(kāi)始時(shí)渲染引擎和JS引擎并沒(méi)有區(qū)分的很明確,后來(lái)JS引擎越來(lái)越獨(dú)立,內(nèi)核就傾向于只指渲染引擎,即瀏覽器內(nèi)核就是該瀏覽器采用的渲染引擎,主要參考X5內(nèi)核調(diào)研報(bào)告[5]。在后續(xù)的討論中,瀏覽器內(nèi)核就單指渲染引擎。
那WebKit內(nèi)核又是什么?這個(gè)不得不追溯WebKit的歷史了。1998,自由軟件社區(qū)KDE開(kāi)發(fā)了HTML排版引擎KHTML和JavaScript解析引擎KJS,也就是現(xiàn)代瀏覽器兩個(gè)重要的引擎。Apple公司的開(kāi)發(fā)者Don Melton于2001年在KDE的基礎(chǔ)之上開(kāi)始了WebKit項(xiàng)目。剛開(kāi)始時(shí),WebKit僅為KDE的復(fù)刻,我們可以理解為WebKit是KDE基礎(chǔ)上fork出來(lái)的分支。后來(lái),在WebKit項(xiàng)目中,KHTML被命名為WebCore,KJS被命名為JavaScriptCore,主要參考維基百科[6]。至此,我們可以回答,至少針對(duì)Apple的產(chǎn)品來(lái)說(shuō),瀏覽器內(nèi)核就是WebKit,即渲染引擎采用的是WebKit內(nèi)核。
webkit項(xiàng)目是Apple公司發(fā)展自家瀏覽器啟動(dòng)的項(xiàng)目。Google公司在發(fā)展Chrome瀏覽器也成立了Chromium項(xiàng)目。在Chromium項(xiàng)目中,JavaScript解析引擎采用Google自己開(kāi)發(fā)的大名鼎鼎的V8引擎,渲染引擎采用的是WebKit內(nèi)核。到2013年7月份,Chromium項(xiàng)目將渲染引擎替換為Blink引擎,并在Chrome28及后續(xù)的版本上采用[4][7]。Blink引擎是Google在WebKit項(xiàng)目中的WebCore基礎(chǔ)上fork出來(lái)的一個(gè)分支[8][9]。我們可以用一幅圖把KDE、WebKit和Chromium串聯(lián)起來(lái):
現(xiàn)在,我們?cè)倩剡^(guò)頭來(lái)看一下Mobile Chrome 53/57,或者M(jìn)obile Chrome 53,其實(shí)它的內(nèi)核還是從WebKit上演化而來(lái)。繞了這么遠(yuǎn),只為一句話:小程序就是運(yùn)行在WebView之上。那么我們的初衷,研究小程序白屏問(wèn)題,其實(shí)就是在探究WebView白屏問(wèn)題。如果要更詳細(xì)一點(diǎn),那就是WKWebview、Android WebView白屏的原因。
關(guān)于WKWebview白屏,網(wǎng)上羅列的常見(jiàn)原因大致有以下幾種:
針對(duì)原因3,解決的方案是判斷IOS系統(tǒng)版本,小于8.2的使用UIWebView。如果站在小程序開(kāi)發(fā)者的角度,這個(gè)跟我們好像沒(méi)有關(guān)系。小程序是個(gè)平臺(tái),我們?cè)谶@個(gè)平臺(tái)上開(kāi)發(fā)我們的小程序應(yīng)用,如果小程序也有這個(gè)問(wèn)題,那只能由小程序團(tuán)隊(duì)去解決這件事情。還有,比如原因4,我們?cè)撉短走€是得嵌套,有問(wèn)題也是小程序團(tuán)隊(duì)去解決。至于原因2,如果是小程序原生開(kāi)發(fā)的話,頁(yè)面間的跳轉(zhuǎn)URL包含中文也是能正常跳轉(zhuǎn)的,這個(gè)應(yīng)該是小程序內(nèi)部兼容了。但是原因1,這個(gè)跟我們就有很大的關(guān)系了,比如我們定義了大量的變量,使用完了卻沒(méi)有釋放,那么這部分內(nèi)存在小程序銷毀之前會(huì)被一直占用。再比如我們?cè)谀骋豢滩僮髁四硞€(gè)比較大的變量,可能在短時(shí)間內(nèi),內(nèi)存使用量也會(huì)飆升。同樣的,對(duì)于導(dǎo)致Android WebView白屏的問(wèn)題,絕大部分也只能由小程序團(tuán)隊(duì)去解決。
這樣一來(lái),從開(kāi)發(fā)小程序應(yīng)用的前端角度來(lái)說(shuō),我們能夠把握的是盡量避免由于內(nèi)存使用緊張導(dǎo)致的部分WebView被回收而出現(xiàn)的白屏問(wèn)題。至此,我們研究的小程序白屏問(wèn)題,可以轉(zhuǎn)向?qū)π〕绦騼?nèi)存優(yōu)化的研究。
下面總結(jié)一下平時(shí)開(kāi)發(fā)過(guò)程中可能會(huì)導(dǎo)致內(nèi)存警告的操作:
使用大圖片和長(zhǎng)列表圖片。根據(jù)小程序團(tuán)隊(duì)分析過(guò)的大部分案例,大圖片和長(zhǎng)列表圖片的使用,都會(huì)引起WKWebview被回收[10]。其中長(zhǎng)列表頁(yè)圖片是指頁(yè)面包含數(shù)目較大的列表,每個(gè)列表里面又引用了圖片。
隨意定義變量,由于小程序的機(jī)制而又沒(méi)有得到釋放。以下四種場(chǎng)景下定義的變量,即使離開(kāi)當(dāng)前頁(yè)面,變量也不會(huì)被回收:
定義在Page構(gòu)造器外層的全局變量。
定義在data內(nèi)部的數(shù)據(jù)。
定義在Page內(nèi)部,類data數(shù)據(jù)。
掛載到getApp().globalData上的數(shù)據(jù)。
假如我們?cè)趖estvar頁(yè)面定義了上述變量,由testvar通過(guò)navigateTo跳轉(zhuǎn)到下一個(gè)頁(yè)面otherpage,在頁(yè)面otherpage里面我們可以通過(guò)getCurrentPages()獲取頁(yè)面testvar的引用,進(jìn)而獲取里面的變量。通過(guò)navigateTo打開(kāi)新頁(yè)面,上一個(gè)頁(yè)面進(jìn)入頁(yè)面棧,并且該頁(yè)面只是hide,并不是unload[11]。小程序框架的頁(yè)面棧最多可支持10層頁(yè)面。設(shè)想一下,那些具有復(fù)雜交互的頁(yè)面,每層頁(yè)面都附帶了眾多的數(shù)據(jù),甚至包含很多圖片,再考慮多層頁(yè)面并存的問(wèn)題,那內(nèi)存使用量將是很可觀的。在頁(yè)面棧里面的頁(yè)面unload之前,都會(huì)造成持續(xù)的內(nèi)存占用。
短時(shí)間內(nèi)大數(shù)據(jù)操作。假設(shè)在某個(gè)時(shí)間點(diǎn),我們需要對(duì)接口返回的大量數(shù)據(jù)進(jìn)行操作,可能會(huì)造成瞬時(shí)的內(nèi)存占用。
列表數(shù)據(jù)的持續(xù)累加,導(dǎo)致某個(gè)數(shù)據(jù)異常大。設(shè)想一下,假如我們的列表頁(yè)有很多條數(shù)據(jù),每經(jīng)過(guò)一次分頁(yè)請(qǐng)求,我們就把新的數(shù)據(jù)concat到已有的數(shù)據(jù)之上,久而久之,這條數(shù)據(jù)可能會(huì)變成巨無(wú)霸,逐漸侵蝕我們的內(nèi)存。
所幸的是,上述這些可能造成內(nèi)存大量占用的操作,我們是可以避免或者優(yōu)化的。
希望大家進(jìn)行批評(píng)和指正!