WebView與原生對比差在哪里?
這里參考百度APP圖片來說明,

百度的開發人員將這一整個程序劃分為了四個階段,并統計出了各個階段的平均耗時,
可以看到,在初始化組件階段就花費了 260 ms,首次創建耗時均值為 500 ms,毫無疑問這是我們要優化的第一點,而最耗時的當屬正文加載&渲染和圖片加載兩個階段,為什么會這么耗時呢,因為這兩個階段需要進行多次網路請求、JS 呼叫、IO讀寫,所以這里也是我們需要優化的地方,
可以得出優化方向:
- WebView預創建和復用
- 渲染優化(JS、CSS、圖片)
- 模板優化(拆分、預熱、復用)
WebView預創建和復用
WebView 的創建是比較耗時的,首次創建耗時幾百毫秒,因此預創建和復用尤為重要,
大致邏輯是先創建WebView并快取起來,等到需要的時候直接取出來,代碼如下:
class WebViewManager private constructor() {
// 為了閱讀體驗,省略部分代碼
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()
}
}
}
這里需要注意以下幾點:
- 預加載時機的選擇
WebView 的創建是比較耗時的,為了使預加載不會影響到當前主執行緒任務,我們選擇 IdleHandler 來執行預創建,以保證不會影響到當前主執行緒任務,詳細請看 prepare(context: Context) 方法,
- Context的選擇
每個 WebView 需要和對應的 Activity Context 實體進行系結,為了保證預加載的 WebView Context 和最終的 Context 之間的一致性,我們通過 MutableContextWrapper 來解決這個問題,
MutableContextWrapper 允許外部替換它的 baseContext ,因此 prepare(context: Context)方法可以傳 applicationContext進行預創建,等到實際呼叫時再進行替換,詳細請看 obtain(context: Context) 方法,
- 復用和銷毀
在頁面關閉前呼叫 recycle(webView: WebView) 進行回收,在應用退出前呼叫 destroy() 進行銷毀,
- 復用WebView回傳空白
在呼叫 recycle(webView: WebView) 進行回收時,我們會呼叫 loadDataWithBaseURL(null, "", "text/html", "utf-8", null)清除頁面內容,導致復用時的加載堆疊底就是這個空白頁面,所以我們需要在回傳時對堆疊底進行判斷,如果為空則直接回傳,代碼如下:
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
}
渲染優化(JS、CSS、圖片)
WebView 在加載內容的時候會進行多次網路請求、JS 呼叫、IO 讀寫,我們可以借由內核的 shouldInterceptRequest 回呼,攔截資源請求由客戶端進行下載,并以管道方式填充到內核的 WebResourceResponse中,這里參考百度APP圖片來說明,

- 預置離線包
精簡并抽取公共的 JS 和 CSS 檔案作為通用資源,將抽取的資源存放在 assets 下,再通過約定的規則去匹配,代碼如下:
webView.webViewClient = object : WebViewClient() {
// 為了閱讀體驗,省略部分代碼
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? {
// 為了閱讀體驗,省略部分代碼
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
}
- 介面更新快取資源
除了預置離線包外,我們還可以通過介面請求,獲取最新的快取資源,以及通過請求資源的型別自行快取,代碼如下:
webView.webViewClient = object : WebViewClient() {
// 為了閱讀體驗,省略部分代碼
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 {
// 為了閱讀體驗,省略部分代碼
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? {
// 為了閱讀體驗,省略部分代碼
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 {
// 開啟網路請求下載資源
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)來判斷是否是需要快取的資源,
再根據URL去獲取快取中檔案,否則開啟網路請求下載資源,詳細請看 cacheResourceRequest(context: Context, webRequest: WebResourceRequest) ,
這邊僅對圖片、字體、CSS、JS、JSON進行快取,可根據專案實際情況快取更多型別資源,
模板優化(拆分、預熱、復用)
關于模板,代碼如下:
<!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>
客戶端加載模板代碼(溫馨提示:上面只是例子,實際模板根據情況拆分),加載完成后再呼叫 JS 方法注入資料,
webView.evaluateJavascript("javascript:changeContent('<p>我是HTML</p>')") {}
資料哪里來呢?這里以串列頁跳轉詳情頁舉個例子,僅供參考:
- 串列頁介面回傳串列資料的時候帶上詳情內容,跳轉詳情頁的時候帶上內容資料,優點簡單粗暴,缺點耗費流量,
當然還有其他方法這里不再多說,可根據自己的實際需求進行選擇,
CDN 加速、DNS 等其他優化
網上資料很多這里就不搬運了,😏
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/397403.html
標籤:其他
上一篇:安卓模擬器怎么切換IP地址
