使用OkHttp+DiskLrucache实现自定义web页面缓存

对于安卓的WebView页面缓存,可以通过WebSetting的setAppCachePath+setCacheMode的方式实现,native实现代码很简单,如下:

// 开启 Application Caches 功能
webSettings.setAppCacheEnabled(true);
String appCachePath = mContext.getDir("webAppCache", Context.MODE_PRIVATE).getPath();
webSettings.setAppCachePath(appCachePath);
// 默认就是LOAD_DEFAULT,所以是LOAD_DEFAULT,则下面一句可不写
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);

当你写完这几句代码之后,可能你认为缓存的功能已经万事大吉了。但是实际情况却是,当你浏览了一个web页面之后,再查看下生成的目录发现,该目录下竟然一个页面都没有缓存下来,具体情况可以通过查看data/data/包名下定义的缓存目录可查。
为什么会这样呢?这篇文章可以解答我们的疑惑:Html5利用AppCache和LocalStorage实现缓存h5页面数据
也就是说我们要缓存的文件需要配合前端一块处理。我经过调研后发现ios似乎不支持这种方式,并且指定页面只要有文件有改动,前端的manifest配置文件就相应的需要改动,整个过程不太灵活,所以前端也就不愿意配合了。
所以,既然如此,我们能不能做个简单的缓存功能呢?
我的思路是,对于预知不会改变的文件,可以在页面加载期间,通过拦截url的方式,根据自定义的规则,动态缓存起来,以后再请求该文件时,则首先判断本地有无该缓存文件,有则直接返回本地资源,否则就默认网络加载,并拦截缓存。
web缓存使用场景代码如下:

class MyWebViewClient extends WebViewClient implements JSInvokeNative.ExitListener {
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            ......
            InputStream is = WebCacheManager.INSTANCE.getCache(activity, url);
            if (is != null) {
                return new WebResourceResponse(WebCacheManager.INSTANCE.getMimeType(url), "UTF-8", is);
            }
            return super.shouldInterceptRequest(view, url);
        }
}

web缓存逻辑代码如下:

object WebCacheManager {
    private const val MAX_SIZE = 100 * 1024 * 1024L //100MB
    private const val cacheDir = "WebCacheDir"
    private val okHttpClient by lazy { OkHttpClient() }
    private val imageSuffix = arrayOf(".png", ".jpg", ".jpeg")
    // 配置缓存指定域名下的文件
    private val hosts = arrayOf("xxx.xxx.com", "xxx.xxx.com")
    // 经过确认,指定域名下的这些格式的文件是不会变的
    private val fileSuffix = arrayOf(".css", ".js")
    private var diskLruCache: DiskLruCache? = null

    /**
     * DiskLruCache初始化
     */
    private fun initDiskLruCache(context: Context) {
        if (diskLruCache == null || diskLruCache!!.isClosed) {
            try {
                val cacheDir = getDiskCacheDir(context)
                if (!cacheDir.exists()) {
                    cacheDir.mkdirs()
                }
                //初始化DiskLruCache
                diskLruCache = DiskLruCache.create(FileSystem.SYSTEM, cacheDir, 1, 1, MAX_SIZE)
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    /**
     * 设置缓存路径,优先sd卡,没有则使用内置路径
     */
    private fun getDiskCacheDir(context: Context): File {
        if (MobileUtil.isSdExist()) {
            val fileDir = context.getExternalFilesDir(cacheDir)
            if (fileDir != null) {
                return fileDir
            }
        }
        return File(context.filesDir.absolutePath, cacheDir)
    }

    /**
     * 获取缓存,若无缓存则通过OkHttp+DiskLruCache缓存文件
     */
    fun getCache(context: Context, url: String): InputStream? {
        if (!canCache(url)) return null
        val result: InputStream? = try {
            initDiskLruCache(context)
            val snapshot = diskLruCache!!.get(hashKeyForDisk(url))
            if (snapshot == null) {
                null
            } else {
                val source = snapshot.getSource(0)
                val buffer = Buffer()
                var len = 0L
                while (len != -1L) {
                    len = source.read(buffer, 4 * 1024)
                }
                //获取到buffer的inputStream对象
                buffer.inputStream()
            }
        } catch (e: IOException) {
            null
        }

        if (result == null) {
            initDiskLruCache(context)
            okHttpClient.newCall(Request.Builder().url(url).build()).enqueue(object : Callback {
                override fun onFailure(call: Call?, e: IOException?) = Unit

                override fun onResponse(call: Call, response: Response) {
                    if (response.isSuccessful) {
                        val key = hashKeyForDisk(url)
                        writeToDisk(response.body(), url, diskLruCache!!.edit(key), key)
                    }
                }
            })
        }
        return result
    }

    /**
     * 缓存非空,且非本地文件,且为图片格式,或指定域名下的指定文件格式
     */
    private fun canCache(url: String): Boolean {
        if (TextUtils.isEmpty(url)) return false
        val uri = Uri.parse(url)
        if ("file" == uri.scheme) return false
        val lastPath = uri.lastPathSegment
        if (TextUtils.isEmpty(lastPath)) return false
        if (imageSuffix.any { lastPath!!.endsWith(it) }) return true
        if (!hosts.contains(uri.host)) return false
        return fileSuffix.any { lastPath!!.endsWith(it) }
    }

    /**
     * DiskLruCache缓存
     */
    private fun writeToDisk(body: ResponseBody?, imageUrl: String, editor: DiskLruCache.Editor?, key: String) {
        if (body == null) return
        if (editor == null) return
        val sink = editor.newSink(0)
        L.e("writeToDisk url ---> $imageUrl\tkey=${hashKeyForDisk(imageUrl)}")

        var inputStream: InputStream? = null
        val buffer = Buffer()
        var isSuccess = false
        try {
            val byteArray = ByteArray(4 * 1024)
            inputStream = body.byteStream()
            while (true) {
                val read = inputStream.read(byteArray)
                if (read == -1) {
                    break
                }
                buffer.write(byteArray, 0, read)
                sink.write(buffer, read.toLong())
                buffer.clear()
            }
            isSuccess = true
        } catch (e: IOException) {
            if (MobileUtil.isDebug()) {
                e.printStackTrace()
            }
            isSuccess = false
            L.e("${imageUrl}${e.printStackTrace()}")
        } finally {
            buffer.clear()
            buffer.close()
            inputStream?.close()
            sink.flush()
            sink.close()

            if (!isSuccess) {
                L.e("${imageUrl}下载不完整,已删除")
                diskLruCache!!.remove(key)
            } else {
                editor.commit()
            }
        }
    }

    /**
     * web加载文件的类型
     */
    fun getMimeType(url: String): String {
        if (TextUtils.isEmpty(url)) return "text/html"
        val uri = Uri.parse(url)
        val lastPath = uri.lastPathSegment
        if (TextUtils.isEmpty(lastPath)) return "text/html"
        return if (lastPath!!.endsWith(".png")) {
            "image/x-png"
        } else if (lastPath.endsWith(".jpg") || lastPath.endsWith(".jpeg")) {
            "image/jpeg"
        } else if (lastPath.endsWith(".css")) {
            "text/css"
        } else {
            "text/javascript"
        }
    }

    /**
     * 将url转成md5
     */
    private fun hashKeyForDisk(url: String): String {
        return try {
            val mDigest = MessageDigest.getInstance("MD5")
            val md5bytes = mDigest.digest(url.toByteArray(charset("UTF-8")))
            ByteString.of(*md5bytes).hex()
        } catch (e: NoSuchAlgorithmException) {
            url.hashCode().toString()
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352