上一节中已经运用Lru算法实现了内存缓存,在从桌面回到前台时可以快速的从内存中进行加载图片,但是如果应用被系统回收或人为的主动清除这样还是会从网络加载,所以我们不仅需要缓存在内存中,还要在磁盘中进行缓存,这样如果内存没有就从磁盘中进行读取数据。
这里我们使用google提供的DiskLruCache来实现disk缓存,由于源码过长就不贴了,所有的代码包括图片加载的demo已经上传到github上。我们将DiskLruCache直接拷贝到项目代码中,并将原来的包名libcore.io更改为我们的包名。
DiskLruCache提供了一系列方法,先依次分析一下
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
可以看到将DiskLruCache设为了private,这样我们就不能直接new出对象了,而是直接提供了一个open方法来返回对象
/**
* 根据地址打开缓存空间,如果不存在就创建一个
*
* @param directory 数据缓存地址
* @param appVersion 当前app版本
* @param valueCount 一个key值对应多少个缓存文件 一般传1
* @param maxSize 最多可以缓存多少字节的数据 一般为10M
* @throws java.io.IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
//调用构造函数得到一个cache对象
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
cache.delete();
}
}
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
DiskLruCache提供了一个flush方法,
为了防止频繁的写数据,建议在Activity的onPause时调用一次flush()方法,除此之外我们需要对url进行MD5加密,防止url出现字符不合法的情况,MD5算法在大数据的hash碰撞上性能也比较良好,编码后每个字符都在0到F之间,符合要求。
/**
* 使用MD5算法对传入的key进行加密,以免出现url不合法
* @param key
* @return
*/
public String hashKeyForDisk(String key){
String cacheKey;
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
e.printStackTrace();
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes){
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
builder.append("0");
}
builder.append(hex);
}
return builder.toString();
}
磁盘的缓存路径一般是在内存卡中,但是现在的一些手机可能并没有内存卡,所以我们需要动态的加载存储路径,
/**
* 根据传入的unique返回硬盘缓存地址
*
* @param context
* @param uniqueName
* @return
*/
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
//当有SD卡时
cachePath = context.getExternalCacheDir().getPath();
} else {
//当没有SD卡或SD卡被移除
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
另外有些图片可能会过大比如远超要显示的大小,这样不仅因为图片过大在下载时比较慢,而且容易造成OOM,所以我们需要对图片进行压缩,让其显示匹配手机显示的大小
/**
* 计算目标图片缩放比例
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
//计算高度和宽度对目标高宽的比例
int heightRadio = Math.round(height/reqHeight);
int widthRadio = Math.round(width/reqWidth);
//选择高宽中较小的一个作为压缩比例,保证图片比目标尺寸大
inSampleSize = heightRadio < widthRadio ? heightRadio : widthRadio;
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmap(FileDescriptor descriptor,int reqWidth,int reqHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
//加载时将injustdecodebounds设置为true,获取图片大小
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(descriptor, null, options);
//计算压缩比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(descriptor, null, options);
}
在将图片写入disk缓存中时,我们需要获取一个DiskLruCache.Editor的editor来将流写入缓存中
@Override
protected Bitmap doInBackground(String... params) {
imageUrl = params[0];
FileDescriptor descriptor = null;
FileInputStream inputStream = null;
Snapshot snapshot = null;
try {
//生成图片对应的key
String key = imageLoader.hashKeyForDisk(imageUrl);
snapshot = imageLoader.diskCache.get(key);
if (snapshot == null) {
//在磁盘中没有找到缓存文件,去网络下载,并写入到缓存中
DiskLruCache.Editor editor = imageLoader.diskCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0); //因为是1对1,直接设为0,就是取第一个
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
}else {
editor.abort(); //取消本次写入操作
}
}
snapshot = imageLoader.diskCache.get(key);
}
if (snapshot != null) {
inputStream = (FileInputStream) snapshot.getInputStream(0);
descriptor = inputStream.getFD();
}
//将缓存数据解析成bitmap对象
Bitmap bitmap = null;
if (descriptor != null) {
bitmap = BitmapUtil.decodeSampledBitmap(descriptor, reqWidth, reqHeight);
}
if (bitmap != null) {
//将bitmap加入到内存缓存中
imageLoader.addBitmapToMemoryCache(params[0], bitmap);
}
return bitmap;
} catch (IOException e) {
e.printStackTrace();
}
// 通过url下载图片
Bitmap bitmap = downloadBitmap(params[0]);
if (bitmap != null) {
// 将图片放入内存缓存中
imageLoader.addBitmapToMemoryCache(params[0], bitmap);
}
return bitmap;
}
/**
* 根据url从网上获取流,并写入到output流中
* @param imageUrl
* @param outputStream
* @return
*/
private boolean downloadUrlToStream(String imageUrl,OutputStream outputStream){
HttpURLConnection conn = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
URL url = new URL(imageUrl);
conn = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(conn.getInputStream(), 8*1024);
out = new BufferedOutputStream(outputStream, 8*1024);
int b;
while((b = in.read())!= -1){
out.write(b);
}
return true;
} catch (Exception e) {
e.printStackTrace();
}finally{
if (conn != null) {
conn.disconnect();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
大概步骤就是这样,中间一些细微的非核心代码没有放上来,可以直接去github上进行下载,包含示例demo
地址https://github.com/sheepm/Cache