DiskLruCache详解 From GuoLin Blogs.

做者:郭霖老师,《第一行代码》做者,开源框架LitePal做者java

http://blog.csdn.net/guolin_blog/article/details/28863651android

 

概述

记得在很早以前,我有写过一篇文章 Android高效加载大图、多图解决方案,有效避免程序OOM ,这篇文章是翻译自Android Doc的,其中防止多图OOM的核心解决思路就是使用LruCache技术。但LruCache只是管理了内存中图片的存储与释放,若是图片从内存中被移除的话,那么又须要从网络上从新加载一次图片,这显然很是耗时。对此,Google又提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但得到官方认证)。只惋惜,Android Doc中并无对DiskLruCache的用法给出详细的说明,而网上关于DiskLruCache的资料也少之又少,所以今天我准备专门写一篇博客来详细讲解DiskLruCache的用法,以及分析它的工做原理,这应该也是目前网上关于DiskLruCache最详细的资料了。缓存

那么咱们先来看一下有哪些应用程序已经使用了DiskLruCache技术。在我所接触的应用范围里,Dropbox、Twitter、网易新闻等都是使用DiskLruCache来进行硬盘缓存的,其中Dropbox和Twitter大多数人应该都没用过,那么咱们就从你们最熟悉的网易新闻开始着手分析,来对DiskLruCache有一个最初的认识吧。网络

初探

相信全部人都知道,网易新闻中的数据都是从网络上获取的,包括了不少的新闻内容和新闻图片,以下图所示:app

可是不知道你们有没有发现,这些内容和图片在从网络上获取到以后都会存入到本地缓存中,所以即便手机在没有网络的状况下依然可以加载出之前浏览过的新闻。而使用的缓存技术不用多说,天然是DiskLruCache了,那么首先第一个问题,这些数据都被缓存在了手机的什么位置呢?框架

其实DiskLruCache并无限制数据的缓存位置,能够自由地进行设定,可是一般状况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<application package>/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,所以即便缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一块儿被清除掉,这样就不会出现删除程序以后手机上还有不少残留数据的问题。ide

那么这里仍是以网易新闻为例,它的客户端的包名是com.netease.newsreader.activity,所以数据缓存地址就应该是 /sdcard/Android/data/com.netease.newsreader.activity/cache ,咱们进入到这个目录中看一下,结果以下图所示:学习

能够看到有不少个文件夹,由于网易新闻对多种类型的数据都进行了缓存,这里简单起见咱们只分析图片缓存就好,因此进入到bitmap文件夹当中。而后你将会看到一堆文件名很长的文件,这些文件命名没有任何规则,彻底看不懂是什么意思,但若是你一直向下滚动,将会看到一个名为journal的文件,以下图所示:gradle

那么这些文件到底都是什么呢?看到这里相信有些朋友已是一头雾水了,这里我简单解释一下。上面那些文件名很长的文件就是一张张缓存的图片,每一个文件都对应着一张图片,而journal文件是DiskLruCache的一个日志文件,程序对每张图片的操做记录都存放在这个文件中,基本上看到journal这个文件就标志着该程序使用DiskLruCache技术了。ui

下载

好了,对DiskLruCache有了最初的认识以后,下面咱们来学习一下DiskLruCache的用法吧。因为DiskLruCache并非由Google官方编写的,因此这个类并无被包含在Android API当中,咱们须要将这个类从网上下载下来,而后手动添加到项目当中。DiskLruCache的源码在Google Source上,地址以下:

android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

若是Google Source打不开的话,也能够 点击这里 下载DiskLruCache的源码。下载好了源码以后,只须要在项目中新建一个libcore.io包,而后将DiskLruCache.java文件复制到这个包中便可。

打开缓存

这样的话咱们就把准备工做作好了,下面看一下DiskLruCache到底该如何使用。首先你要知道,DiskLruCache是不能new出实例的,若是咱们要建立一个DiskLruCache的实例,则须要调用它的open()方法,接口以下所示:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key能够对应多少个缓存文件,基本都是传1,第四个参数指定最多能够缓存多少字节的数据。

其中缓存地址前面已经说过了,一般都会存放在 /sdcard/Android/data/<application package>/cache 这个路径下面,但同时咱们又须要考虑若是这个手机没有SD卡,或者SD正好被移除了的状况,所以比较优秀的程序都会专门写一个方法来获取缓存地址,以下所示:

public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); }

能够看到,当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,不然就调用getCacheDir()方法来获取缓存路径。前者获取到的就是 /sdcard/Android/data/<application package>/cache 这个路径,然后者获取到的是 /data/data/<application package>/cache 这个路径。

