Android?WebView預(yù)渲染介紹
前言
在一個(gè)Hybrid項(xiàng)目中,必不可少的就是加載h5頁面。h5頁面的加載性能極大影響著用戶體驗(yàn),并會(huì)從各方面影響到我們APP的業(yè)務(wù)數(shù)據(jù)。試想,假設(shè)一個(gè)h5頁面要花好幾秒才能打開,那用戶還會(huì)使用我們的APP嗎?所以今天我們講一講,客戶端上優(yōu)化h5頁面加載速度的一種方式:預(yù)渲染。
照例,拋出本篇文章要解決的幾個(gè)問題:
- 客戶端可以從哪些方面優(yōu)化h5頁面的加載速度?
- 預(yù)渲染的基本實(shí)現(xiàn)邏輯是怎樣的?
- 預(yù)渲染存在哪些局限性?
術(shù)語對(duì)齊
| 術(shù)語 | 描述 |
|---|---|
| WebView | 用于承載h5頁面的native組件。 |
| 預(yù)創(chuàng)建 | 在用戶打開一個(gè)h5頁面之前,在內(nèi)存中先創(chuàng)建好一個(gè)WebView實(shí)例。當(dāng)用戶打開h5頁面時(shí),可以直接取到預(yù)先創(chuàng)建好的WebView實(shí)例,用于承載h5頁面。 |
| 預(yù)渲染 | 在用戶打開一個(gè)h5頁面之前,在內(nèi)存中不僅預(yù)創(chuàng)建好了WebView實(shí)例,還進(jìn)一步根據(jù)url提前渲染好了WebView。當(dāng)用戶打開指定的url頁面時(shí),可以直接拿預(yù)先渲染好了的WebView進(jìn)行展示。 |
客戶端可以從哪些方面優(yōu)化h5頁面的加載速度?
我們可以看一下,在Android上完整打開一個(gè)WebView需要經(jīng)歷怎樣的一個(gè)鏈路(來自優(yōu)秀的前輩們):

