Android?WebView預(yù)渲染介紹
前言
在一個(gè)Hybrid項(xiàng)目中,必不可少的就是加載h5頁(yè)面。h5頁(yè)面的加載性能極大影響著用戶體驗(yàn),并會(huì)從各方面影響到我們APP的業(yè)務(wù)數(shù)據(jù)。試想,假設(shè)一個(gè)h5頁(yè)面要花好幾秒才能打開(kāi),那用戶還會(huì)使用我們的APP嗎?所以今天我們講一講,客戶端上優(yōu)化h5頁(yè)面加載速度的一種方式:預(yù)渲染。
照例,拋出本篇文章要解決的幾個(gè)問(wèn)題:
- 客戶端可以從哪些方面優(yōu)化h5頁(yè)面的加載速度?
- 預(yù)渲染的基本實(shí)現(xiàn)邏輯是怎樣的?
- 預(yù)渲染存在哪些局限性?
術(shù)語(yǔ)對(duì)齊
術(shù)語(yǔ) | 描述 |
---|---|
WebView | 用于承載h5頁(yè)面的native組件。 |
預(yù)創(chuàng)建 | 在用戶打開(kāi)一個(gè)h5頁(yè)面之前,在內(nèi)存中先創(chuàng)建好一個(gè)WebView實(shí)例。當(dāng)用戶打開(kāi)h5頁(yè)面時(shí),可以直接取到預(yù)先創(chuàng)建好的WebView實(shí)例,用于承載h5頁(yè)面。 |
預(yù)渲染 | 在用戶打開(kāi)一個(gè)h5頁(yè)面之前,在內(nèi)存中不僅預(yù)創(chuàng)建好了WebView實(shí)例,還進(jìn)一步根據(jù)url提前渲染好了WebView。當(dāng)用戶打開(kāi)指定的url頁(yè)面時(shí),可以直接拿預(yù)先渲染好了的WebView進(jìn)行展示。 |
客戶端可以從哪些方面優(yōu)化h5頁(yè)面的加載速度?
我們可以看一下,在Android上完整打開(kāi)一個(gè)WebView需要經(jīng)歷怎樣的一個(gè)鏈路(來(lái)自優(yōu)秀的前輩們):
所以,要想優(yōu)化WebView的加載速度,就要想辦法去縮短鏈路中這些節(jié)點(diǎn)所耗費(fèi)的時(shí)間。
從客戶端的角度上來(lái)講,Page初始化就是H5容器的初始化。一般而言,容器初始化時(shí)間是比較快的(如果沒(méi)有太多初始化邏輯的話),優(yōu)化空間有限。WebView的初始化相對(duì)就比較復(fù)雜,涉及到了瀏覽器內(nèi)核在主線程的初始化。APP冷啟動(dòng)后,首次創(chuàng)建WebView時(shí)需要去初始化瀏覽器內(nèi)核。
這里要分3種情況去看:
- 全新安裝APP,冷啟動(dòng)后首次打開(kāi)一個(gè)WebView,耗時(shí)最長(zhǎng),可能會(huì)需要1000ms左右,取決于瀏覽器內(nèi)核。
- 非全新安裝APP,冷啟動(dòng)后首次打開(kāi)一個(gè)WebView,耗時(shí)分布在500ms左右。
- 冷啟動(dòng)后,非首次打開(kāi)一個(gè)WebView,耗時(shí)就非常短了,分布在15ms左右。
這里我們可以看到,第1種和第2種情況是存在比較大的提升空間的。
當(dāng)我們創(chuàng)建好WebView,執(zhí)行WebView#loadUrl()時(shí),WebView就會(huì)經(jīng)歷上圖中的白屏-loading-可交互狀態(tài)。這幾個(gè)階段,可以說(shuō)是整個(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加載階段。
通過(guò)預(yù)創(chuàng)建WebView,去解決首次創(chuàng)建WebView耗時(shí)長(zhǎng)的問(wèn)題。
通過(guò)預(yù)渲染W(wǎng)ebView,去提前經(jīng)歷用戶需要等待的白屏-loading階段,當(dāng)用戶打開(kāi)相應(yīng)頁(yè)面時(shí),能夠直接上屏展示,給用戶的感覺(jué)就是秒開(kāi)。
預(yù)渲染的基本實(shí)現(xiàn)邏輯是怎樣的?
預(yù)創(chuàng)建
預(yù)創(chuàng)建是預(yù)渲染的前提(沒(méi)有預(yù)創(chuàng)建好怎么預(yù)渲染呢..),所以我們先講下預(yù)創(chuàng)建。預(yù)創(chuàng)建WebView,一個(gè)基本原則就是,當(dāng)內(nèi)存中沒(méi)有預(yù)創(chuàng)建的WebView可以復(fù)用(即預(yù)創(chuàng)建沒(méi)有命中)時(shí),就走原來(lái)創(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加載慢的問(wèn)題。后續(xù)的WebView實(shí)例的創(chuàng)建都是很快的。所以,即使后面沒(méi)有命中預(yù)創(chuàng)建的WebView,用的重新創(chuàng)建的WebView,也就是多花了15ms左右的時(shí)間,影響是很小的。所以綜合下來(lái),預(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)建。可以選擇把這個(gè)時(shí)機(jī)放到進(jìn)入首頁(yè)后,用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)沒(méi)有可用的WebView了,若一個(gè)pha頁(yè)面又打開(kāi)了另外一個(gè)pha頁(yè)面,我們?cè)谶@個(gè)case最好也能提供預(yù)創(chuàng)建的WebView。
以上兩個(gè)時(shí)機(jī)都是自動(dòng)觸發(fā)。后來(lái)發(fā)現(xiàn)一個(gè)場(chǎng)景,當(dāng)用戶在某個(gè)路徑比較深的頁(yè)面時(shí),若需要預(yù)加載下一個(gè)頁(yè)面,那么這個(gè)頁(yè)面往往是不需要一冷啟動(dòng)就預(yù)渲染的。這時(shí)候就需要一個(gè)接口讓業(yè)務(wù)方能在用戶打開(kāi)頁(yè)面之前將該頁(yè)面進(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未來(lái)要綁定的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ī)”中說(shuō)的,在復(fù)用時(shí),往棧頂插入一個(gè)新的預(yù)創(chuàng)建的WebView。相應(yīng)的,當(dāng)頁(yè)面關(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; // 緩存中無(wú)可用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(),將頁(yè)面提前渲染完成。但這里面還有一些細(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è)我們可以選擇在頁(yè)面關(guān)閉時(shí)進(jìn)行預(yù)渲染。打比方說(shuō),假設(shè)我預(yù)渲染了頁(yè)面A,那么用戶在訪問(wèn)完頁(yè)面A后,我需要再次預(yù)渲染頁(yè)面A,從而保證頁(yè)面A的實(shí)效性。
預(yù)渲染白名單
首先,預(yù)渲染是有對(duì)象的,預(yù)渲染的對(duì)象就是頁(yè)面url。而預(yù)創(chuàng)建是沒(méi)有特定對(duì)象的,只需要隨時(shí)準(zhǔn)備一個(gè)可用的WebView就行了,誰(shuí)都可以用。但預(yù)渲染不行,不告訴我預(yù)渲染誰(shuí),我還怎么預(yù)渲染。
所以,從初始化開(kāi)始,我們就已經(jīng)決定好了該預(yù)渲染哪些頁(yè)面,也就是預(yù)渲染白名單。白名單可通過(guò)服務(wù)器配置的方式進(jìn)行下發(fā)。但也不能一股腦把頁(yè)面全都配上,因?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è)角度來(lái)校驗(yàn)WebView的有效性:時(shí)間有效性與狀態(tài)有效性。
時(shí)間有效性
存在這樣一種場(chǎng)景:當(dāng)我們已經(jīng)預(yù)渲染了A頁(yè)面,且用戶一直沒(méi)有訪問(wèn)。某個(gè)時(shí)刻這個(gè)頁(yè)面做了更新,即重新發(fā)布了,那么如果用戶這時(shí)候去訪問(wèn)A頁(yè)面,看到的還是舊的A頁(yè)面。所以這里需要在預(yù)渲染頁(yè)面時(shí),給頁(yè)面設(shè)置一個(gè)過(guò)期時(shí)間,若復(fù)用預(yù)渲染W(wǎng)ebView時(shí)已經(jīng)過(guò)期了,就說(shuō)明WebView已經(jīng)失效了,需要重新loadUrl保證頁(yè)面的實(shí)效性。
狀態(tài)有效性
狀態(tài)有效性就是去校驗(yàn)預(yù)渲染的WebView最終是否有渲染成功,這一點(diǎn)我們可以通過(guò)WebViewClient的生命周期回調(diào)(onPageFinished/onReceivedError)來(lái)進(jìn)行判斷。之所以要做這個(gè)校驗(yàn),是為了防止一開(kāi)始預(yù)渲染就失敗了,卻還是拿這個(gè)WebView去進(jìn)行展示。比如,假設(shè)我們?cè)诰W(wǎng)絡(luò)異常狀態(tài)下去進(jìn)行了預(yù)渲染,在網(wǎng)絡(luò)恢復(fù)正常后用戶訪問(wèn)預(yù)渲染頁(yè)面,若不進(jìn)行狀態(tài)校驗(yàn),那么看到的就會(huì)是網(wǎng)絡(luò)異常狀態(tài)下的WebView了。
頁(yè)面顯示狀態(tài)通知
頁(yè)面顯示狀態(tài),通俗來(lái)講就是頁(yè)面現(xiàn)在是離屏的,還是上屏的。在h5的一些業(yè)務(wù)場(chǎng)景中,有一部分是需要感知到頁(yè)面的顯示狀態(tài)的。比如引導(dǎo)類的動(dòng)畫(huà),比如會(huì)場(chǎng)的一些倒計(jì)時(shí)等等。所以我們需要將頁(yè)面的顯示狀態(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)來(lá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ù)渲染離屏加載了頁(yè)面,所以頁(yè)面的初始化行為是需要納入評(píng)估的,只有評(píng)估通過(guò)后,才能放入預(yù)渲染白名單中。具體的初始化行為包括但不限于:業(yè)務(wù)的曝光埋點(diǎn)、前端邏輯(如倒計(jì)時(shí)、跨天活動(dòng))、消費(fèi)型(如首次引導(dǎo))、后端流量評(píng)估、頁(yè)面在后臺(tái)是否會(huì)有聲音、是否會(huì)彈框(系統(tǒng)框、權(quán)限框、對(duì)話框...)等等。
預(yù)渲染存在哪些局限性?
- 低端機(jī)內(nèi)存空間有限,預(yù)渲染白名單的實(shí)際配置數(shù)量需要視情況進(jìn)行調(diào)整。
- 預(yù)渲染頁(yè)面必須經(jīng)過(guò)白名單配置。頁(yè)面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ù)渲染頁(yè)面的實(shí)效性無(wú)法保證。預(yù)渲染頁(yè)面一旦重新部署,端上是不能立刻感知到并重新加載的。按上面的預(yù)渲染時(shí)機(jī),目前只有以下二個(gè)場(chǎng)景會(huì)觸發(fā)端上對(duì)預(yù)渲染頁(yè)面的更新:
- 1.冷啟動(dòng);
- 2.頁(yè)面被訪問(wèn)后關(guān)閉;
- 3.業(yè)務(wù)調(diào)用接口主動(dòng)注入。
所以大家如果有比較好的方案歡迎分享給我呀!
- 命中率。預(yù)渲染頁(yè)面是不能百分百命中的,即即使我們把某個(gè)頁(yè)面配置進(jìn)了預(yù)渲染白名單,app也有可能沒(méi)預(yù)渲染上這個(gè)頁(yè)面。有很多異常場(chǎng)景會(huì)影響到命中率,比如:
- 1.上面講到的預(yù)渲染的時(shí)間有效性與狀態(tài)有效性;
- 2.服務(wù)端下發(fā)的預(yù)渲染白名單沒(méi)有及時(shí)拉取到;
- 3.主線程一直繁忙,導(dǎo)致預(yù)渲染邏輯一直沒(méi)執(zhí)行;
- 4.內(nèi)存不夠,將緩存的預(yù)渲染W(wǎng)ebView回收掉了。
總結(jié)
所以雖然預(yù)渲染能從表面上實(shí)現(xiàn)h5頁(yè)面的秒開(kāi),但也不是萬(wàn)能的,是存在一些缺陷的(否則也不需要?jiǎng)e的優(yōu)化手段了)。但我認(rèn)為是諸多優(yōu)化手段中比較簡(jiǎn)單卻又能立竿見(jiàn)影的一個(gè)手段,特別是對(duì)本身h5頁(yè)面加載就非常慢的app而言。所以如果還沒(méi)做起來(lái)的同學(xué)可以試一試,后面再結(jié)合其它優(yōu)化的手段抹除不足。今天就講到這里啦。
到此這篇關(guān)于Android WebView預(yù)渲染介紹的文章就介紹到這了,更多相關(guān)Android WebView預(yù)渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android編程實(shí)現(xiàn)基于局域網(wǎng)udp廣播自動(dòng)建立socket連接的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)基于局域網(wǎng)udp廣播自動(dòng)建立socket連接的方法,涉及Android使用udp廣播實(shí)現(xiàn)socket通訊的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Android自定義控件實(shí)現(xiàn)通用驗(yàn)證碼輸入框(二)
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)通用驗(yàn)證碼輸入框的第二篇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01實(shí)例講解Android中的AutoCompleteTextView自動(dòng)補(bǔ)全組件
AutoCompleteTextView組件被用在輸入框中能實(shí)現(xiàn)輸入內(nèi)容自動(dòng)補(bǔ)全的功能,類似于大家平時(shí)用Google時(shí)的輸入聯(lián)想,這里我們來(lái)用實(shí)例講解Android中的AutoCompleteTextView自動(dòng)補(bǔ)全組件,特別是實(shí)現(xiàn)郵箱地址補(bǔ)全的例子,非常實(shí)用2016-05-05Android仿硅谷商城實(shí)現(xiàn)購(gòu)物車實(shí)例代碼
這篇文章主要介紹了Android購(gòu)物車編輯實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,一起跟隨小編過(guò)來(lái)看看吧2018-05-05一文詳解Jetpack?Android新一代導(dǎo)航管理Navigation
這篇文章主要為大家介紹了Jetpack?Android新一代導(dǎo)航管理Navigation詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03Android運(yùn)用onTouchEvent自定義滑動(dòng)布局
這篇文章主要為大家詳細(xì)介紹了Android運(yùn)用onTouchEvent寫(xiě)一個(gè)上下滑動(dòng)的布局,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03Android自定義View實(shí)現(xiàn)九宮格圖形解鎖(Kotlin版)
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)九宮格圖形解鎖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09