接着又将获取到的路径和一个uniqueName进行拼接,做为最终的缓存路径返回。那么这个uniqueName又是什么呢?其实这就是为了对不一样类型的数据进行区分而设定的一个惟一值,好比说在网易新闻缓存路径下看到的bitmap、object等文件夹。

接着是应用程序版本号,咱们可使用以下代码简单地获取到当前应用程序的版本号:

public int getAppVersion(Context context) {
		try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 1; }

须要注意的是,每当版本号改变,缓存路径下存储的全部数据都会被清除掉,由于DiskLruCache认为当应用程序有版本更新的时候,全部的数据都应该从网上从新获取。

后面两个参数就没什么须要解释的了,第三个参数传1,第四个参数一般传入10M的大小就够了,这个能够根据自身的状况进行调节。

所以,一个很是标准的open()方法就能够这样写:

DiskLruCache mDiskLruCache = null;
try {
	File cacheDir = getDiskCacheDir(context, "bitmap"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); }

首先调用getDiskCacheDir()方法获取到缓存地址的路径,而后判断一下该路径是否存在,若是不存在就建立一下。接着调用DiskLruCache的open()方法来建立实例,并把四个参数传入便可。

有了DiskLruCache的实例以后,咱们就能够对缓存的数据进行操做了,操做类型主要包括写入、访问、移除等,咱们一个个进行学习。

写入缓存

先来看写入,好比说如今有一张图片,地址是http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg,那么为了将这张图片下载下来,就能够这样写:

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
	HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; }

这段代码至关基础,相信你们都看得懂,就是访问urlString中传入的网址,并经过outputStream写入到本地。有了这个方法以后,下面咱们就可使用DiskLruCache来进行写入了,写入的操做是借助DiskLruCache.Editor这个类完成的。相似地,这个类也是不能new的,须要调用DiskLruCache的edit()方法来获取实例,接口以下所示:

public Editor edit(String key) throws IOException

能够看到,edit()方法接收一个参数key,这个key将会成为缓存文件的文件名,而且必需要和图片的URL是一一对应的。那么怎样才能让key和图片的URL可以一一对应呢?直接使用URL来做为key?不太合适,由于图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。其实最简单的作法就是将图片的URL进行MD5编码,编码后的字符串确定是惟一的,而且只会包含0-F这样的字符,彻底符合文件的命名规则。

那么咱们就写一个方法用来将字符串进行MD5编码,代码以下所示:

public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }

代码很简单,如今咱们只须要调用一下hashKeyForDisk()方法,并把图片的URL传入到这个方法中,就能够获得对应的key了。

所以,如今就能够这样写来获得一个DiskLruCache.Editor的实例:

String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key);

有了DiskLruCache.Editor的实例以后,咱们能够调用它的newOutputStream()方法来建立一个输出流,而后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能了。注意newOutputStream()方法接收一个index参数,因为前面在设置valueCount的时候指定的是1,因此这里index传0就能够了。在写入操做执行完以后,咱们还须要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃这次写入。

所以,一次完整写入操做的代码以下所示:

