技术, Android

安卓 WebView 图片离线缓存方案

有这样一个项目,UI 渲染全部由 WebView 来完成,套个安卓的壳,壳子里面做一些和硬件交互的功能,例如摄像头、麦克风等。WebView 加载的页面走的本地打包的文件。不过 WebView 中的图片等资源走的是网络访问。

为了减少网络访问的流量,以及提升在弱网络或无网络情况下的体验,需要对网络访问的图片进行本地缓存。

原先采用的是 WebView 自带的缓存机制来实现,但并不可靠,于是需要通过拦截网络请求,通过本地缓存干预的方式来实现。具体原理如下:

  1. 通过 shouldInterceptRequest 拦截请求,判断是否是访问网络图片,如果是则进行干预
  2. 取请求地址的 md5 值加图片文件扩展名组成的文件名,拼接 cache 目录获得一个本地资源地址,判断该资源是否存在,若存在则直接返回该资源
  3. 若该资源不存在,说明是首次访问,则将该网络图片下载到该地址下,并返回该资源

具体代码如下:

import android.content.Context
import android.net.http.SslError
import android.webkit.*
import androidx.webkit.WebViewAssetLoader
import java.io.File
import java.io.FileOutputStream
import java.math.BigInteger
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest

class CommonWebClient(context: Context) : WebViewClient() {
    private var assetLoader: WebViewAssetLoader = WebViewAssetLoader.Builder()
            .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
            .build()

    private fun md5(input: String): String {
        return BigInteger(1, MessageDigest.getInstance("MD5").digest(input.toByteArray())).toString(16).padStart(32, '0')
    }

    override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
        return true
    }

    override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
        return request?.url?.let { url ->
            val urlString = url.toString()
            if (!urlString.contains("appassets.androidplatform.net") && urlString.contains("aliyuncs.com")) {
                try {
                    var extension = urlString.substring(urlString.lastIndexOf("."))
                    if (extension.lastIndexOf("?") > -1) {
                        extension = extension.substring(0, extension.lastIndexOf("?"))
                    }
                    val fileName = "${md5(urlString)}${extension}"
                    val file = File(view?.context?.externalCacheDir, fileName)
                    if (!file.exists()) {
                        val conn = URL(urlString).openConnection() as HttpURLConnection
                        conn.connectTimeout = 5000
                        conn.requestMethod = "GET"
                        conn.doInput = true
                        if (conn.responseCode == 200) {
                            val fos = FileOutputStream(file)
                            val buffer = ByteArray(1024)
                            var len = 0
                            while (conn.inputStream.read(buffer).also { len = it } != -1) {
                                fos.write(buffer, 0, len)
                            }
                            conn.inputStream.close()
                            fos.close()
                        }
                    }
                    WebResourceResponse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension), "UTF-8", file.inputStream())
                } catch (ex: Exception) {
                    assetLoader.shouldInterceptRequest(url)
                }
            } else {
                val response = assetLoader.shouldInterceptRequest(url)
                if (url.path?.endsWith(".js") == true && response != null) {
                    response.mimeType = "text/javascript"
                }
                response
            }
        }
    }

    override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
        handler?.proceed()
        super.onReceivedSslError(view, handler, error)
    }
}
您已成功订阅 HADB.ME
真棒!下一步,完成结账以便解锁 HADB.ME
欢迎回来!您已登录成功。
登录失败,请重试。
操作成功!您的账户已全面激活,现在您有所有内容的权限了。
错误!Stripe 结账失败。
成功!您的账单信息已更新。
错误!账单信息更新失败。