一、前期基础知识储备
缓存策略在Android中有着广泛的使用场景,尤其在图片加载这个场景下,缓存策略更是尤为重要。加载图片是消耗流量的,如果用户加载图片,每次都要从网络中拉取,不但耗费更好的流量,同时加载速度也不是每次都可以接受的。如何避免过多的流量消耗呢?那就是Android中的缓存了。
当程序第一次从网络中加载图片之后,就将其缓存在存储设备上,这样下次使用这张图片就不用再从网络上获取了,这样就为用户节省了流量。很多时候为了提高用户的体验,往往还会把图片在内存中再缓存一份,这样当用户应用打算从网络上请求一张图片时,程序会首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中没有,那就从网络中下载这张图片。因为从内存中加载扸比从存储设备中加载图片要快,所以这样既提高了程序的效率又为用户节约了不必要的流量开销。
上述的三级缓存策略不仅仅适用于图片,也适用于其他文件类型。
缓存策略:包括缓存的添加、获取和删除。不管是内存缓存还是存储设备缓存,它们的缓存大小都是有限制的,因此在使用缓存时总要为缓存指定一个最大的容量。如果当容量满了,但是程序还需要添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存,如何定义缓存的新旧这就是一种策略。
目前常用的一种缓存算法是LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想就是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过三者的完美结合,就可以很方便的实现一个具有很高实用价值的ImageLoader。
二、上代码,具体实现
1.LruCache
LruCache是Android3.1所提供的一个缓存类。为了兼容Android低版本,在使用LruCache时建议使用support-v4兼容包下的LruCache,而不是直接使用Android3.1提供的LruCache。
LruCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式实现存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作。
- 强引用:直接的对象引用;
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
- 弱引用:当一个对象只用弱引用存在时,此对象会随时被gc回收。
LruCache的实现比较简单,以下是使用LruCache来实现内存缓存:
1)LruCache的典型初始化过程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8; // 总容量为当前进程的可用内存的1/8
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//sizeOf方法用以计算缓存对象的大小 比如Bitmap对象的大小
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
上述代码中,只需要提供缓存的总容量大小并重写sizeOf方法即可。sizeOf方法的作用是计算缓存对象的大小,这里的大小单位和总容量的单位一致。对于上面的示例代码来说,总容量大小为当前进程可用内存的1/8,单位为KB。而sizeOf方法则完成了Bitmap对象的大小计算。
2)LruCache的添加、获取和删除:
从LruCache中获取一个缓存对象:mMemoryCache.get(key)
从LruCache中添加一个缓存对象:mMemoryCache.put(key, bitmap)
从LruCache中删除一个缓存对象:remove方法
从Android3.1开始,LruCache就是Android源码的一部分了。
2.DiskLruCache
DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统,从而实现缓存的效果。DiskLruCache得到了Android官方文档的推荐,但它不属于AndroidSDK的一部分,可以从网上找到它的源码,修改后添加到项目中。
1)DiskLruCache的创建:
DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身:
public static DiskLruCache open (File directory, int appVersion, int valueCount, long maxSize)
open方法有四个参数,其中第一个参数表示磁盘缓存在文件系统中的存储路径。缓存路径可以选择SD卡上的缓存目录,具体是指/sdcard/Android/data/package_name/cache目录,其中package_name表示当前应用的包名,当应用被卸载后,此目录会一并被删除。这里给出一个建议:如果应用卸载后就希望删除缓存文件,那么就选择SD卡上的缓存目录,如果希望保留缓存数据那就应该选择SD卡上的其他特定目录;
第二个参数表示应用的版本号,设为1即可;
第三个参数表示单个节点所对应的数据个数大小,一般设为1即可;
第四个参数表示缓存的总大小,当缓存超过这个设定值后,DiskLruCache就会自行清除一些缓存。
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
2)DiskLruCache的缓存添加:
DiskLruCache的缓存添加操作是通过Editor对象完成的,Editor表示一个缓存对象的编辑对象。这里以图片缓存为例,首先需要获取图片url对应的key,然后根据key就可以edit()来获取Editor对象,如果这个对象正在被编辑,那么edit()就会返回null。之所以把url转换成key,是因为图片的url中很可能有特殊字符,这将影响url在Android中的直接使用,一般采用url的md5值作为key。
//url转key DisLruCache 添加+读取
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.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();
}
将图片的url转为key之后,就可以获取Editor对象了。对于这个key来说,如果当前不存在其他Editor对象,那么edit()就会返回一个新的Editor对象,通过它就可以得到一个文件输出流。
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
当从网络中下载图片时,图片就可以通过这个文件输出流写入到文件系统上:
public 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(),
IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
经过上面的步骤,其实并没有真正将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载过程发生了异常,那么还可以通过Editor的abort()来回退整个操作。
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
// 图片下载正常
editor.commit();
} else {
// 图片下载异常
editor.abort();
}
mDiskLruCache.flush();
经过上面几个步骤,图片就已经被正确地写入到文件系统中,接下来图片的获取操作就不需要请求网络了。
3)DiskLruCache的缓存查找:
和缓存的添加过程类似,缓存的查找过程也需要将url转化为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,有了文件输出流,自然就可以得到Bitmap对象了。为了避免图片加载过程中导致的OOM问题,一般不建议直接加载原始图片。可以通过文件流来得到它所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一整缩放后的图片。实现如下:
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
除了上述方法之外,DiskLruCache还提供了remove和delete方法来用于磁盘缓存的删除操作。
三、ImageLoader的实现
一般来说,一个优秀的ImageLoader应该具备如下功能:
- 图片的同步加载
- 图片的异步加载
- 图片压缩
- 内存缓存
- 磁盘缓存
- 网络拉取
图片的同步加载是指能够以同步的方式向调用者提供所加载的图片;图片的异步加载是一个很有用的功能,很多时候调用者不想在单独的线程中以同步的方式来获取图片,这个时候ImageLoader内部就需要自己在线程中加载图片并将图片甚至所需的ImageView。图片压缩的作用更加毋庸置疑了,这是降低OOM概率的有效手段,ImageLoader必须合理地处理图片的压缩问题。
内存缓存和磁盘缓存时ImageLoader的核心。
以下为完整实现:
1)ImageLoader
/**
* 缓存:当应用第一次从网络加载图片之后,就将其缓存到内存和存储设备中,下次使用时就不必再从网络中获取;
* 三级缓存:再次请求时,先访问内存,没有就再去访问存储设备,如果两者都没有再从网络中进行下载;
* 缓存三要素:添加、读取、删除
* 缓存算法:LRU,近期使用最少算法,采用LRU算法的缓存有两种:LruCache和DisLruCache
* LruCache:Android3.1提供的缓存类,v4包下可以兼容到早期版本。线程安全,提供了get和put方法来完成获取和添加操作
* DisLruCache:没有加入源码,本项目中可以copy一份出来。将缓存对象写入文件系统,从而实现缓存的效果。
* 创建:open()①SD卡上的缓存目录(getDiskCacheDir(context,"name")getCacheDir/getExternalCacheDir),②1;③1;④缓存的总容量。
* 添加:Editor缓存对象的编辑对象,url转为key,根据key通过edit()来获取Editor对象,通过后者可以获取到一个文件输出流(OutputStream),然后写入到文件系统中。commit提交;abort回退。
* OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
* 查找:添加类似:url转为key,通过get方法获取Snapshot对象,通过后者获取一个文件输入流,有了文件输入流(FileInputStream),自然可以得到Bitmap查找对象了。
* FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
* 为了避免加载图片过程中导致的OOM问题,一般不建议直接加载原始图片,磁盘缓存时使用BitmapFactory.decodeFileDescriptor()方法来加载一张缩放后的图片。
*
* 加载的对象会优先缓存在磁盘缓存中,添加缓存成功后再往内存中缓存一份。读取的顺序是反过来的。
*/
public class ImageLoader {
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
//创建线程池 用于执行网络请求这一耗时操作
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
//创建Handler 用于发送消息 切回主线程执行UI操作
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) ms