Android 缓存浅谈(二) DiskLruCache
上篇文章讲解了使用LruCache策略在内存中缓存图片,如果你还未了解,请先看Android 缓存浅谈(一) LruCache。
在Android应用开发中,为了提高UI的流畅性、响应速度,提供更高的用户体验,开发者常常会绞尽脑汁地思考如何实现高效加载图片,而DiskLruCache实现正是开发者常用的图片缓存技术之一。Disk LRU Cache,顾名思义,硬件缓存,就是一个在文件系统中使用有限空间进行高速缓存。每个缓存项都有一个字符串键和一个固定大小的值。
今天这篇文章 是讲解使用DiskLruCache做二级缓存。DiskLruCache是用来做磁盘缓存的良药。关于更加仔细的描述,请看郭神的这篇文章,Android DiskLruCache完全解析,硬盘缓存的最佳方案。本篇文章是讲解自己对DiskLruCache的认识。
一、下载DiskLruCache。
DiskLruCache虽然得到了Google认证,但是SDK中还没有加入,所以我们使用DiskLruCache需要自己去下载,下载地址(该地址需要FQ,并且经常打不开),因此,我们可以在JakeWharton/DiskLruCache下载DiskLruCache源码(DiskLruCache是JakeWharton大神的杰作),另外,我提供了jar包,jar包下载地址。
二、DiskLruCache使用。
2.1、创建DiskLruCache。
DiskLruCache不能通过构造方法来创建,它通过open方法来穿件自身。
- public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
参数说明:
(1). File directory 指定数据的缓存地址。获取路径详见2.1.1。
(2). int appVersion 指定当前应用程序的版本号。当appVersion改变时,之前的缓存都会被清除,所以如非必要,我们为其指定一个1。获取应用程序的版本号详见2.1.2。
(3). int valueCount 是Key所对应的文件数,我们通常选择一一对应的简单关系,这样比较方便控制,当然我们也可以一对多的关系,通常写入1,表示一一对应的关系。
(4). long maxSize 缓存大小。
2.1.1、设置DiskLruCache的存储路径。
- public static 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);
- }
PS:
- Context.getExternalCacheDir().getPath(); 获取到的是 /sdcard/Android/data/<application package>/cache 这个路径
- Context.getCacheDir().getPath(); 获取到的是 /data/data/<application package>/cache 这个路径
建议尽可能将缓存数据保存到/sdcard/Android/data/<application package>/cache 这个路径,好处是,当应用卸载时,该目录底下的数据会清空。更多 有关存储路径,可以查看这篇文章, Android File(一) 存储以及File操作介绍。
2.1.2、获取应用程序的版本号。
- public static int getAppVersion(Context context) {
- try {
- return context.getPackageManager().getPackageInfo(
- context.getPackageName(), 0).versionCode;
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
- return 1;
- }
2.2、添加到缓存。
将图片缓存到磁盘,需要使用DiskLruCache.Editor对象。Editor 表示一个缓存对象的编辑对象。同样Editor也不是new出来的,是通过DiskLruCache获取的。
- public DiskLruCache.Editor edit(String key) throws IOException {
- return this.edit(key, -1L);
- }
首先获取图片 url 所对应的 key,然后根据 key 通过 editor() 来获取 Editor 对象,如果这个缓存正在被编辑,那么 editor()会返回 null,即 DiskLruCache 不允许同时编辑一个缓存对象。
对于每一个存储资源都需要有一个key,这个key要是唯一的,并且和数据一一对应。现在我们存放的是图片,自然会想到了图片独一无二的url。图片Url路径,可能包含一些特殊字符,不符合文件名的标准,无法直接命名为文件名,如果将给这个url进行编码,让其比较规整,推荐使用MD5编码。下面展示 MD5代码,(该代码是网上的一段示例代码)
- public static 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 static 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();
- }
下面,看看如何编写具体的写入缓存代码,(该代码是网上的代码片段)
首先看看addBitmapToDiskLruCache()方法,
- /**
- * 该代码需要在子线程中进行 将图片添加到磁盘缓存
- * @param imageUrl 图片的下载地址
- * @param diskLruCache 缓存对象
- * @return
- */
- public static boolean addBitmapToDiskLruCache(String imageUrl,
- DiskLruCache diskLruCache) {
- boolean result = false;
- String key = Md5Utils.hashKeyForDisk(imageUrl); // 通过md5加密了这个URL,生成一个key
- try {
- Editor editor = diskLruCache.edit(key);// 产生一个editor对象
- if (editor != null) {
- // 创建一个新的输出流 ,创建DiskLruCache时设置一个节点只有一个数据,所以这里的index直接设为0即可
- OutputStream outputStream = editor.newOutputStream(0);
- // 通过地址获取图片数据写入到输出流
- if (DownLoadBitmapUtils.downloadUrlToStream(imageUrl,
- outputStream)) {
- // 写入成功,提交
- editor.commit();
- result = true;
- } else {
- // 写入失败,中止
- editor.abort();
- result = false;
- }
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- return result;
- }
首先把传递参数即图片Url,通过Md5生成一个key,然后得到一个editor对象,接着获取editor对象的输出流,通过地址获取图片数据写入到该输出流,如果返回‘true’,则调用editor.commit();提交,否则,调用editor.abort();中止此次操作。
接下来看看downloadUrlToStream()方法,
- /**
- * 建立HTTP请求,并获取图片流对象。
- * @param urlString 图片下载路径
- * @param outputStream Editor的输出流
- * @return
- */
- public static 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;
- }
获取图片的流写入到参数输出流中。代码都有注释,多看几遍,相信就能理解。
2.3、读取缓存。
取出磁盘缓存需要使用到Snapshot对象。同样Snapshot 也不是new出来的,是通过DiskLruCache获取的。
首先需要将URL转换成Key,然后通过DiskLruCache的get方法得到一个Snapshot对象,在通过Snapshot对象得到缓存文件的输入流,再把输入流转换成Bitamp对象。
- public synchronized DiskLruCache.Snapshot get(String key) throws IOException
具体代码如下所示,
- /**该代码需要在子线程中进行
- * 从缓存中获取Bitmap对象
- *
- * @param imageUrl
- * @return
- */
- public static Bitmap getCacheBitmap(String imageUrl,DiskLruCache diskLruCache) {
- String key = Md5Utils.hashKeyForDisk(imageUrl);// 把Url转换成KEY
- try {
- DiskLruCache.Snapshot snapShot = diskLruCache.get(key);// 通过key获取Snapshot对象
- if (snapShot != null) {
- InputStream is = snapShot.getInputStream(0);// 通过Snapshot对象获取缓存文件的输入流
- Bitmap bitmap = BitmapFactory.decodeStream(is);// 把输入流转换成Bitmap对象
- return bitmap;
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
读取的时候我们最先拿到的是一个Snapshot 对象,再根据我们之前传入的参数0拿到缓存文件的流,最后把流转换为图片。到这里大家可能就明白了,之前的editor.newOutputStream(0);方法为什么会有一个0的参数了,相当于一个标识,读取时也传入参数0才能读到我们想要的数据。(假如我们的key与缓存文件不是一一对应,也就是我们一开始的open方法中传入的不是valueCount的值不是 1,那么一个key对应多个缓存文件我们要怎么区分?就是通过这种方式,有兴趣的同学查看源码就一目了然了)。
2.4、DiskLruCache的其他常用方法。
(1). size(),获取缓存大小。
- /**
- * 获取缓存大小
- * @return
- */
- public long getDiskLruCacheSize(DiskLruCache diskLruCache){
- return diskLruCache.size();
- }
这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位。
(2). remove()和delete(), 清除缓存。
- /**
- * 清除某一个缓存
- *
- * @param diskLruCache
- * @return
- */
- public void clearDiskLruCacheBykey(DiskLruCache diskLruCache,
- String imageUrl) {
- try {
- String key = Md5Utils.hashKeyForDisk(imageUrl);
- diskLruCache.remove(key);
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- /**
- * 清除所有缓存
- *
- * @param diskLruCache
- * @return
- */
- public void clearAllDiskLruCache(DiskLruCache diskLruCache) {
- try {
- diskLruCache.delete();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
(3). flush(), 这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。
- /**
- * 并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间,
- * 推荐在activity的onPause中调用
- * @param diskLruCache
- */
- public void flushDiskLruCache(DiskLruCache diskLruCache) {
- try {
- diskLruCache.flush();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
(4). close(),关闭DiskLruCache。
- /**
- * 这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法
- * @param diskLruCache
- */
- public void closeDiskLruCache(DiskLruCache diskLruCache) {
- try {
- diskLruCache.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。
三、实战。
1.新建Android项目,新建布局文件等等。
以上这几步,更加详细的可以参考 Android 缓存浅谈(一) LruCache。
2. 实现DiskLruCache功能。
这是工程的包以及类截图,重点说明DiskLruCacheUtils类,下面看该类的具体代码,
- package cn.xinxing.test.utils;
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.os.Handler;
- import android.os.Looper;
- import android.os.Message;
- import android.util.Log;
- import android.widget.ImageView;
- import android.widget.ListView;
- import com.jakewharton.disklrucache.DiskLruCache;
- import java.io.File;
- import java.io.IOException;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.Future;
- import cn.xinxing.test.R;
- import cn.xinxing.test.constant.Images;
- import cn.xinxing.test.model.LoaderResult;
- /**
- * 磁盘缓存工具类
- */
- public class DiskLruCacheUtils {
- private DiskLruCache mDiskLruCache;
- private static final int DISK_CACHE_SIZE = 1024 * 1024 * 50; // 磁盘缓存的大小为50M
- private static final String DISK_CACHE_SUBDIR = "bitmap"; // 设置缓存的文件名bitmap
- private static final int APP_VERSION = 1;
- private static final int VALUES_COUNT = 1;
- private static final int CPU_COUNT = Runtime.getRuntime()
- .availableProcessors();
- private static final int CORE_POOL_SIZE = CPU_COUNT + 1;// 核心线程数
- private ExecutorService pool;// 线程池
- private Future future;
- private static final String TAG = "DiskLruCacheUtils";
- public static final int MESSAGE_POST_RESULT = 1;
- private ListView mListView;// ListView的实例
- private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
- @Override
- public void handleMessage(Message msg) {
- LoaderResult result = (LoaderResult) msg.obj;
- ImageView imageView = result.imageView;
- String uri = (String) imageView.getTag();
- if (uri.equals(result.uri)) {
- imageView.setImageBitmap(result.bitmap);
- } else {
- imageView.setImageResource(R.mipmap.ic_launcher);
- }
- }
- ;
- };
- public DiskLruCacheUtils(Context context, ListView listView) {
- mListView = listView;
- pool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
- initDiskLruCache(context);
- }
- /**
- * 初始化
- * @param context
- */
- public void initDiskLruCache(Context context) {
- try {
- File cacheDir = FileUtils.getDiskCacheDir(context, DISK_CACHE_SUBDIR);
- if (!cacheDir.exists()) {
- cacheDir.mkdirs();
- }
- mDiskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, VALUES_COUNT, DISK_CACHE_SIZE);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- /**
- * 显示图片
- *
- * @param imageView
- * @param imageUrl
- */
- public void showImage(final ImageView imageView, final String imageUrl) {
- imageView.setTag(imageUrl);
- // 从缓存中获取
- Runnable loadBitmapTask = new Runnable() {
- @Override
- public void run() {
- if (!Thread.currentThread().isInterrupted()) {
- Bitmap bitmap = loadBitmapFromDishLruCache(imageUrl);
- if (bitmap != null) {
- LoaderResult result = new LoaderResult(imageView, imageUrl,
- bitmap);
- mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
- .sendToTarget();
- }
- }
- }
- };
- future = pool.submit(loadBitmapTask);
- }
- /**
- * 加载Bitmap对象。
- *
- * @param start 第一个可见的ImageView的下标
- * @param end 最后一个可见的ImageView的下标
- */
- public void showIamges(int start, int end) {
- for (int i = start; i < end; i++) {
- String imageUrl = Images.imageUrls[i];
- //从缓存中取图片
- ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl);
- loadImage(imageUrl, imageView);
- }
- }
- /**
- * 加载图片
- * @param imageUrl 图片的下载路径
- * @param imageView
- */
- public void loadImage(final String imageUrl, final ImageView imageView) {
- imageView.setTag(imageUrl);
- // 从缓存中获取
- Runnable loadBitmapTask = new Runnable() {
- @Override
- public void run() {
- if (!Thread.currentThread().isInterrupted()) {
- Bitmap bitmap = loadBitmap(imageUrl);
- if (bitmap != null) {
- LoaderResult result = new LoaderResult(imageView, imageUrl,
- bitmap);
- mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
- .sendToTarget();
- }
- Log.e(TAG,"---->Thread run");
- }
- }
- };
- future = pool.submit(loadBitmapTask);
- }
- /**
- * 取消所有任务
- */
- public void cancelAllTask() {
- future.cancel(true);
- }
- /**
- * 从缓存中加载图片
- * @param imageUrl
- * @return
- */
- private Bitmap loadBitmapFromDishLruCache(String imageUrl) {
- Bitmap bitmap;
- //从缓存中获取
- bitmap = BitmapCacheUtils.getCacheBitmap(imageUrl, mDiskLruCache);
- return bitmap;
- }
- /**
- * 获取图片
- * @param imageUrl
- * @return
- */
- private Bitmap loadBitmap(String imageUrl) {
- Bitmap bitmap;
- //从缓存中获取
- bitmap = BitmapCacheUtils.getCacheBitmap(imageUrl, mDiskLruCache);
- if (bitmap != null) {
- return bitmap;
- }
- try {
- bitmap = loadBitmapFromHttp(imageUrl);
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- return bitmap;
- }
- /**
- * 磁盘缓存的添加
- *
- * @param imageUrl
- * @return
- * @throws IOException
- */
- private Bitmap loadBitmapFromHttp(String imageUrl) throws IOException {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- throw new RuntimeException("can not visit network from UI Thread.");
- }
- if (mDiskLruCache == null) {
- return null;
- }
- if (BitmapCacheUtils.addBitmapToDiskLruCache(imageUrl, mDiskLruCache)) {
- return BitmapCacheUtils.getCacheBitmap(imageUrl, mDiskLruCache);
- }
- return null;
- }
- }
首先初始化,创建缓存目录以及线程池,然后加载图片时,先从缓存中获取(要在子线程中进行),如果缓存中有,则显示图片,如果没有则去下载并加入到缓存中,然后从缓存中获取,再显示。
使用DiskLruCacheUtils时,使用了线程池机制,因为在列表中可能会同时加载多个图片,如果只是一直创建线程,那么对app的性能以及体验都是考验,所以,建议使用线程池机制。
有关线程池,请参考这篇文章Android(线程二) 线程池详解 。
PS:代码下载连接!
总结:
DiskLruCache的使用比LruCache稍微复杂一点,但是这一点都不影响它的性能。目前市面上大部分涉及缓存的App以及开源项目例如Android-Universal-Image-Loader,都有DiskLruCache的影子,所以值得推荐。本篇中的大部分代码都是从网上直接复制的,不要发明重复的轮子。
!
PS: 参考文章: 详细解读DiskLruCache。

浙公网安备 33010602011771号