Full of WebView optimized dry goods, let your H5 realize the second opening experience.

What is the difference between WebView and native?

Baidu APP pictures are cited here to illustrate.

Baidu developers divided the whole process into four stages and counted the average time-consuming of each stage.

It can be seen that it took 260 ms to initialize the component, and the average time for the first creation was 500 ms. there is no doubt that this is the first point we want to optimize. The most time-consuming is text loading & rendering and image loading. Why is it so time-consuming? These two phases require multiple network requests, JS calls, and IO reads and writes. So this is where we need to optimize.

The optimization direction can be obtained:

  • WebView pre creation and reuse
  • Rendering optimization (JS, CSS, pictures)
  • Template optimization (splitting, preheating, reuse)

WebView pre creation and reuse

The creation of WebView is time-consuming. The first creation takes hundreds of milliseconds, so pre creation and reuse are particularly important.
The general logic is to first create a WebView and cache it, and then directly take it out when necessary. The code is as follows:

class WebViewManager private constructor() {

    // Omit some of the code for the reading experience

    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()
        }
    }

}

Here we need to pay attention to the following points:

  • Selection of preload timing

The creation of WebView is time-consuming. In order to ensure that the preloading will not affect the current main thread task, we select IdleHandler to perform the pre creation to ensure that it will not affect the current main thread task. See the prepare(context: Context) method for details.

  • Context selection

Each WebView needs to be bound to the corresponding Activity Context instance. In order to ensure the consistency between the preloaded WebView Context and the final Context, we use MutableContextWrapper to solve this problem.

MutableContextWrapper allows external replacement of its baseContext, so the prepare(context: Context) method can be passed to applicationContext for pre creation, and the replacement can be carried out when the actual call is made. See the obtain(context: Context) method for details.

  • Reuse and destruction

recycle(webView: WebView) is called before the page closes, and destroy() is destroyed before the application is exited.

  • Reuse WebView return blank

When calling recycle(webView: WebView) for recycling, we will call loaddatawithbaseurl (null, ",", "text / HTML", "UTF-8", null) to clear the page content, resulting in the blank page at the bottom of the loading stack during reuse. Therefore, we need to judge the bottom of the stack when returning. If it is empty, we will return directly. The code is as follows:

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 the bottom of the stack is not a link, it will be returned directly
        if (currentHost.isNullOrBlank()) return false
    }
    return canBack
}

Rendering optimization (JS, CSS, pictures)

WebView will make multiple network requests, JS calls and IO reads and writes when loading content. We can use the shouldInterceptRequest callback of the kernel to intercept resource requests, download them by the client, and fill them into the WebResourceResponse of the kernel in the form of pipeline. Baidu APP pictures are cited here to illustrate.

  • Preset offline package

Simplify and extract public JS and CSS files as general resources, store the extracted resources under assets, and then match them through agreed rules. The code is as follows:

webView.webViewClient = object : WebViewClient() {

    // Omit some of the code for the reading experience

    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? {

    // Omit some of the code for the reading experience
    
    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
}
  • Interface update cache resources

In addition to the preset offline package, we can also request through the interface to obtain the latest cache resources, and self cache through the type of requested resources. The code is as follows:

webView.webViewClient = object : WebViewClient() {

    // Omit some of the code for the reading experience

    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 {

    // Omit some of the code for the reading experience

    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? {

    // Omit some of the code for the reading experience
    
    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 {
                        // Open network request to download resources
                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
}

We use canCacheResource(webRequest: WebResourceRequest) to determine whether it is a resource to be cached.
Then obtain the files in the cache according to the URL, otherwise open the network request to download resources. For details, see cacheResourceRequest(context: Context, webRequest: WebResourceRequest).
Here, only images, fonts, CSS, JS and JSON are cached. More types of resources can be cached according to the actual situation of the project.

Template optimization (splitting, preheating, reuse)

For the template, the code is as follows:

<!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>

The client loads the template code (warm tip: the above is only an example, and the actual template is split according to the situation). After loading, call the JS method to inject data.

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

Where does the data come from? Here, take the list page to jump to the details page as an example for reference only:

  • When the list page interface returns the list data, it will bring the detail content, and when it jumps to the detail page, it will bring the content data. The advantages are simple and crude, while the disadvantages consume traffic.

Of course, there are other methods, which can be selected according to their actual needs.

Learning Resource Recommendation: summary of the latest Android intermediate and advanced interview questions in 2022

This full version of the PDF e-book "summary of the latest Android intermediate and advanced interview questions in 2022", Click here to see the whole content . Or click[ here ]View the acquisition method.

Thanks

The above is the whole content of this article. If you have any questions, please point them out and make progress together. If you like it, I hope you will like it. Your encouragement is my driving force. Thank you~~

Keywords: Android Interview Programmer

Added by bokehman on Tue, 18 Jan 2022 09:46:12 +0200