new Thread(new Runnable() { @Override public void run() { try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start();

因为这里调用了downloadUrlToStream()方法来从网络上下载图片,因此必定要确保这段代码是在子线程当中执行的。注意在代码的最后我还调用了一下flush()方法,这个方法并非每次写入都必需要调用的,但在这里却不可缺乏,我会在后面说明它的做用。

如今的话缓存应该是已经成功写入了,咱们进入到SD卡上的缓存目录里看一下,以下图所示:

能够看到,这里有一个文件名很长的文件,和一个journal文件,那个文件名很长的文件天然就是缓存的图片了,由于是使用了MD5编码来进行命名的。

读取缓存

缓存已经写入成功以后,接下来咱们就该学习一下如何读取了。读取的方法要比写入简单一些,主要是借助DiskLruCache的get()方法实现的,接口以下所示:

public synchronized Snapshot get(String key) throws IOException

很明显,get()方法要求传入一个key来获取到相应的缓存数据,而这个key毫无疑问就是将图片URL进行MD5编码后的值了,所以读取缓存数据的代码就能够这样写:

String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);

很奇怪的是,这里获取到的是一个DiskLruCache.Snapshot对象,这个对象咱们该怎么利用呢?很简单,只须要调用它的getInputStream()方法就能够获得缓存文件的输入流了。一样地,getInputStream()方法也须要传一个index参数,这里传入0就好。有了文件的输入流以后,想要把缓存图片显示到界面上就垂手可得了。因此,一段完整的读取缓存,并将图片加载到界面上的代码以下所示:

try {
	String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }

咱们使用了BitmapFactory的decodeStream()方法将文件流解析成Bitmap对象,而后把它设置到ImageView当中。若是运行一下程序,将会看到以下效果:

OK,图片已经成功显示出来了。注意这是咱们从本地缓存中加载的,而不是从网络上加载的,所以即便在你手机没有联网的状况下,这张图片仍然能够显示出来。

移除缓存

学习完了写入缓存和读取缓存的方法以后,最难的两个操做你就都已经掌握了,那么接下来要学习的移除缓存对你来讲也必定很是轻松了。移除缓存主要是借助DiskLruCache的remove()方法实现的,接口以下所示:

public synchronized boolean remove(String key) throws IOException

相信你已经至关熟悉了,remove()方法中要求传入一个key,而后会删除这个key对应的缓存图片,示例代码以下:

try {
	String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); mDiskLruCache.remove(key); } catch (IOException e) { e.printStackTrace(); }

用法虽然简单,可是你要知道,这个方法咱们并不该该常常去调用它。由于你彻底不须要担忧缓存的数据过多从而占用SD卡太多空间的问题,DiskLruCache会根据咱们在调用open()方法时设定的缓存最大值来自动删除多余的缓存。只有你肯定某个key对应的缓存内容已通过期,须要从网络获取最新数据的时候才应该调用remove()方法来移除缓存。

其它API

除了写入缓存、读取缓存、移除缓存以外,DiskLruCache还提供了另一些比较经常使用的API,咱们简单学习一下。

1. size()

这个方法会返回当前缓存路径下全部缓存数据的总字节数,以byte为单位,若是应用程序中须要在界面上显示当前缓存数据的总大小,就能够经过调用这个方法计算出来。好比网易新闻中就有这样一个功能,以下图所示:

2.flush()

这个方法用于将内存中的操做记录同步到日志文件(也就是journal文件)当中。这个方法很是重要,由于DiskLruCache可以正常工做的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操做的时候我有调用过一次这个方法,但其实并非每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增长同步journal文件的时间。比较标准的作法就是在Activity的onPause()方法中去调用一次flush()方法就能够了。

3.close()

这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了以后就不能再调用DiskLruCache中任何操做缓存数据的方法,一般只应该在Activity的onDestroy()方法中去调用close()方法。

4.delete()

这个方法用于将全部的缓存数据所有删除,好比说网易新闻中的那个手动清理缓存功能,其实只须要调用一下DiskLruCache的delete()方法就能够实现了。

解读journal

前面已经提到过,DiskLruCache可以正常工做的前提就是要依赖于journal文件中的内容,所以,可以读懂journal文件对于咱们理解DiskLruCache的工做原理有着很是重要的做用。那么journal文件中的内容究竟是什么样的呢?咱们来打开瞧一瞧吧,以下图所示:

因为如今只缓存了一张图片,因此journal中并无几行日志,咱们一行行进行分析。第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着咱们使用的是DiskLruCache技术。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,咱们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,一般状况下都为1。第五行是一个空行。前五行也被称为journal文件的头,这部份内容仍是比较好理解的,可是接下来的部分就要稍微动点脑筋了。

第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。一般咱们看到DIRTY这个字样都不表明着什么好事情,意味着这是一条脏数据。没错,每当咱们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示咱们正准备写入一条缓存数据,但不知结果如何。而后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,不然这条数据就是“脏”的,会被自动删除掉。

若是你足够细心的话应该还会注意到,第七行的那条记录,除了CLEAN前缀和key以外,后面还有一个152313,这是什么意思呢?其实,DiskLruCache会在每一行CLEAN记录的最后加上该条缓存数据的大小,以字节为单位。152313也就是咱们缓存的那张图片的字节数了,换算出来大概是148.74K,和缓存图片刚恰好同样大,以下图所示:

前面咱们所学的size()方法能够获取到当前缓存路径下全部缓存数据的总字节数,其实它的工做原理就是把journal文件中全部CLEAN记录的字节数相加,求出的总合再把它返回而已。

除了DIRTY、CLEAN、REMOVE以外,还有一种前缀是READ的记录,这个就很是简单了,每当咱们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。所以,像网易新闻这种图片和数据量都很是大的程序,journal文件中就可能会有大量的READ记录。

那么你可能会担忧了,若是我不停频繁操做的话,就会不断地向journal文件中写入数据,那这样journal文件岂不是会愈来愈大?这倒没必要担忧,DiskLruCache中使用了一个redundantOpCount变量来记录用户操做的次数,每执行一次写入、读取或移除缓存的操做,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、没必要要的记录所有清除掉,保证journal文件的大小始终保持在一个合理的范围内。

好了,这样的话咱们就算是把DiskLruCache的用法以及简要的工做原理分析完了。至于DiskLruCache的源码仍是比较简单的, 限于篇幅缘由就不在这里展开了,感兴趣的朋友能够本身去摸索。下一篇文章中,我会带着你们经过一个项目实战的方式来更加深刻地理解DiskLruCache的用法。

相关文章
相关标签/搜索