所以,要想優(yōu)化WebView的加載速度,就要想辦法去縮短鏈路中這些節(jié)點(diǎn)所耗費(fèi)的時(shí)間。
從客戶端的角度上來講,Page初始化就是H5容器的初始化。一般而言,容器初始化時(shí)間是比較快的(如果沒有太多初始化邏輯的話),優(yōu)化空間有限。WebView的初始化相對(duì)就比較復(fù)雜,涉及到了瀏覽器內(nèi)核在主線程的初始化。APP冷啟動(dòng)后,首次創(chuàng)建WebView時(shí)需要去初始化瀏覽器內(nèi)核。
這里要分3種情況去看:
- 全新安裝APP,冷啟動(dòng)后首次打開一個(gè)WebView,耗時(shí)最長,可能會(huì)需要1000ms左右,取決于瀏覽器內(nèi)核。
- 非全新安裝APP,冷啟動(dòng)后首次打開一個(gè)WebView,耗時(shí)分布在500ms左右。
- 冷啟動(dòng)后,非首次打開一個(gè)WebView,耗時(shí)就非常短了,分布在15ms左右。
這里我們可以看到,第1種和第2種情況是存在比較大的提升空間的。
當(dāng)我們創(chuàng)建好WebView,執(zhí)行WebView#loadUrl()時(shí),WebView就會(huì)經(jīng)歷上圖中的白屏-loading-可交互狀態(tài)。這幾個(gè)階段,可以說是整個(gè)鏈路中耗時(shí)占比最大的一部分。但客戶端在這里能做的優(yōu)化是很有限的,而且通常需要跟前端、服務(wù)端去配合優(yōu)化,比如可以并行請(qǐng)求數(shù)據(jù),這里就先不發(fā)散了。但如果換個(gè)角度思考,客戶端先不去干涉后面這幾個(gè)階段的邏輯,而是提前去執(zhí)行后面這幾個(gè)階段的邏輯,那么不也就相當(dāng)于提高加載速度了么?這其實(shí)就是我們要講的預(yù)加載。
優(yōu)化思路
所以我們的優(yōu)化思路主要針對(duì)WebView初始化階段,以及WebView加載階段。
通過預(yù)創(chuàng)建WebView,去解決首次創(chuàng)建WebView耗時(shí)長的問題。
通過預(yù)渲染W(wǎng)ebView,去提前經(jīng)歷用戶需要等待的白屏-loading階段,當(dāng)用戶打開相應(yīng)頁面時(shí),能夠直接上屏展示,給用戶的感覺就是秒開。
預(yù)渲染的基本實(shí)現(xiàn)邏輯是怎樣的?
預(yù)創(chuàng)建
預(yù)創(chuàng)建是預(yù)渲染的前提(沒有預(yù)創(chuàng)建好怎么預(yù)渲染呢..),所以我們先講下預(yù)創(chuàng)建。預(yù)創(chuàng)建WebView,一個(gè)基本原則就是,當(dāng)內(nèi)存中沒有預(yù)創(chuàng)建的WebView可以復(fù)用(即預(yù)創(chuàng)建沒有命中)時(shí),就走原來創(chuàng)建WebView的邏輯。
預(yù)創(chuàng)建個(gè)數(shù)
這里我們選擇只預(yù)創(chuàng)建1個(gè)WebView。之所以選擇1個(gè),是因?yàn)槲覀冾A(yù)創(chuàng)建WebView的根本目的,是為了解決APP首次安裝/冷啟動(dòng)時(shí),第一個(gè)WebView加載慢的問題。后續(xù)的WebView實(shí)例的創(chuàng)建都是很快的。所以,即使后面沒有命中預(yù)創(chuàng)建的WebView,用的重新創(chuàng)建的WebView,也就是多花了15ms左右的時(shí)間,影響是很小的。所以綜合下來,預(yù)創(chuàng)建1個(gè)WebView的性價(jià)比是最高的,多了反而浪費(fèi)內(nèi)存。
預(yù)創(chuàng)建時(shí)機(jī)
這里的時(shí)機(jī)要分為三個(gè),第一個(gè)時(shí)機(jī)是在冷啟動(dòng)后,我們需要進(jìn)行預(yù)創(chuàng)建??梢赃x擇把這個(gè)時(shí)機(jī)放到進(jìn)入首頁后,用IdleHandler進(jìn)行主線程閑時(shí)創(chuàng)建。當(dāng)然也可以選擇前置。前置的話有可能會(huì)影響到APP的啟動(dòng),所以如果不是特別有必要的話,建議還是后置一些。
第二個(gè)時(shí)機(jī)是在預(yù)創(chuàng)建的WebView被拿去復(fù)用后,此時(shí)也是需要預(yù)創(chuàng)建的。因?yàn)橐坏┍荒萌?fù)用,意味著我們緩存中已經(jīng)沒有可用的WebView了,若一個(gè)pha頁面又打開了另外一個(gè)pha頁面,我們?cè)谶@個(gè)case最好也能提供預(yù)創(chuàng)建的WebView。
以上兩個(gè)時(shí)機(jī)都是自動(dòng)觸發(fā)。后來發(fā)現(xiàn)一個(gè)場景,當(dāng)用戶在某個(gè)路徑比較深的頁面時(shí),若需要預(yù)加載下一個(gè)頁面,那么這個(gè)頁面往往是不需要一冷啟動(dòng)就預(yù)渲染的。這時(shí)候就需要一個(gè)接口讓業(yè)務(wù)方能在用戶打開頁面之前將該頁面進(jìn)行預(yù)加載。
void preload(Context context, String url);
預(yù)創(chuàng)建復(fù)用
復(fù)用WebView需要注意一點(diǎn),每個(gè)WebView都是跟指定的Context綁定的,但預(yù)創(chuàng)建時(shí),還獲取不到WebView未來要綁定的Context。因此預(yù)創(chuàng)建時(shí)可以用MutableContextWrapper包ApplicationContext去創(chuàng)建。MutableContextWrapper支持我們將其中的BaseContext進(jìn)行替換,復(fù)用預(yù)創(chuàng)建的WebView時(shí),將ApplicationContext替換為需要綁定的Context即可。同時(shí)根據(jù)“預(yù)創(chuàng)建時(shí)機(jī)”中說的,在復(fù)用時(shí),往棧頂插入一個(gè)新的預(yù)創(chuàng)建的WebView。相應(yīng)的,當(dāng)頁面關(guān)閉時(shí),我們也需要將綁定的Context解綁,防止內(nèi)存泄漏。
大致的邏輯如下圖所示:

