滿滿的WebView優(yōu)化干貨颅夺,讓你的H5實(shí)現(xiàn)秒開體驗(yàn)。

WebView與原生對比差在哪里蛹稍?

這里引用百度APP圖片來說明吧黄。

6asdasd40.jpg

百度的開發(fā)人員將這一整個過程劃分為了四個階段,并統(tǒng)計(jì)出了各個階段的平均耗時唆姐。

可以看到稚字,在初始化組件階段就花費(fèi)了 260 ms,首次創(chuàng)建耗時均值為 500 ms厦酬,毫無疑問這是我們要優(yōu)化的第一點(diǎn)胆描。而最耗時的當(dāng)屬正文加載&渲染和圖片加載兩個階段。為什么會這么耗時呢仗阅,因?yàn)檫@兩個階段需要進(jìn)行多次網(wǎng)絡(luò)請求昌讲、JS 調(diào)用、IO 讀寫减噪。所以這里也是我們需要優(yōu)化的地方短绸。

可以得出優(yōu)化方向:

  • WebView預(yù)創(chuàng)建和復(fù)用
  • 渲染優(yōu)化(JS、CSS筹裕、圖片)
  • 模板優(yōu)化(拆分醋闭、預(yù)熱、復(fù)用)

WebView預(yù)創(chuàng)建和復(fù)用

WebView 的創(chuàng)建是比較耗時的朝卒,首次創(chuàng)建耗時幾百毫秒证逻,因此預(yù)創(chuàng)建和復(fù)用尤為重要。
大致邏輯是先創(chuàng)建WebView并緩存起來抗斤,等到需要的時候直接取出來囚企,代碼如下:

class WebViewManager private constructor() {

    // 為了閱讀體驗(yàn),省略部分代碼

    private val webViewCache: MutableList<WebView> = ArrayList(1)

    private fun create(context: Context): WebView {
        val webView = WebView(context)
    // ......
        return webView
    }

    fun prepare(context: Context) {
        if (webViewCache.isEmpty()) {
            Looper.myQueue().addIdleHandler {
                webViewCache.add(create(MutableContextWrapper(context)))
                false
            }
        }
    }

    fun obtain(context: Context): WebView {
        if (webViewCache.isEmpty()) {
            webViewCache.add(create(MutableContextWrapper(context)))
        }
        val webView = webViewCache.removeFirst()
        val contextWrapper = webView.context as MutableContextWrapper
        contextWrapper.baseContext = context
        webView.clearHistory()
        webView.resumeTimers()
        return webView
    }

