Android 缓存浅谈(二) DiskLruCache

     上篇文章讲解了使用LruCache策略在内存中缓存图片,如果你还未了解,请先看Android 缓存浅谈(一) LruCache

     在Android应用开发中,为了提高UI的流畅性、响应速度,提供更高的用户体验,开发者常常会绞尽脑汁地思考如何实现高效加载图片,而DiskLruCache实现正是开发者常用的图片缓存技术之一。Disk LRU Cache,顾名思义,硬件缓存,就是一个在文件系统中使用有限空间进行高速缓存。每个缓存项都有一个字符串键和一个固定大小的值。

     今天这篇文章 是讲解使用DiskLruCache做二级缓存。DiskLruCache是用来做磁盘缓存的良药。关于更加仔细的描述,请看郭神的这篇文章,Android DiskLruCache完全解析,硬盘缓存的最佳方案。本篇文章是讲解自己对DiskLruCache的认识。

一、下载DiskLruCache。

      DiskLruCache虽然得到了Google认证,但是SDK中还没有加入,所以我们使用DiskLruCache需要自己去下载,下载地址(该地址需要翻墙,并且经常打不开),因此,我们可以在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




  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值