這里也貼出部分偽代碼:
/**
* 閑時(shí)預(yù)創(chuàng)建
*/
private void preCreateWebView() {
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 預(yù)創(chuàng)建webView
WebView webView = new WebView(new MutableContextWrapper(APPLICATION_CONTEXT));
cache.add(webView);
return false;
}
});
}
/**
* 獲取預(yù)創(chuàng)建的WebView
* @param context 要與使用的webview綁定的context
* @return
*/
public PHAWebView acquirePreCreateWebView(Context context) {
WebView webview;
// 緩存中無可用WebView時(shí),直接新建
if (cache.isEmpty()) {
webview = createWebView();
} else {
webview = cache.peekWebView();
}
// 更改context
if (webview != null && webview.getContext() instanceof MutableContextWrapper) {
MutableContextWrapper webViewMutableContext = (MutableContextWrapper) webview.getContext();
webViewMutableContext.setBaseContext(context);
}
return webview;
}預(yù)渲染
預(yù)渲染,其實(shí)就是在預(yù)創(chuàng)建的基礎(chǔ)上,執(zhí)行WebView#loadUrl(),將頁面提前渲染完成。但這里面還有一些細(xì)節(jié)點(diǎn)需要關(guān)注。
預(yù)渲染時(shí)機(jī)
預(yù)渲染的時(shí)機(jī)也是分為兩個(gè),一個(gè)是冷啟動(dòng)后的主線程閑時(shí)階段進(jìn)行預(yù)渲染,這一點(diǎn)跟預(yù)加載的時(shí)機(jī)保持一致。還有一個(gè)我們可以選擇在頁面關(guān)閉時(shí)進(jìn)行預(yù)渲染。打比方說,假設(shè)我預(yù)渲染了頁面A,那么用戶在訪問完頁面A后,我需要再次預(yù)渲染頁面A,從而保證頁面A的實(shí)效性。
預(yù)渲染白名單
首先,預(yù)渲染是有對(duì)象的,預(yù)渲染的對(duì)象就是頁面url。而預(yù)創(chuàng)建是沒有特定對(duì)象的,只需要隨時(shí)準(zhǔn)備一個(gè)可用的WebView就行了,誰都可以用。但預(yù)渲染不行,不告訴我預(yù)渲染誰,我還怎么預(yù)渲染。
所以,從初始化開始,我們就已經(jīng)決定好了該預(yù)渲染哪些頁面,也就是預(yù)渲染白名單。白名單可通過服務(wù)器配置的方式進(jìn)行下發(fā)。但也不能一股腦把頁面全都配上,因?yàn)閮?nèi)存是有限的,配太多可能會(huì)引起低端機(jī)型的OOM。
預(yù)渲染有效性校驗(yàn)
所謂的有效性校驗(yàn),就是在復(fù)用預(yù)渲染W(wǎng)ebView時(shí),校驗(yàn)這個(gè)WebView是否被正常預(yù)渲染了。如果失效,就走預(yù)創(chuàng)建/重新創(chuàng)建的邏輯。這里分兩個(gè)角度來校驗(yàn)WebView的有效性:時(shí)間有效性與狀態(tài)有效性。
時(shí)間有效性
存在這樣一種場景:當(dāng)我們已經(jīng)預(yù)渲染了A頁面,且用戶一直沒有訪問。某個(gè)時(shí)刻這個(gè)頁面做了更新,即重新發(fā)布了,那么如果用戶這時(shí)候去訪問A頁面,看到的還是舊的A頁面。所以這里需要在預(yù)渲染頁面時(shí),給頁面設(shè)置一個(gè)過期時(shí)間,若復(fù)用預(yù)渲染W(wǎng)ebView時(shí)已經(jīng)過期了,就說明WebView已經(jīng)失效了,需要重新loadUrl保證頁面的實(shí)效性。
狀態(tài)有效性
狀態(tài)有效性就是去校驗(yàn)預(yù)渲染的WebView最終是否有渲染成功,這一點(diǎn)我們可以通過WebViewClient的生命周期回調(diào)(onPageFinished/onReceivedError)來進(jìn)行判斷。之所以要做這個(gè)校驗(yàn),是為了防止一開始預(yù)渲染就失敗了,卻還是拿這個(gè)WebView去進(jìn)行展示。比如,假設(shè)我們?cè)诰W(wǎng)絡(luò)異常狀態(tài)下去進(jìn)行了預(yù)渲染,在網(wǎng)絡(luò)恢復(fù)正常后用戶訪問預(yù)渲染頁面,若不進(jìn)行狀態(tài)校驗(yàn),那么看到的就會(huì)是網(wǎng)絡(luò)異常狀態(tài)下的WebView了。
頁面顯示狀態(tài)通知
頁面顯示狀態(tài),通俗來講就是頁面現(xiàn)在是離屏的,還是上屏的。在h5的一些業(yè)務(wù)場景中,有一部分是需要感知到頁面的顯示狀態(tài)的。比如引導(dǎo)類的動(dòng)畫,比如會(huì)場的一些倒計(jì)時(shí)等等。所以我們需要將頁面的顯示狀態(tài)同步到h5那邊。
實(shí)現(xiàn)上,就是要在預(yù)渲染W(wǎng)ebView時(shí)給h5注入一個(gè)全局的環(huán)境變量,window.page_on_screen=false。當(dāng)復(fù)用WebView,即上屏?xí)r,再將window.page_on_screen設(shè)置為true,同時(shí)發(fā)一個(gè)通知給h5。這樣h5就可以根據(jù)同步到的顯示狀態(tài)來控制自己的業(yè)務(wù)邏輯。
其它注意事項(xiàng)
預(yù)渲染、預(yù)創(chuàng)建,本質(zhì)上是用空間換時(shí)間的優(yōu)化,所以是比較耗費(fèi)內(nèi)存的。所以我們需要在內(nèi)存不足的時(shí)候,及時(shí)將內(nèi)存中待使用的WebView給回收掉,避免APP發(fā)生OOM。
另外,因?yàn)轭A(yù)渲染離屏加載了頁面,所以頁面的初始化行為是需要納入評(píng)估的,只有評(píng)估通過后,才能放入預(yù)渲染白名單中。具體的初始化行為包括但不限于:業(yè)務(wù)的曝光埋點(diǎn)、前端邏輯(如倒計(jì)時(shí)、跨天活動(dòng))、消費(fèi)型(如首次引導(dǎo))、后端流量評(píng)估、頁面在后臺(tái)是否會(huì)有聲音、是否會(huì)彈框(系統(tǒng)框、權(quán)限框、對(duì)話框...)等等。
預(yù)渲染存在哪些局限性?
- 低端機(jī)內(nèi)存空間有限,預(yù)渲染白名單的實(shí)際配置數(shù)量需要視情況進(jìn)行調(diào)整。
- 預(yù)渲染頁面必須經(jīng)過白名單配置。頁面url、參數(shù)發(fā)生改變,配置也需要改變。這一點(diǎn)其實(shí)也是有優(yōu)化空間的,即離屏預(yù)渲染時(shí)加載url前綴域名,上屏?xí)r再根據(jù)完整的url參數(shù)做邏輯調(diào)整。實(shí)現(xiàn)上會(huì)比較麻煩,可以視ROI情況進(jìn)行投入。
- 預(yù)渲染頁面的實(shí)效性無法保證。預(yù)渲染頁面一旦重新部署,端上是不能立刻感知到并重新加載的。按上面的預(yù)渲染時(shí)機(jī),目前只有以下二個(gè)場景會(huì)觸發(fā)端上對(duì)預(yù)渲染頁面的更新:
- 1.冷啟動(dòng);
- 2.頁面被訪問后關(guān)閉;
- 3.業(yè)務(wù)調(diào)用接口主動(dòng)注入。
所以大家如果有比較好的方案歡迎分享給我呀!
- 命中率。預(yù)渲染頁面是不能百分百命中的,即即使我們把某個(gè)頁面配置進(jìn)了預(yù)渲染白名單,app也有可能沒預(yù)渲染上這個(gè)頁面。有很多異常場景會(huì)影響到命中率,比如:
- 1.上面講到的預(yù)渲染的時(shí)間有效性與狀態(tài)有效性;
- 2.服務(wù)端下發(fā)的預(yù)渲染白名單沒有及時(shí)拉取到;
- 3.主線程一直繁忙,導(dǎo)致預(yù)渲染邏輯一直沒執(zhí)行;
- 4.內(nèi)存不夠,將緩存的預(yù)渲染W(wǎng)ebView回收掉了。
總結(jié)
所以雖然預(yù)渲染能從表面上實(shí)現(xiàn)h5頁面的秒開,但也不是萬能的,是存在一些缺陷的(否則也不需要?jiǎng)e的優(yōu)化手段了)。但我認(rèn)為是諸多優(yōu)化手段中比較簡單卻又能立竿見影的一個(gè)手段,特別是對(duì)本身h5頁面加載就非常慢的app而言。所以如果還沒做起來的同學(xué)可以試一試,后面再結(jié)合其它優(yōu)化的手段抹除不足。今天就講到這里啦。
到此這篇關(guān)于Android WebView預(yù)渲染介紹的文章就介紹到這了,更多相關(guān)Android WebView預(yù)渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用RecylerView完成拖動(dòng)排序高仿qq側(cè)滑刪除功能
最近在做一個(gè)android項(xiàng)目,使用到Recylerview完成拖動(dòng)排序,側(cè)滑刪除功能,今天小編把思路分享到腳本之家平臺(tái),供大家學(xué)習(xí)2016-10-10
Android開發(fā)應(yīng)用第一步 安裝及配置模擬器Genymotion
這篇文章主要介紹了Android開發(fā)應(yīng)用第一步,即安裝及配置模擬器Genymotion,感興趣的小伙伴們可以參考一下2015-12-12
Android編程實(shí)現(xiàn)控件不同狀態(tài)文字顯示不同顏色的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)控件不同狀態(tài)文字顯示不同顏色的方法,涉及Android針對(duì)控件布局文件屬性設(shè)置及狀態(tài)判定等相關(guān)技巧,需要的朋友可以參考下2016-02-02
Android評(píng)分控件RatingBar使用實(shí)例解析
這篇文章主要為大家詳細(xì)介紹了Android評(píng)分控件RatingBar使用實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
Android利用ContentProvider獲取本地?cái)?shù)據(jù)的方法
這篇文章主要介紹了Android利用ContentProvider獲取本地?cái)?shù)據(jù)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04

