磁盘缓存-DiskLruCache
来源:http://blog.csdn.net/guolin_blog/article/details/28863651
不知道大家有没有发现,这些内容和图片在从网络上获取到之后都会存入到本地缓存中,因此即使手机在没有网络的情况下依然能够加载出以前浏览过的新闻。而使用的缓存技术不用多说,自然是DiskLruCache了,那么首先第一个问题,这些数据都被缓存在了手机的什么位置呢?其实DiskLruCache并没有限制数据的缓存位置,可以自由地进行设定,但是通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<application package>/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。
当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,否则就调用getCacheDir()方法来获取缓存路径。前者获取到的就是 /sdcard/Android/data/<application package>/cache 这个路径,而后者获取到的是 /data/data/<application package>/cache 这个路径。
由于DiskLruCache并不是由Google官方编写的,所以这个类并没有被包含在Android API当中,我们需要将这个类从网上下载下来,然后手动添加到项目当中。
主活动MainActivity代码: 这里使用到的DiskLruCache类可以在博客文件里下载,直接把这个java类复制到工程里就可以用了
import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.ImageView; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /* LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。 通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<application package>/cache 这个路径。选择在这个位置有两点好处: 第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。 第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。 journal文件是DiskLruCache的一个日志文件,程序对每张图片的操作记录都存放在这个文件中, 基本上看到journal这个文件就标志着该程序使用DiskLruCache技术了。 */ public class MainActivity extends AppCompatActivity { /** * 磁盘缓存对象,创建出这个对象时就会创建journal文件,用来记录数据的信息 */ private DiskLruCache mDiskLruCache; private ImageView img; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); img = (ImageView) MainActivity.this.findViewById(R.id.imageView); createDiskLruCache(this,"bitmap");//创建缓存对象 String imgUrl="http://a.hiphotos.baidu.com/image/h%3D360/sign=968ba75bd2160924c325a41de406359b/a08b87d6277f9e2f277edd051d30e924b899f370.jpg"; Bitmap bitmap = getBitmapToDisk(imgUrl);//从缓存中获取 if(bitmap!=null){ Log.i("tag","从缓存中获取的"); img.setImageBitmap(bitmap); }else{ new DownloadImgToNet().execute(imgUrl);//异步下载并保存 Log.i("tag","下载的"); } removeImgToDisk(imgUrl);//从缓存中删除 } /**===================创建操作================================ * 创建一个缓存的目录 * @param fileName 自定义的缓存的目录名 */ private void createDiskLruCache(Context context,String fileName) { try { File cacheDir = getDiskCacheDir(context, fileName); if (!cacheDir.exists()) { cacheDir.mkdirs();//创建缓存目录 } /** * 打开缓存: * 每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为 * 当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。 * File directory,数据的缓存地址 * int appVersion,指当前应用程序的版本号, * int valueCount,指定同一个key可以对应多少个缓存文件,基本都是传1 * long maxSize 指定最多可以缓存多少字节的数据 */ mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);//这里为10兆 } catch (IOException e) { e.printStackTrace(); } } /** * 获取磁盘缓存目录,根据sd卡的状态设置不同的缓存地址 * @param context 上下文 * @param uniqueName 设置的缓存文件夹名 */ public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { // /sdcard/Android/data/<application package>/cache cachePath = context.getExternalCacheDir().getPath(); } else { // /data/data/<application package>/cache cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } /** * 获取当前应用版本号 */ public int getAppVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return 1; } //有了DiskLruCache的实例之后,我们就可以对缓存的数据进行操作了,操作类型主要包括写入、访问、移除等 /*=======下载与保存============写入缓存操作================================ *使用DiskLruCache来进行写入了,写入的操作是借助DiskLruCache.Editor这个类完成的。类似地, *这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,接口如下所示: */ /** * 将字符串进行MD5编码(全部都是0-F的字符),这里作为图片的命名,保证了唯一性与避免了特殊符号的出现 */ public String strToHexString(String key) { String cacheKey; try { MessageDigest mDigest = MessageDigest.getInstance("MD5"); byte[] bytes=mDigest.digest(key.getBytes()); 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(); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } /** * 通过异步任务下载图片并保存到磁盘 */ class DownloadImgToNet extends AsyncTask<String,Void,Bitmap>{ private Bitmap bitmap; @Override protected Bitmap doInBackground(String... params) { try { String imgUrl=params[0];//获取网络地址 String key = strToHexString(imgUrl);//将地址转变为MD5编码作为图片的命名 DiskLruCache.Editor edit = mDiskLruCache.edit(key);//获取写入数据的对象 if (edit != null) { OutputStream outputStream = edit.newOutputStream(0);//获取mDiskLruCache对应的磁盘流 if (downloadUrlToStream(imgUrl, outputStream)) { edit.commit();//提交才能使写入生效 } else { edit.abort();//表示放弃此次写入 } } mDiskLruCache.flush();//在这里不可缺少 } catch (IOException e) { e.printStackTrace(); } return bitmap; } /** * 写入到mDiskLruCache的磁盘目录中,outputStream代表该目录的流对象 */ private boolean downloadUrlToStream(String imgUrl, OutputStream outputStream) { try { URL url=new URL(imgUrl); HttpURLConnection connection = (HttpURLConnection)url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); boolean result = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);//保存图片到磁盘中 return result; } catch (IOException e) { e.printStackTrace(); } return false; } @Override protected void onPostExecute(Bitmap bitmap) { if(bitmap!=null){ //下载成功,更新UI img.setImageBitmap(bitmap); }else{ Log.i("tag","图片下载失败"); } } } /** * =======获取=============读取缓存里的图片操作================================ */ /** * 根据图片的网络地址从磁盘缓存中获取bitmap图片 */ public Bitmap getBitmapToDisk(String imgUrl){ try { String key = strToHexString(imgUrl);//先转换为MD5编码 DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);//通过key从缓存中获取 if (snapshot != null) { InputStream is = snapshot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); return bitmap; } } catch (IOException e) { e.printStackTrace(); } return null; } /** * =====删除===========移除缓存的操作============================== * 用法虽然简单,但是你要知道,这个方法我们并不应该经常去调用它。 * 因为你完全不需要担心缓存的数据过多从而占用SD卡太多空间的问题, * DiskLruCache会根据我们在调用open()方法时设定的缓存最大值来自动删除多余的缓存。 * 只有你确定某个key对应的缓存内容已经过期,需要从网络获取最新数据的时候才应该调用remove()方法来移除缓存。 */ public void removeImgToDisk(String imgUrl){ try { String key = strToHexString(imgUrl);//转换为MD5编码 mDiskLruCache.remove(key);//从缓存中删除 Log.i("tag","删除成功"); } catch (IOException e) { Log.i("tag","删除失败"); e.printStackTrace(); } } }
切记加上权限: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
其它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文件的大小始终保持在一个合理的范围内。
内存缓存与磁盘缓存相结合才是硬道理:
在真正的项目实战当中如果仅仅是使用硬盘缓存的话,程序是有明显短板的。而如果只使用内存缓存的话,程序当然也会有很大的缺陷。因此,一个优秀的程序必然会将内存缓存和硬盘缓存结合到一起使用. 结合流程:每次加载图片的时候都优先去内存缓存当中读取,当读取不到的时候则回去硬盘缓存中读取,而如果硬盘缓存仍然读取不到的话,就从网络上请求原始数据。不管是从硬盘缓存还是从网络获取,读取到了数据之后都应该添加到内存缓存当中,这样的话我们下次再去读取图片的时候就能迅速从内存当中读取到