    fun recycle(webView: WebView) {
        try {
            webView.stopLoading()
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            webView.clearHistory()
            webView.pauseTimers()
            webView.webChromeClient = null
            webView.webViewClient = null
            val parent = webView.parent
            if (parent != null) {
                (parent as ViewGroup).removeView(webView)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            if (!webViewCache.contains(webView)) {
                webViewCache.add(webView)
            }
        }
    }

    fun destroy() {
        try {
            webViewCache.forEach {
                it.removeAllViews()
                it.destroy()
                webViewCache.remove(it)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}

這里需要注意以下幾點(diǎn):

  • 預(yù)加載時機(jī)的選擇
    WebView 的創(chuàng)建是比較耗時的瑞眼,為了使預(yù)加載不會影響到當(dāng)前主線程任務(wù)龙宏,我們選擇 IdleHandler 來執(zhí)行預(yù)創(chuàng)建,以保證不會影響到當(dāng)前主線程任務(wù)伤疙。詳細(xì)請看 prepare(context: Context) 方法银酗。
  • Context的選擇
    每個 WebView 需要和對應(yīng)的Activity Context 實(shí)例進(jìn)行綁定,為了保證預(yù)加載的 WebView Context 和最終的 Context 之間的一致性,我們通過 MutableContextWrapper 來解決這個問題黍特。

MutableContextWrapper 允許外部替換它的 baseContext 蛙讥,因此 prepare(context: Context)方法可以傳 applicationContext 進(jìn)行預(yù)創(chuàng)建,等到實(shí)際調(diào)用時再進(jìn)行替換衅澈,詳細(xì)請看 obtain(context: Context) 方法键菱。

  • 復(fù)用和銷毀
    在頁面關(guān)閉前調(diào)用 recycle(webView: WebView) 進(jìn)行回收谬墙,在應(yīng)用退出前調(diào)用 destroy() 進(jìn)行銷毀今布。
  • 復(fù)用WebView返回空白
    在調(diào)用 recycle(webView: WebView) 進(jìn)行回收時,我們會調(diào)用 loadDataWithBaseURL(null, "", "text/html", "utf-8", null) 清除頁面內(nèi)容拭抬,導(dǎo)致復(fù)用時的加載棧底就是這個空白頁面部默,所以我們需要在返回時對棧底進(jìn)行判斷,如果為空則直接返回造虎,代碼如下:
fun canGoBack(): Boolean {
    val canBack = webView.canGoBack()
    if (canBack) webView.goBack()
    val backForwardList = webView.copyBackForwardList()
    val currentIndex = backForwardList.currentIndex
    if (currentIndex == 0) {
        val currentUrl = backForwardList.currentItem.url
        val currentHost = Uri.parse(currentUrl).host
        //棧底不是鏈接則直接返回
        if (currentHost.isNullOrBlank()) return false
    }
    return canBack
}

渲染優(yōu)化(JS傅蹂、CSS、圖片)

WebView 在加載內(nèi)容的時候會進(jìn)行多次網(wǎng)絡(luò)請求算凿、JS 調(diào)用份蝴、IO 讀寫。我們可以借由內(nèi)核的 shouldInterceptRequest 回調(diào)氓轰,攔截資源請求由客戶端進(jìn)行下載婚夫,并以管道方式填充到內(nèi)核的 WebResourceResponse中,這里引用百度APP圖片來說明署鸡。

safasfas.jpg
  • 預(yù)置離線包
    精簡并抽取公共的 JS 和 CSS 文件作為通用資源案糙,將抽取的資源存放在 assets 下,再通過約定的規(guī)則去匹配靴庆,代碼如下:
webView.webViewClient = object : WebViewClient() {

    // 為了閱讀體驗(yàn)时捌,省略部分代碼

    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        if (view != null && request != null) {
            if(canAssetsResource(request)){
                return assetsResourceRequest(view.context, request)
            }
        }
        return super.shouldInterceptRequest(view, request)
    }
}
private fun assetsResourceRequest(
    context: Context, 
    webRequest: WebResourceRequest
): WebResourceResponse? {

    // 為了閱讀體驗(yàn),省略部分代碼
    
    try {
        val url = webRequest.url.toString()
        val filenameIndex = url.lastIndexOf("/") + 1
        val filename = url.substring(filenameIndex)
        val suffixIndex = url.lastIndexOf(".")
        val suffix = url.substring(suffixIndex + 1)
        val webResourceResponse = WebResourceResponse()
        webResourceResponse.mimeType = getMimeTypeFromUrl(url)
        webResourceResponse.encoding = "UTF-8"
        webResourceResponse.data = context.assets.open("$suffix/$filename")
        return webResourceResponse
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}
  • 接口更新緩存資源
    除了預(yù)置離線包外炉抒,我們還可以通過接口請求奢讨,獲取最新的緩存資源,以及通過請求資源的類型自行緩存焰薄,代碼如下:
webView.webViewClient = object : WebViewClient() {

    // 為了閱讀體驗(yàn)禽笑,省略部分代碼

    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        if (view != null && request != null) {
            if(canCacheResource(request)){
                return cacheResourceRequest(view.context, request)
            }
        }
        return super.shouldInterceptRequest(view, request)
    }
}
private fun canCacheResource(webRequest: WebResourceRequest): Boolean {

    // 為了閱讀體驗(yàn),省略部分代碼

    val url = webRequest.url.toString()
    val extension = getExtensionFromUrl(url)
    return extension == "ico" || extension == "bmp" || extension == "gif"
            || extension == "jpeg" || extension == "jpg" || extension == "png"
            || extension == "svg" || extension == "webp" || extension == "css"
            || extension == "js" || extension == "json" || extension == "eot"
            || extension == "otf" || extension == "ttf" || extension == "woff"
}
private fun cacheResourceRequest(
    context: Context, 
    webRequest: WebResourceRequest
): WebResourceResponse? {

    // 為了閱讀體驗(yàn)蛤奥,省略部分代碼
    
    try {
        val url = webRequest.url.toString()
        val cachePath = CacheUtils.getCacheDirPath(context, "web_cache")
        val filePathName = cachePath + File.separator + url.encodeUtf8().md5().hex()
        val file = File(filePathName)
        if (!file.exists() || !file.isFile) {
            runBlocking {
                        // 開啟網(wǎng)絡(luò)請求下載資源
                download(HttpRequest(url).apply {
                    webRequest.requestHeaders.forEach { putHeader(it.key, it.value) }
                }, filePathName)
            }
        }
        if (file.exists() && file.isFile) {
            val webResourceResponse = WebResourceResponse()
            webResourceResponse.mimeType = getMimeTypeFromUrl(url)
            webResourceResponse.encoding = "UTF-8"
            webResourceResponse.data = file.inputStream()
            webResourceResponse.responseHeaders = mapOf("access-control-allow-origin" to "*")
            return webResourceResponse
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}

我們通過canCacheResource(webRequest: WebResourceRequest)來判斷是否是需要緩存的資源佳镜。
再根據(jù)URL去獲取緩存中文件,否則開啟網(wǎng)絡(luò)請求下載資源凡桥,詳細(xì)請看 cacheResourceRequest(context: Context, webRequest: WebResourceRequest) 蟀伸。
這邊僅對圖片、字體、CSS啊掏、JS蠢络、JSON進(jìn)行緩存,可根據(jù)項(xiàng)目實(shí)際情況緩存更多類型資源迟蜜。

模板優(yōu)化(拆分刹孔、預(yù)熱、復(fù)用)

關(guān)于模板娜睛,代碼如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>title</title>
        <link rel="stylesheet" type="text/css" href="xxx.css">
    <script>
        function changeContent(data){
            document.getElementById('content').innerHTML=data;
        }
    </script>
</head>
<body>
    <div id="content"></div>
</body>
</html>

客戶端加載模板代碼(溫馨提示:上面只是例子髓霞,實(shí)際模板根據(jù)情況拆分),加載完成后再調(diào)用 JS 方法注入數(shù)據(jù)畦戒。

webView.evaluateJavascript("javascript:changeContent('<p>我是HTML</p>')") {}

數(shù)據(jù)哪里來呢方库?這里以列表頁跳轉(zhuǎn)詳情頁舉個例子,僅供參考:

  • 列表頁接口返回列表數(shù)據(jù)的時候帶上詳情內(nèi)容障斋,跳轉(zhuǎn)詳情頁的時候帶上內(nèi)容數(shù)據(jù)纵潦。優(yōu)點(diǎn)簡單粗暴,缺點(diǎn)耗費(fèi)流量垃环。
    當(dāng)然還有其他方法這里不再多說邀层,可根據(jù)自己的實(shí)際需求進(jìn)行選擇。

CDN 加速遂庄、DNS 等其他優(yōu)化

網(wǎng)上資料很多這里就不搬運(yùn)了寥院。??

示例代碼

Thanks

以上就是本篇文章的全部內(nèi)容,如有問題歡迎指出涧团,我們一起進(jìn)步只磷。
如果喜歡的話希望點(diǎn)個贊吧,您的鼓勵是我前進(jìn)的動力泌绣。
謝謝~~

參考

本文內(nèi)容參考或引用以下文章钮追,推薦大家閱讀會有更多的收獲。

Android WebView H5 秒開方案總結(jié)
百度APP-Android H5首屏優(yōu)化實(shí)踐
今日頭條品質(zhì)優(yōu)化 - 圖文詳情頁秒開實(shí)踐

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末阿迈,一起剝皮案震驚了整個濱河市元媚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苗沧,老刑警劉巖刊棕,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異待逞,居然都是意外死亡甥角,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門识樱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗤无,“玉大人震束,你說我怎么就攤上這事〉狈福” “怎么了垢村?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嚎卫。 經(jīng)常有香客問我嘉栓,道長,這世上最難降的妖魔是什么拓诸? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任侵佃,我火速辦了婚禮,結(jié)果婚禮上恰响,老公的妹妹穿的比我還像新娘趣钱。我一直安慰自己涌献,他們只是感情好胚宦,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著燕垃,像睡著了一般枢劝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卜壕,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天您旁,我揣著相機(jī)與錄音,去河邊找鬼轴捎。 笑死鹤盒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侦副。 我是一名探鬼主播侦锯,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秦驯!你這毒婦竟也來了尺碰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤译隘,失蹤者是張志新(化名)和其女友劉穎亲桥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體固耘,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡题篷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厅目。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片番枚。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡偿枕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出户辫,到底是詐尸還是另有隱情渐夸,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布渔欢,位于F島的核電站墓塌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奥额。R本人自食惡果不足惜苫幢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垫挨。 院中可真熱鬧韩肝,春花似錦、人聲如沸九榔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哲泊。三九已至剩蟀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間切威,已是汗流浹背育特。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留先朦,地道東北人缰冤。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像喳魏,于是被迫代替她去往敵國和親棉浸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容