WebView與原生對比差在哪里蛹稍?
這里引用百度APP圖片來說明吧黄。
百度的開發(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圖片來說明署鸡。
- 預(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)了寥院。??
示例代碼
- github: fragmject
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í)踐