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

概述

磁盘存储有两种形式,一种是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下的存储目录:


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缓存注意缓存的时效性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 0. 前言 前面有被用户投诉 APP 流量消耗厉害: 于是乎考虑了流量方面的问题。暂时 APP 中涉及流量的几个方...
    zyl06阅读 23,904评论 5 62
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 理论总结 它要解决什么样的问题? 数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因...
    jiangmo阅读 2,833评论 0 11
  • 今天在补刘润五分钟,我很喜欢刘润老师的专栏,所以也都会很认真的听,今天听到一篇值得分享的内容,题目为事实有真假,观...
    Gzw丶南山阅读 590评论 0 0