Android异步加载全解析之引入二级缓存
为啥要二级缓存
前面我们有了一级缓存,为啥还要二级缓存呢?说白了,这就和电脑是一样的,我们电脑有内存和硬盘,内存读取速度快,所以CPU直接读取内存中的数据,但是,内存资源有限,所以我们可以把数据保存到硬盘上,这就是二级缓存,硬盘虽然读取速度慢,但是人家容量大。
Android的缓存技术也是使用了这样一个特性,总的来说,使用二级缓存的方案,就是先从一级缓存——内存中拿,没有的话,再去二级缓存——手机中拿,如果还没有,那就只能去下载了。
有了
DiskLruCache,我们就可以很方便的将一部分内容缓存到手机存储中,做暂时的持久化保存,像我们经常用的一些新闻聚合类App、ZARKER等,基本都利用了
DiskLruCache,浏览过的网页,即使在没有网络的情况下,也可以浏览。
DiskLruCache
配置
DiskLruCache,听名字就知道是
LruCache的兄弟,只不过这个应该是Google的私生子,还没有像
LruCache一样添加到API中,所以我们只能去官网上下载
DiskLruCache的代码,其实也就一个类。下载地址:
https://developer.android.com/samples/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.html#l22
在工程中使用
DiskLruCache非常简单,只需要在项目中新建一个libcore.io的包,并将DiskLruCache.java文件copy过去即可。
初始化
在使用
DiskLruCache之前,我们需要对缓存的目录进行下配置,
DiskLruCache并不需要限定缓存保存的位置,但一般情况下,我们的缓存都保存在缓存目录下: /sdcard/Android/data/package name/cache,当然,如果没有sdcard,那么我们就使用内置存储的缓存区域:/data/data/package name/cache。
在设置好缓存目录后,我们就可以使用DiskLruCache.open方法来创建DiskLruCache:
File cacheDir = getFileCache(context, "disk_caches");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
try {
mDiskCaches = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
private File getFileCache(Context context, String cacheFileName) {
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 + cacheFileName);
}
DiskLruCache.open方法有这样几个参数:
缓存目录
程序版本号:版本更新后,缓存清0
valueCount
缓存大小:随意,但也不能太任性,按字节算
应该不用解释了,唯一值得说的是valueCount这个参数,它是说同一个key可以对应Value的个数,一般都是1,基本没用。最后我们来看看最后返回的:
return new File(cachePath + File.separator + cacheFileName)
这里通过cacheFileName在缓存目录下再创建一个目录是干嘛呢?这个目录是用来对不同的缓存对象进行区分的,例如images、text等等。我们可以通过size()方法来获取所有缓存数据的大小。也可以使用delete()方法来删除所有缓存。
写入缓存
权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
都说了写缓存,那读写权限肯定是不能少了。
DiskLruCache写入缓存与使用SharedPreferences方法类似,需要使用Editor对象:
DiskLruCache.Editor editor = mDiskCaches.edit(key);
传入的key,就是我们需要下载的url地址,例如图片的地址,但是,url经常具有很多非法字符,这些会对我们的解析工作造成很多困难,而且,有时候我们的url地址也是需要保密的,所以我们经常通过MD5来进行url的加密,这样不仅可以加密,而且可以让所有的URL都变为规则的十六进制字符串。下面我们展示一个经典的写入缓存模板代码:
String key = toMD5String(url);
/
DiskLruCache.Editor editor = mDiskCaches.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (getBitmapUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
mDiskCaches.flush();
/
public String toMD5String(String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.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();
}
private static boolean getBitmapUrlToStream(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;
}
这里唯一的需要注意的是,下载的方法与我们之前使用的方法有所不同,主要是为了通用性,DiskLruCache将对应URL的内容以流的形式进行存储,文件名就是MD5加密后的字符串。
读取缓存
读取缓存的方法大家应该也能想到了,自然是调用get方法:
DiskLruCache.Snapshot snapShot = mDiskCaches.get(key);
不过它返回的是DiskLruCache的Snapshot对象。当我们获取到了Snapshot对象,就可以从它里面获取输出流,从而取出缓存的数据:
DiskLruCache.Snapshot snapShot = mDiskCaches.get(key);
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImageView.setImageBitmap(bitmap);
移除缓存
移除缓存,我们可以猜到,我们需要使用remove方法来实现:
mDiskCache.remove(key);
当然,DiskLruCache并不希望我们手动去移除缓存,因为人家用了Lru算法,跟我们在内存中使用的算法一样,该死的时候,它自己会死。
与生命周期绑定
DiskLruCache在使用时,经常与我们的Activity的生命周期进行绑定,例如在onPause()方法中调用flush()方法,将内容与journal日志文件同步,在onDestroy()方法中去调用close()方法结束
DiskLruCache的open。
日志同步
通过前面的方法,我们已经可以缓存一个来自网络的图片了。下面我们进入缓存的文件夹,并查看里面的数据:
我们可以发现,这些文件,就是以MD5命名的缓存文件,它的最后面,有一个journal文件,我们通过cat命令打开:
这里我们选取一类记录,这些记录总是以dirty开头,然后clean,最后read。这个是什么意思呢?第一行dirty代表我们准备开始缓存数据,clean代表我们缓存到数据了,后面的30405代表缓存的大小,最后的read代表进行了读取操作。
看到这里,相信大家已经想起了我们非常熟悉的sqlite,它实际上也是利用文件来进行存储的。DiskLruCache实际上就是模拟了一个简化的sqlite,它的实现机制与sqlite基本类似。
引入二级缓存
ok,我们回到原来的项目,给工程增加二级缓存,导入DiskLruCache的源文件,这里就不讲了。我们在前面一级缓存的基础上,修改下ImageLoaderWithCaches类,创建ImageLoaderWithDoubleCaches类,这里面我们只需要在构造方法中增加对DiskLruCache的初始化,在AsyncTask中,我们来修改二级缓存的逻辑。前面的步骤相同,在取图像的时候都从内存缓存中取,如果取不到,那么在AsyncTask在硬盘缓存中取,如果还取不到,那就去下载,同时,将下载好的图像加入内存缓存,如果硬盘缓存中有,那么就直接加入内存缓存。看起来其实还是非常简单的,只要修改下AsyncTask即可。
package com.imooc.listviewacyncloader;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.ListView;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;
import libcore.io.DiskLruCache;
public class ImageLoaderWithDoubleCaches {
private Set<ASyncDownloadImage> mTasks;
private LruCache<String, Bitmap> mMemoryCaches;
private DiskLruCache mDiskCaches;
private ListView mListView;
public ImageLoaderWithDoubleCaches(Context context, ListView listview) {
this.mListView = listview;
mTasks = new HashSet<>();
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 10;
mMemoryCaches = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
File cacheDir = getFileCache(context, "disk_caches");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
try {
mDiskCaches = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
public void showImage(String url, ImageView imageView) {
Bitmap bitmap = getBitmapFromMemoryCaches(url);
if (bitmap == null) {
imageView.setImageResource(R.drawable.ic_launcher);
} else {
imageView.setImageBitmap(bitmap);
}
}
public Bitmap getBitmapFromMemoryCaches(String url) {
return mMemoryCaches.get(url);
}
public void addBitmapToMemoryCaches(String url, Bitmap bitmap) {
if (getBitmapFromMemoryCaches(url) == null) {
mMemoryCaches.put(url, bitmap);
}
}
public void loadImages(int start, int end) {
for (int i = start; i < end; i++) {
String url = Images.IMAGE_URLS[i];
Bitmap bitmap = getBitmapFromMemoryCaches(url);
if (bitmap == null) {
ASyncDownloadImage task = new ASyncDownloadImage(url);
mTasks.add(task);
task.execute(url);
} else {
ImageView imageView = (ImageView) mListView.findViewWithTag(url);
imageView.setImageBitmap(bitmap);
}
}
}
private File getFileCache(Context context, String cacheFileName) {
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 + cacheFileName);
}
private static boolean getBitmapUrlToStream(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;
}
public void cancelAllTasks() {
if (mTasks != null) {
for (ASyncDownloadImage task : mTasks) {
task.cancel(false);
}
}
}
public String toMD5String(String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.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();
}
public void flushCache() {
if (mDiskCaches != null) {
try {
mDiskCaches.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ASyncDownloadImage extends AsyncTask<String, Void, Bitmap> {
private String url;
public ASyncDownloadImage(String url) {
this.url = url;
}
@Override
protected Bitmap doInBackground(String... params) {
url = params[0];
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
DiskLruCache.Snapshot snapShot = null;
String key = toMD5String(url);
try {
snapShot = mDiskCaches.get(key);
if (snapShot == null) {
DiskLruCache.Editor editor = mDiskCaches.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (getBitmapUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
snapShot = mDiskCaches.get(key);
}
if (snapShot != null) {
fileInputStream = (FileInputStream) snapShot.getInputStream(0);
fileDescriptor = fileInputStream.getFD();
}
Bitmap bitmap = null;
if (fileDescriptor != null) {
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
if (bitmap != null) {
addBitmapToMemoryCaches(params[0], bitmap);
}
return bitmap;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileDescriptor == null && fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
}
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
ImageView imageView = (ImageView) mListView.findViewWithTag(url);
if (imageView != null && bitmap != null) {
imageView.setImageBitmap(bitmap);
}
mTasks.remove(this);
}
}
}
整体代码与之前使用一级缓存的代码基本相同,大家只要在AsyncTask修改一定逻辑就好了。
再次运行程序,与之前使用一级缓存的图相同,这里就不贴了,只是这里在断网后,同样可以加载缓存中的图片。
以上,未完待续,后面我们会进一步优化>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
代码下载地址 http://download.csdn.net/detail/x359981514/8562525