背景
在最近的项目中,由于后台的特殊要求,每次加载图片时,图片url都会携带一个时间戳,即如下图片格式为这样的形式:http://xxxx/aaaa/cccc.jpg?timestamp 其中问号前面的部分是不变的,而timestamp每次是不同的。换句话说,每次加载图片的url是不同的。也就是说,如果不经过特殊处理,ImageLoader的缓存是没有办法使用的,也就是说所有图片只要显示必须当下下载,也就别提什么用户体验了。
通过抓包了解到,服务端每张图片的url其实是在url的基础上加了一个时间戳,也就是说对于同一张图片,每次下载时其实是在一个固定url的基础上加时间戳构成真的url。换句话说,就是上面那个示例url其实问号前面的部分是不变的,既然有不变的地方,可否为我们所用?
感觉上当然是可以的,实现就需要我们重新分析一下ImageLoader是怎样加载缓存图片的。
加载图片过程
详细过程分析可以参考如下链接http://blog.csdn.net/lidec/article/details/50133533,此处我们只看bitmap是如何被存入与取出缓存的。
由之前的博文可以知道,下载的主要业务逻辑存在于ImageLoader的displayImage方法。
图片的缓存分为内存缓存(memoryCache)和硬盘缓存(diskCache),如果我们能找到url与缓存索引的对应关系,我们就可以直接将url中固定不变的部分拿出来,用这部分生成索引,然后当我们查找缓存中的索引时,直接使用固定不变那部分url生成索引。总之,就是用url中不带时间戳的部分来作为取缓存的依据,这样,不管后面时间戳怎么变化,我们都能得到唯一的图片。
通过分析源码可知,图片内存缓存的索引生成方式如下:
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
......
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
通过图片的uri与大小,经过一定的算法生成内存缓存索引,然后通过这个索引就可以在缓存中获取bitmap
当内存中不存在缓存时,我们就需要从硬盘中获取。在前文分析配置文件时候,我们发现ImageLoader中为我们提供了一个FileNameGenerator接口,通过重写这个接口可以实现我们对硬盘缓存文件的自定义命名。所以,我们可以通过实现自己的FileNameGenerator,来截取我们所需要的有效url,并使用一定的算法生成我们的命名。是不是这样呢,我们看看源码。
//尝试在文件中加载
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
......
//从文件中解码图片
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
我们可以看看diskCache的get(uri)是怎么做的。
protected File getFile(String imageUri) {
String fileName = this.fileNameGenerator.generate(imageUri);
File dir = this.cacheDir;
if(!this.cacheDir.exists() && !this.cacheDir.mkdirs() && this.reserveCacheDir != null && (this.reserveCacheDir.exists() || this.reserveCacheDir.mkdirs())) {
dir = this.reserveCacheDir;
}
return new File(dir, fileName);
}
可见,里面是调用fileNameGenerator来生成的文件名,而解码文件时,用到的file就是这个文件民对应的file,所以,我们只需要实现自己的fileNameGenerator,将时间戳部分截取掉,用其余部分生成文件名,就可以实现uri的固定。
两个实际问题
原理就是这样,而让人蛋疼的问题还在后面,首先,要修正以前博客配置ImageLoder文章的一个bug,经过试验,在config中设置fileNameGenrator是无效的。如下
config.diskCacheFileNameGenerator(new MyHashCodeNameGenerator());
这么写就扯了dan了,其实打开缓存文件夹,里面根本不是按照你的规则生成的文件名,而是默认的hashcode生成。通过阅读源码,我发现必须在设置diskCache类型的时候传入将Generator构造函数,这样才能有效设置。代码如下:
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
......
config.diskCache(new BaseDiskCache(new File(FileUtils.getImageDir()), null, new MyHashCodeNameGenerator()) {
});
//config.diskCacheFileNameGenerator(new MyHashCodeNameGenerator()); 这个没用
ImageLoader.getInstance().init(config.build());
第二坑是我一开始直接截取字符串后,用这个字符串的hashcode作为文件名返回,后来发现对于相同的字符串,返回的hashcode是不一样的。所以,是时候回头看看java基础了............. 然后我开始参照原文件中的MD5NameGenerator进行了实现,MD5加密只和字符有关系,只要传入相同字符串,返回值必定相同,所以只是对问号前的部分进行MD5编码并返回字符串。代码如下
/**
* Created by vonchenchen on 2016/1/6 0006.
*/
public class MyHashCodeNameGenerator implements FileNameGenerator {
private static final String HASH_ALGORITHM = "MD5";
private static final int RADIX = 10 + 26; // 10 digits + 26 letters
@Override
public String generate(String s) {
String urlHeader = s.split("[?]")[0];
byte[] md5 = getMD5(urlHeader.getBytes());
BigInteger bi = new BigInteger(md5).abs();
return bi.toString(RADIX);
//String urlHeader = s.split("[?]")[0]; //这样写虽然截取后url一样,但是生成的hashcode每次不同
//return String.valueOf(s.hashCode());
}
private byte[] getMD5(byte[] data) {
byte[] hash = null;
try {
MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
digest.update(data);
hash = digest.digest();
} catch (NoSuchAlgorithmException e) {
//L.e(e);
}
return hash;
}
}
这样,我们就在不改变ImageLoader源码的情况下,为程序增加了硬盘缓存,节省了流量,也使图片加载更为流畅。问题有点烦人,原理其实很简单,实现也走了些弯路,总结经验,稳步前进。