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>')") {}

数据哪里来呢?这里以列表页跳转详情页举个例子,仅供参考:

  • 列表页接口返回列表数据的时候带上详情内容,跳转详情页的时候带上内容数据。长处简略粗犷,毛病消耗流量。

当然还有其余办法这里不再多说,可依据本人的理论需要进行抉择。

学习资源举荐:《2022最新Android中高级面试题汇总》

这份完整版的《2022最新Android中高级面试题汇总》PDF版电子书,点这里能够看到全部内容。或者点击 【这里】 查看获取形式。

Thanks

以上就是本篇文章的全部内容,如有问题欢送指出,咱们一起提高。 如果喜爱的话心愿点个赞吧,您的激励是我后退的能源。 谢谢~~