美文网首页Android开发经验谈Android开发
深入理解Android缓存机制(三)磁盘缓存

深入理解Android缓存机制(三)磁盘缓存

作者: Android高级架构 | 来源:发表于2020-08-25 10:05 被阅读0次

    概述

    磁盘存储有两种形式,一种是File存储,一种是DB(DataBase)存储。

    File

    File存储比较常见,当我们数据量较小,数据的分类以及检索没有较大的要求的时候,可以采用File存储

    File存在的问题:

    • 文件较大时,对文件的读取速度较慢
    • 定位,读写具体的数据较为困难

    DataBase

    对数据的并发性和检索速度有高要求的时候,这个时候,DB就上场了,DB具有如下特点

    • 大数据访问速度更快
    • 索引特定条件的数据较为方便

    Http缓存机制

    相对于内存缓存而言,磁盘时效性很低,所以通常单独的磁盘缓存没有太大意义,每次去读缓存之前需要判断一下懁促是否有效,必须要结合HTTP的缓存机制来做一些处理,这样缓存才会比较有效,所以下面还是先介绍一下HTTP缓存机制,将从缓存存储策略缓存过期策略缓存对比策略三个方面来分析一下Http的缓存机制。

    缓存存储策略

    用来确定 Http 响应内容是否可以被客户端缓存,以及可以被哪些客户端缓存

    对于 Cache-Control 头里的 Public、Private、no-cache、max-age 、no-store 他们都是用来指明响应内容是否可以被客户端存储的,其中前4个都会缓存文件数据(关于 no-cache 应理解为“不建议使用本地缓存”,其仍然会缓存数据到本地),后者 no-store 则不会在客户端缓存任何响应数据。另关于 no-cache 和 max-age 有点特别,我认为它是一种混合体,下面我会讲到。

    通过 Cache-Control:Public 设置我们可以将 Http 响应数据存储到本地,但此时并不意味着后续浏览器会直接从缓存中读取数据并使用,为啥?因为它无法确定本地缓存的数据是否可用(可能已经失效),还必须借助一套鉴别机制来确认才行, 这就是我们下面要讲到的“缓存过期策略”。

    缓存过期策略

    客户端用来确认存储在本地的缓存数据是否已过期,进而决定是否要发请求到服务端获取数据


    刚上面我们已经阐述了数据缓存到了本地后还需要经过判断才能使用,那么浏览器通过什么条件来判断呢? 答案是:Expires,Expires 指名了缓存数据有效的绝对时间,告诉客户端到了这个时间点(比照客户端时间点)后本地缓存就作废了,在这个时间点内客户端可以认为缓存数据有效,可直接从缓存中加载展示。

    不过 Http 缓存头设计并没有想象的那么规矩,像上面提到的 Cache-Control(这个头是在Http1.1里加进来的)头里的 no-cache 和 max-age 就是特例,它们既包含缓存存储策略也包含缓存过期策略,以 max-age 为例,他实际上相当于:

    Cache-Control:public/private
    Expires:当前客户端时间 + maxAge 。
    

    而 Cache-Control:no-cache 和 Cache-Control:max-age=0 (单位是秒)

    这里需要注意的是:

    1. Cache-Control 中指定的缓存过期策略优先级高于 Expires,当它们同时存在的时候,后者会被覆盖掉。
    2. 缓存数据标记为已过期只是告诉客户端不能再直接从本地读取缓存了,需要再发一次请求到服务器去确认,并不等同于本地缓存数据从此就没用了,有些情况下即使过期了还是会被再次用到,具体下面会讲到。

    缓存对比策略

    将缓存在客户端的数据标识发往服务端,服务端通过标识来判断客户端 缓存数据是否仍有效,进而决定是否要重发数据。

    客户端检测到数据过期或浏览器刷新后,往往会重新发起一个 http 请求到服务器,服务器此时并不急于返回数据,而是看请求头有没有带标识( If-Modified-Since、If-None-Match)过来,如果判断标识仍然有效,则返回304告诉客户端取本地缓存数据来用即可(这里要注意的是你必须要在首次响应时输出相应的头信息(Last-Modified、ETags)到客户端)。至此我们就明白了上面所说的本地缓存数据即使被认为过期,并不等于数据从此就没用了的道理了。

    Android中的磁盘缓存

    很多时候我们都会说一些图片加载框架使用了两级或者三级缓存,然后就会说先从内存中取,然后再从磁盘中取,最后再从网络中去取,我们现在按照这个思路来分析一下Picasso,Picasso一开始就从内存中去读,然后就会去进行网络请求,如果内存中没有读取到,他就会去生成一个Request,去请求网络数据,他为什么没有直接去读磁盘缓存,这个时候你可能会说,Picasso默认没有设置磁盘缓存,只要当设置了OkHttp.Downloader之后才会进行磁盘缓存,实际上不是这样的,Picasso是有磁盘缓存的,因为他的缓存依赖于HTTP缓存机制,所以每次是在请求之后根据Response的响应头去看是否读取内存缓存,当Picasso在build的时候,如果没有设置DownLoader,他会自己去设置一个Downloader

      public Picasso build() {
        Context context = this.context;
        if (downloader == null) {
          //创建一个默认的Downloader
          downloader = Utils.createDefaultDownloader(context);
        }
        return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
            defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
      }
    }
    

    继续查看createDefaultDownloader,如果没有OkhttpDownloader,那么就会采用UrlConnectionDownloader

    static Downloader createDefaultDownloader(Context context) {
      try {
        Class.forName("com.squareup.okhttp.OkHttpClient");
        return OkHttpLoaderCreator.create(context);
      } catch (ClassNotFoundException ignored) {
      }
      return new UrlConnectionDownloader(context);
    }
    

    然后我们就分开查看,因为OkHttpLoader是在UrlConnectionDownloader的基础上进行改良的,所以我们先查看一下UrlConnectionDownloader,也就是load方法

    UrlConnectionDownloader

    @Override 
    public Response load(Uri uri, int networkPolicy) throws IOException {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        installCacheIfNeeded(context);
      }
      HttpURLConnection connection = openConnection(uri);
      connection.setUseCaches(true);
      if (networkPolicy != 0) {
        String headerValue;
        if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
          headerValue = FORCE_CACHE;
        } else {
          StringBuilder builder = CACHE_HEADER_BUILDER.get();
          builder.setLength(0);
          if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
            builder.append("no-cache");
          }
          if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
            if (builder.length() > 0) {
              builder.append(',');
            }
            builder.append("no-store");
          }
          headerValue = builder.toString();
        }
        //设置缓存策略
        connection.setRequestProperty("Cache-Control", headerValue);
      }
    
      int responseCode = connection.getResponseCode();
      if (responseCode >= 300) {
        connection.disconnect();
        throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
            networkPolicy, responseCode);
      }
    long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
      //我们根据服务端返回的Response的Header来判断是走缓存还是重新取数据
    boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
      return new Response(connection.getInputStream(), fromCache, contentLength);
    }
    

    紧接着看一下UrlConnectionDownloader

    OkHttpDownloader

    @Override 
    public Response load(Uri uri, int networkPolicy) throws IOException {
      CacheControl cacheControl = null;
      if (networkPolicy != 0) {
        if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
          cacheControl = CacheControl.FORCE_CACHE;
        } else {
          CacheControl.Builder builder = new CacheControl.Builder();
          if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
            builder.noCache();
          }
          if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
            builder.noStore();
          }
          cacheControl = builder.build();
        }
      }
    
      Request.Builder builder = new Request.Builder().url(uri.toString());
      if (cacheControl != null) {
        //设置缓存策略
        builder.cacheControl(cacheControl);
      }
      com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();
      int responseCode = response.code();
      if (responseCode >= 300) {
        response.body().close();
        throw new ResponseException(responseCode + " " + response.message(), networkPolicy,
            responseCode);
      }
     //是否读取缓存的标志
      boolean fromCache = response.cacheResponse() != null;
      ResponseBody responseBody = response.body();
      return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
    }
    

    存储目录

    在开发Android的过程中,也会涉及到很多的IO操作,比如说网络请求,下载图片等,由于很多框架平时已经帮我们封装好了,所以平时容易忽略,下面简单分析一下Android下的存储目录:

    内部存储

    data文件夹就是我们常说的内部存储,对于没有root的手机来说,我们是没有权限打开这个文件夹的但是可以访问到,

    外部存储

    外部存储才是我们平时操作最多的,外部存储一般就是我们上面看到的storage文件夹,当然也有可能是mnt文件夹,这个名称不影响我们操作数据。

    路径获取

    两种存储方式都是通过Context类来进行获取的

    内部存储

       getFilesDir();//获取内部存储的File路径
       getCacheDir();//获取内部存储的Cache路径
       getDatabasePath("demo.db");//获取database路径
       getSharedPreferences("demo",MODE_PRIVATE);//获取SP
    

    外部存储

       getExternalCacheDir();//获取外部存储私有目录
       getExternalFilesDir(Environment.DIRECTORY_DCIM);//获取外部存储公有目录
    
    

    清除缓存/清除数据

    清除缓存:缓存是程序运行时的临时存储空间,它可以存放从网络下载的临时图片,从用户的角度出发清除缓存对用户并没有太大的影响,但是清除缓存后用户再次使用该APP时,由于本地缓存已经被清理,所有的数据需要重新从网络上获取,注意:为了在清除缓存的时候能够正常清除与应用相关的缓存,请将缓存文件存放在getCacheDir()或者 getExternalCacheDir()路径下。 清除数据:清除用户配置,比如SharedPreferences、数据库等等,这些数据都是在程序运行过程中保存的用户配置信息,清除数据后,下次进入程序就和第一次进入程序时一样

    关于权限

    Android6.0以后,谷歌加强了对用户权限的控制,但是这个权限只是针对于外部存储的公有目录,对于私有目录的,仍然可以正常访问。所以当遇到有些手机权限很难适配的时候可以把文件存储在外部存储的私有目录。

    总结

    磁盘缓存在Android中需要注意访问外部存储时候需要权限,注意各个不同路径下的区别,同时需要结合Http缓存注意缓存的时效性。

    作者:wustor
    链接:https://juejin.im/post/6844903559285178381


    更多系列教程GitHub白嫖入口:https://github.com/Timdk857/Android-Architecture-knowledge-2-

    B站全套Android移动架构师进阶视频教程白嫖地址:https://space.bilibili.com/544650554

    相关文章

      网友评论

        本文标题:深入理解Android缓存机制(三)磁盘缓存

        本文链接:https://www.haomeiwen.com/subject/webnjktx.html