小程式白屏問題和記憶體研究

叫我振振發表於2019-03-03

在開發小程式應用中,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/5a3522… [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…

相關文章