从基本配置中寻找代码的入口:
这篇文章是ImageLoader的硬盘缓存策略完成解析,写起来比较详细,所以字数和图片都比较多,希望大家能认识读完。
一般情况下,我们都要配置上图的Imageloer配置,明显可以看出来,这里用到了“建造者模式”来完成基本数据参数的注入,我们都知道,Imageloader是通过三级访问,来实现对图片文件的展示的,
三级分别是:网络(Net)、硬盘(Disk)、内存(Memory)。这次我写的文章不是总结性的文章,而是先带大家,如何一步一步去看源代码,之后再进行总结。
硬盘缓存策略的配置入口ImageLoaderConfiguration.Builder. diskCache(disck) :
首先我们来了解一下,默认情况下,硬盘缓存会使用哪种策略:
点击 ctrl+鼠标选中 ,能够跳转到 ImageLoaderConfiguration类 这个类的职责是完成ImageLodaer的配置 diskCache(disck) 是其建造类的方法,注入了硬盘缓存算法:
通过注释我们可以看出来,这个函数方法的作用就是设置图片硬盘缓存策略的,默认使用的的硬盘策略是UnlimitedDiskCache这个缓存策略,默认的路径通过StorageUtils.getCacheDirectory(Context)来获取到,我们暂时不考虑他们内部的实现过程,我们只需要知道,默认情况下,我们什么都不设置的时候回使用UnlimitedDiskCache这种缓存策略,和默认的cache路径从StorageUtils.getCacheDirectory(Context)获取。
探寻硬盘缓存策略ImageLoader实现多少种策略:
接下来我们会先考虑一个问题,我们如何知道,Imageloade中到底有多少种缓存策略呢。。根据java建立包的规范,我们能考虑到,硬盘缓存的所有实现方法应该是放在同一目录下的,所以我们先看看UnlimitedDiskCache所在的目录“com.nostra13.universalimageloader.cache.disc.impl”,建包规范或者了熟悉面对对象编程的朋友通过这个目录,可以知道,这个目录下应该就是硬盘缓存的具体实现,(我使用的是Android studio 可以直接打开jar包) 我们直接点开 Universal-ImageLoader.jar包 的com.nostra13.universalimageloader.cache.disc目录 如下:
以下是我看过类后画出的UML类图:
我这里来解释下上面的图DiskCache是一个接口,通过“实现关系”规范BaseDisckCache和LruDiskCache必须实现的方法,BaseDiskCache是一个抽象类,实现的就是最简单的保存网络图片文件到硬盘,获取本地硬盘,并且没有任何限制,LruDiskCache类实现DiskCache,是基于“最近最少使用”算法来实现的,而LimitedAgeDiskCache和UnlimitedDiskCache跟BaseDiskCache有很多共同点,相对之下,LimitedAgeDiskCache只是对时间进行控制,对超时的图片文件进行删除处理,而UnlimitedDiskCache没有任何限制,他们对缓存的大小都没有控制,而LruDiskCache会控制缓存大小和缓存的文件多少,所以他们都继承BaseDiskCache抽象类。
我们来总结一下,Imageloder中的硬盘缓存有三种策略:
LruDiskCache:最近最少使用缓存策略 考虑文件缓存大小和缓存文件多少
LimitedAgeDiskCache:设置文件存活时间,当文件超过这个时间时就删除该文件,不考虑文件缓存大小
UnlimitedDiskCache:没有任何限制的存取策略,不考虑文件缓存大小
上面是硬盘缓存的三种策略,通过分析我们知道,LruDiskCache是里面非常好的策略,所以我们设置的时候尽量设置LruDiskCache,因为默认情况下我们使用的是UnlimitedDiskCache,这样对于用户来说是非常不友好的,因为每个用户的手机配置是不相同的,有些存储比较少的时候,这样就能给用户更加友好的体验,安卓的发展就靠大家了。。。
为什么默认策略是UnlimitedDiskCache:
掌握了大致的方向之后,我们可以思考一个问题:为什么默认的情况下是UnlimitedDiskCache,在代码中,Imageloder框架的如何实现的呢。。。
默认配置,也是属于配置,所以我们看一下ImageLoaderConfiguration类,在Android studio 中进入ImageLoaderConfiguration类 ,键盘设置ctrl+7(这个7是左边键盘的不能按数字键),你会看到:
你能看到有一个Builder类,这是建造者模式的标配,再看下来,我要找的是createDefault(Context context)这个方法函数进入过程如下:
createDefault(Context context)-->.build()-->initEmptyFieldsWithDefaultValues()(这个函数就是初始化默认值和空值)-->
看图:这里是硬盘缓存为空时的核心,显判断diskCacheFileNameGenerator是否为空,不为空就创建默认的,这里的diskCacheFileNameGenerator是硬盘的名字生成器,因为硬盘缓存策略都要命名,所以这里传入的名字生成器不能为null 下来就是重点了,我们继续点.createDiskCache进去,我们可以看到,我们调到这个新的类型里面去,这个类叫DefaultConfigurationFactory,其只要功能就是生成一些默认的配置,我们不管其他,继续看我们之前那方法,createDiskCache()意思就是说创建一个硬盘缓存策略:
看注释,我们就懂:创建一个取决于传入参数的继承于DiskCache的类,这句话怎么理解呢?意思就是说创建一个继承于DiskCache的类,但是具体实现的策略由传入的参数的决定,我们看86-95的判断就知道,影响的参数是diskCacheSize和diskCacheFileCount,如果有其中一个值大于0,我们使用的策略就是LruDiskCache,所以当我们需要使用LruDiskCache硬盘缓存策略的就只需要设置其中一个值为正整数就行了,当我们什么都不设置的时候,默认就会执行96-97行,使用的就是UnlimitedDiskCache。
下面说一下整个createDiskCache思路:
85行是:创建磁盘缓存文件夹,如果主磁盘缓存文件夹不可用,将使用该磁盘缓存文件夹,也就是一个备份区。
86行:对diskCacheSize或者diskCacheFileCount进行判断,如果大于0,执行87行 然后返回一个LruDiskCache实现对象。
87行:获取到一个保存图片的私人文件夹。
89-90行:返回LruDiskCache实现对象。
96-97行:获取获取文件夹路径,并返回没有任何限制的实现硬盘缓存。
探寻硬盘缓存策略ImageLoader三种策略的具体实现:
DiskCache:
之前已经说了三种策略都实现于DiskCache,接下来我们看看其源代码:
public interface DiskCache {
/**
硬盘缓存的接口
*/
/**
返回硬盘缓存的保存文件夹
*/
File getDirectory();
/**
* 获取到硬盘缓存的图片文件
*
* @param 图片唯一url
* @return File of cached image or <b>null</b> if image wasn't cached
*/
File get(String imageUri);
/**
* 在磁盘缓存中保存图像流。
* 此方法不应关闭传入的图像流
*
*/
boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;
/**
* 保存磁盘缓存中的图像位图。
*
*/
boolean save(String imageUri, Bitmap bitmap) throws IOException;
/**
* 删除输入URI关联的图像文件
*/
boolean remove(String imageUri);
/** Closes disk cache, releases resources. */
void close();
/** Clears disk cache. */
void clear();
}
这个接口就是规定硬盘缓存策略必须实现的方法,具体方法实现由具体类来实现,在这里使用接口是为了遵循面向对象的六大原则中的开闭原则,和里式替换原则。
BaseDiskCache:
下面,我们来看看BaseDiskCache的抽象类,我们主要看核心的三个方法:
1.boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener):在磁盘缓存中保存图像流
2.boolean save(String imageUri, Bitmap bitmap):保存磁盘缓存中的图像位图
3.File get(String imageUri):获取到硬盘缓存的图片文件
我们一个个方法来看看 BaseDiskCache是如何实现的:
save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener)
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
//根据url返回一个文件
File imageFile = getFile(imageUri);
//生成一个以.tmp结尾的临时文件
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
//声明是否保存成功
boolean loaded = false;
try {
//创建一个缓冲输入流
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
try {
//调用IoUtils的.copyStream复制流的函数方法读写到输入流中
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
//关闭临时文件
IoUtils.closeSilently(os);
}
} finally {
//结束后加载成功并转移到imageFile不成功时 loaded设置为false
if (loaded && !tmpFile.renameTo(imageFile)) {
loaded = false;
}
//loaded为false,删除临时文件
if (!loaded) {
tmpFile.delete();
}
}
return loaded;
}
上面就是直接对图像流进行保存,注释写的很多,我就不一一说了,下面我们看一下getFile(imageUri)的实现:
//通过imgeurl返回一个非空的文件,文件可以引用一个不存在的文件。
protected File getFile(String imageUri) {
//根据文件名成生成器生成文件名 这个文件名生成器有 两种实现方式,后面我们再说
String fileName = fileNameGenerator.generate(imageUri);
File dir = cacheDir;
//如果满足cacheDir不存在并且!cacheDir.mkdirs()说明磁盘不可操作,我就使用备用的文件夹
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
return new File(dir, fileName);
}
这里应该很好理解,就是获取cache文件的根目录,来生成一个以fileName命名的文件。
下面我们来看一下IoUtils这个Io流的工具类吧:
上面就是IoUtIls类的结构,主要的职能是复制图片文件,停止加载文件,当我们调用copyStream方法有三、四个参数两种多态,最终都会调用四个参数的,只是三个参数的不设置一次读取缓存流的大小,默认为DEFAULT_BUFFER_SIZE = 32 * 1024,下来我们来看看四个参数的:
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
//根据url返回一个文件
File imageFile = getFile(imageUri);
//生成一个以.tmp结尾的临时文件
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
//声明是否保存成功
boolean loaded = false;
try {
//创建一个缓冲输入流
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
try {
//调用IoUtils的.copyStream复制流的函数方法读写到输入流中
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
//关闭临时文件
IoUtils.closeSilently(os);
}
} finally {
//结束后加载成功并转移到imageFile不成功时 loaded设置为false
if (loaded && !tmpFile.renameTo(imageFile)) {
loaded = false;
}
//loaded为false,删除临时文件
if (!loaded) {
tmpFile.delete();
}
}
return loaded;
}
//通过imgeurl返回一个非空的文件,文件可以引用一个不存在的文件。
protected File getFile(String imageUri) {
//根据文件名成生成器生成文件名 这个文件名生成器有 两种实现方式,后面我们再说
String fileName = fileNameGenerator.generate(imageUri);
File dir = cacheDir;
//如果满足cacheDir不存在并且!cacheDir.mkdirs()说明磁盘不可操作,我就使用备用的文件夹
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
return new File(dir, fileName);
}
下面直接看一下IOUtils类:
/**
* 复制流、通过监听器发进度监听,监听器可以中断复制过程
*
* @param is 输入流
* @param os 输出流
* @param listener 复制过程和可以中断复制的监听器
* @param bufferSize 用于复制的缓冲流大小
* @return 如果为true 复制完成 如果falae中断复制
* @throws IOException
*/
public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)
throws IOException {
int current = 0;
//获取输入流的总大小
int total = is.available();
//如果为负数 就设置为图片默认的总大小为500 * 1024 即为500kb
if (total <= 0) {
total = DEFAULT_IMAGE_TOTAL_SIZE;
}
//创建字节数组
final byte[] bytes = new byte[bufferSize];
int count;
//首先判断是否应该停止正在复制的流 返回true 中断复制
if (shouldStopLoading(listener, current, total)) return false;
//遍历开始复制
while ((count = is.read(bytes, 0, bufferSize)) != -1) {
//写入
os.write(bytes, 0, count);
//记录当前复制完成的大小
current += count;
//每次都判断一下
if (shouldStopLoading(listener, current, total)) return false;
}
//刷新输出流
os.flush();
return true;
}
//是否应该停止正在复制的任务
private static boolean shouldStopLoading(CopyListener listener, int current, int total) {
//如果中断监听器为空直接返回false不中断
if (listener != null) {
//是否中断看客户端具体实现监听方法
boolean shouldContinue = listener.onBytesCopied(current, total);
//如果判断为中断后 ,还需要判断 是否加载少于75% 我们才会返回true中断
if (!shouldContinue) {
if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
return true; // if loaded more than 75% then continue loading anyway
}
}
}
return false;
}
//关闭流
public static void closeSilently(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception ignored) {
}
}
}
//读取 并关闭流
public static void readAndCloseStream(InputStream is) {
final byte[] bytes = new byte[DEFAULT_BUFFER_SIZE];
try {
while (is.read(bytes, 0, DEFAULT_BUFFER_SIZE) != -1);
} catch (IOException ignored) {
} finally {
closeSilently(is);
}
}
public boolean save(String imageUri, Bitmap bitmap)
下面我们讲一下保存磁盘缓存中的图像位图怎么处理的:
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
File imageFile = getFile(imageUri);
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
//是否保存成功
boolean savedSuccessfully = false;
try {
//直接调用Bitmap的复制方法
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
//如果成功 但是没有转移成功 判断为保存失败
if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
savedSuccessfully = false;
}
//保存失败删除临时文件
if (!savedSuccessfully) {
tmpFile.delete();
}
}
//释放Bitmap 因为btmap是非常耗费内存的
bitmap.recycle();
return savedSuccessfully;
}
比之前的简单,获取图片文件更加简单,我这里就不写了。
继承于BaseDiskCache的LimitedAgeDiskCache和UnlimitedDiskCache和其父类有何不同呢?
我们先说UnlimitedDiskCache,这个方法是没有任何限制,所以他直接继承了BaseDiskCache然后啥米事情都没干。
那么LimitedAgeDiskCache是怎么限制时间的呢?
我们看到源码中扩展了两个变量:
private final long maxFileAge;
private final Map<File, Long> loadingDates = Collections.synchronizedMap(new HashMap<File, Long>());
我们暂时不考虑这两个变量有啥用,我们关注重点的那两个方法,先看save():
我们能看到,保存文件的方法,还是使用父类的,但是这里他扩展了一个方法,rememberUsage(imageUri),这也是常用的扩展方法的方法,rememberUsage(imageUri)具体实现为:
private void rememberUsage(String imageUri) {
File file = getFile(imageUri);
long currentTime = System.currentTimeMillis();
file.setLastModified(currentTime);
loadingDates.put(file, currentTime);
}
这个方法就是实现,获取文件,然后设置当前时间为最后修改的时间,存储到强引用loadingDates变量中,现在我们大概能猜出,loadingDates的作用了吧,就是为了以文件为key,更改时间为values来记录,文件的保存时间。
下面我们看看:public File get(String imageUri)
public File get(String imageUri) {
//从父类获取文件 一模一样
File file = super.get(imageUri);
//如果file不为空,并且存在
if (file != null && file.exists()) {
//是否缓存有时间保存缓存
boolean cached;
//直接从loadingDates获取
Long loadingDate = loadingDates.get(file);
//没有缓存记录存在
if (loadingDate == null) {
//设置cached为false 说明之前没有访问过
cached = false;
//loadingDate从文件中获取最后更改时间
loadingDate = file.lastModified();
} else {
//有访问过缓存
cached = true;
}
//判断当前时间是否过时
if (System.currentTimeMillis() - loadingDate > maxFileAge) {
//过时就删除文件
file.delete();
//删除缓存记录
loadingDates.remove(file);
} else if (!cached) {
//没有过时 又没有本地缓存有访问记录 就添加
loadingDates.put(file, loadingDate);
}
}
return file;
}
上面不管是save还有get方法,我们发现都是采用新生成一个子类,重写部分方法来实现功能的扩展。当然我们也可以通过装饰者模式来实现功能的拓展。这是我发现的两张扩展比较好的方法。
DisKLrucache作为一个重点,另外篇幅讲解:http://www.jianshu.com/p/d03f10b18dff