Android H5资源缓存,真的不存在了?

1,532 阅读6分钟

Android H5资源缓存,到底存不存在?

这个疑问真的不是为了吸人眼球,而是我在工作中真实遇到的一个疑问。

起因

最近为了提升H5页面加载性能,需要做H5的资源缓存。虽然一直都知道这个东西,也大概知道其原理,但一直没有做过。因为公司已经有轮子了,我就按照文档一步步接入。最后接入成功了,想看下提升效果如何。但最终的数据让我怀疑人生了...接入前与接入后对比,提升的性能微乎其微,几乎可以忽略不计,甚至有时候还不如没接入前呢!!这我还接啥??居然还有那么多人在用这个??网上也有一堆类似的代码说这样能优化性能??Android H5资源缓存,真的不存在了吗?

我开始了此番调查...

调查

Android H5资源缓存,其实可以分为两种,一种是系统WebView自带的缓存;另一种则是我们应用层自己做的缓存(如果没做,那就默认使用WebView缓存)。

之所以做了应用层缓存之后,体验跟做之前没区别,是因为之前在用WebView缓存呀。那这里问题又来了,既然WebView缓存已经有了,应用层缓存也没有比WebView缓存更快,那为什么还要花时间做应用层缓存呢?难道又是一个KPI产物?

其实应用层缓存对比WebView缓存,是存在一些优势的,这些优势让我们(可以考虑)去做应用层缓存。这里我先介绍一下这两者的区别。

WebView缓存机制

大家在网上搜索一下H5资源缓存机制,就可以看到很多文章会比较详细地介绍6种缓存机制。汇总如下图所示:

image.png 图片来源:juejin.cn/post/684490…

但是在我理解看来,WebView的缓存机制只有两种,一种是“浏览器缓存”,一种是“AppCache"。另外的4种(Dom Storage、Web SQL DataBase、IndexedDB、File System),应该只能算作存储机制,而不是缓存机制。

浏览器缓存”其实就是Chrome浏览器内核自身的缓存机制,基于HTTP协议来控制,相应的协议字段为Cache-Control(或 Expires)和 Last-Modified(或 Etag)。

而“AppCache"本身是作为“浏览器缓存”的一种补充,可以在html头中通过manifest属性引入manifest文件,manifest文件中列出需要缓存哪些的静态资源文件。但据说最新标准中已不再支持这种缓存了,所以可以理解为只有“浏览器缓存”一种机制了。

这里介绍得比较简单,如果有感兴趣的同学可以到上面图片的链接里去详细地查阅,我就不拿来凑字数啦~

在本地文件中,我们可以看到浏览器缓存在如下位置:/data/data/pkgName/Cache/WebView中,大差不差,不同机型可能会有稍微变化。

应用层缓存机制

应用层缓存的原理,主要是利用WebViewClient#shouldInterceptRequest方法进行资源请求的拦截。这里的逻辑主要是判断缓存中是否有要请求的静态资源,如果有则直接返回WebResourceResponse,没有的话就进行网络请求,并将结果保存到缓存一份。这里的缓存,可以是内存缓存,也可以是持久化缓存,亦或是两者结合。

这里我给出持久化缓存的一个简单示例:

findViewById<WebView>(R.id.webview).apply {
    webViewClient = object : WebViewClient() {
        override fun onPageFinished(view: WebView, url: String) {
            super.onPageFinished(view, url)
        }

        @TargetApi(21)
        override fun shouldInterceptRequest(
            view: WebView,
            request: WebResourceRequest
        ): WebResourceResponse? {
            return shouldInterceptRequest(view, request.url.toString())
        }

        override fun shouldInterceptRequest(
            view: WebView,
            url: String
        ): WebResourceResponse? {
            Log.d("InterceptRequestTest", "url:$url")
            if (url.endsWith(".js")) {
                kotlin.runCatching {
                    val file = File(externalCacheDir, getFileName(url))
                    if (file.exists()) {
                        Log.d("InterceptRequestTest", "read from cache")
                        val fileInputStream = FileInputStream(file)
                        val data = ByteArray(file.length().toInt())
                        fileInputStream.read(data)
                        fileInputStream.close()
                        return WebResourceResponse(getMimeType(url), "UTF-8", ByteArrayInputStream(data))
                    } else {
                        Log.d("InterceptRequestTest", "read from net")
                        val connection = URL(url).openConnection()
                        val inputStream = connection.getInputStream()
                        val byteArrayOutputStream = ByteArrayOutputStream()
                        val byteArray = ByteArray(1024)
                        var n = 0
                        while ((inputStream.read(byteArray).also { n = it }) != -1) {
                            byteArrayOutputStream.write(byteArray, 0, n)
                        }
                        inputStream.close()
                        // save to local file
                        val result = byteArrayOutputStream.toByteArray()
                        val fileOutputStream = FileOutputStream(file)
                        fileOutputStream.write(result)
                        fileOutputStream.close()
                        return WebResourceResponse(getMimeType(url), "UTF-8", ByteArrayInputStream(result))
                    }
                }
            }
            return super.shouldInterceptRequest(view, url)
        }
    }

shouldInterceptRequest中,我们单独对js资源进行了缓存。那么等下一次访问相同页面的资源时,我们就可以直接拿缓存的数据了。

应用层缓存优势

大家在上面的使用过程中可能已经想到了应用层缓存相比较浏览器缓存而言存在的一些优势,那这里我分享一下我认为的几点优势,也欢迎大家在评论里补充~

  1. 浏览器缓存必须得在第二次访问时页面才会生效,第一次访问起不到效果,首屏渲染时间仍然很慢。但如果是应用层缓存,那就可以做一个后台配置系统,当APP启动后从后台拉取或者推送到APP端,就可做到APP启动后直接加载缓存,当第一次访问页面时就有缓存的优势了。甚至我们可以选择直接把部分重要的资源包预置到APP的Assets目录下,保证这部分资源时肯定会有缓存的。
  2. 浏览器缓存机制无法修改,具有一定的局限性。比如我们无法修改浏览器缓存的文件位置,以及缓存区的大小。在静态资源经过浏览器缓存后,由于不清楚缓存的内部机制,那么缓存了什么东西我们也是无法直接直观地观察出来的。有时候某个资源被淘汰了,我们也不得而知。并且,由于浏览器缓存是基于HTTP协议的,也就意味着只要符合协议的约定,不管什么资源都会缓存进去。像我们上面做的,只对JS文件进行缓存这一逻辑,就比较难加上了。所以应用层缓存在方面可以更精细地控制我们自己的缓存逻辑。
  3. 浏览器缓存必须得在页面初始化完成之后才会去读取缓存资源,应用层缓存可以做到在页面或者Webview初始化的过程中,并行去提前读取缓存资源或者更新缓存资源。这一点我也是看了开源框架VasSonic之后才知道还有这种骚操作,实在佩服!感兴趣的同学可以自行研究一下哈~
  4. 欢迎补充...

结论

Android H5资源缓存,是存在的。但是如果你没有以下需求之一,其实系统自带的浏览器缓存就已经足够使用了:

  1. app首次访问页面就需要利用缓存提升渲染性能
  2. 需要比较精确地控制缓存区域或对缓存文件进行划分、归类、筛选
  3. 需要提前读取缓存或者更新缓存

参考文章

Webview - 缓存机制 & 资源预加载方案

Android WebView缓存机制和性能优化