1.图片缓存遇到的问题
在缓存网络图片的过程中,有一种情况是图片的地址不变,但图片发生了变化,如果只按照图片的地址进行缓存,在加载缓存中的图片时就会发生图片一直显示为旧图的现象。
在App中修改用户头像的功能中,如果服务器存储头像的地址保持不变,加载缓存就会出现上述的情况。马上想到,当修改头像之后,马上将本地原头像的缓存清除,并加载新头像,此方法只是暂时解决了APP端头像的显示效果,如果在其他终端进行了头像修改,手机上并不能同步显示最新图片。
那么该如何获取最新的网络图片呢?显然不使用缓存是肯定可以显示最新的图片,但要使用缓存图片功能,又希望可以获取最新的图片,我们需要记录图片是否发生了变化,根据变化与否,选择是否更新缓存中的内容。
有人说下载图片之后,判断其SHA值是否相同,即可得知图片是否相同。然而,每次都要下载图片,再判断SHA值,还用缓存做什么,已经完全背道而驰。
好的做法是在服务器上加上图片是否改变的标识,在APP端存储该值,在加载缓存内容之前判断是否有改变,需要更新缓存内容。该标识可以使用时间戳,来记录图片更新时间,或使用累加数来记录标识。
当然,以上说的是处理自己的服务器上,数据可以增加字段的情况。如果只是单纯的加载网络上的图片,可以在图片下载之后,在APP中做标识,一段时间之内不更新,在一天或固定时间后检测标识并更新网络图片。比如一天更新一次,则可将日期作为标识。
2.Glide简析
Glide作为一个优秀的加载图片库,提供了signature方法对图片进行标识,现针对安卓Glide-3.7.0进行简要分析。
Glide的一般调用方法为Glide.with(context).load(url).into(target);使用缓存时.diskCacheStrategy(DiskCacheStrategy.
ALL)来指定缓存类型。
枚举类DiskCacheStrategy有两个属性,
private final boolean cacheSource;
private final boolean cacheResult;
//四个枚举值
/** Caches with both {@link #SOURCE} and {@link #RESULT}. */
ALL(true, true), //缓存原文件和处理后(如尺寸变化、类型转换) 的数据
/** Saves no data to cache. */
NONE(false, false), //不缓存
/** Saves just the original data to cache. */
SOURCE(true, false), //只缓存原文件
/** Saves the media item after all transformations to cache. */
RESULT(false, true); //只缓存处理后的数据
也可以通过.signature(Key signature)方法传入标识,来实现预期功能。传入参数为实现Key接口的类对象,Glide中有三个类StringSignature,MediaStoreSignature,EmptySignature.
构造方法StringSignature(String signature)中传入String,可以简单传入刚才说的时间戳进行标识。
构造方法MediaStoreSignature(String mimeType, long dateModified, int
orientation)可以传入图片的mimeType,修改时间,图片或视频的方向
当然也可以自定义类实现Key接口并复写其equals方法判断标识相同,及updateDiskCacheKey方法去更新本地缓存文件。
现在说说Glide加载图片的过程。
在into()方法(GenericRequestBuilder类中的方法)中创建了Request:
Request request = buildRequest(target);//创建GenericRequest实例
requestTracker.runRequest(request);//调用GenericRequest的begin方法
而在begin()方法中会调用onSizeReady(int width,int height)方法
此方法中调用engine.load方法开始加载图片
Engine类中load方法部分如下:
final String id = fetcher.getId();//当加载url图片时,id为图片地址
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());//根据id,signature以及其他的信息生成key,用于缓存文件的key
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority);
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable);
在runnable的run方法中调用decode()方法
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
return decodeFromCache();
} else {
return decodeFromSource();
}
}
private boolean isDecodingFromCache() {
return stage == Stage.CACHE;
}
而EngineRunnable的初始化中
this.stage = Stage.CACHE;
故首先执行decodeFromCache();首次加载返回空,调用:
private void onLoadFailed(Exception e) {
if (isDecodingFromCache()) {
stage = Stage.SOURCE;//改变值
manager.submitForSource(this);//重新调起run()方法,之后进入decodeFromSource()方法
} else {
manager.onException(e);
}
}
两个方法长这样:
private Resource<?> decodeFromCache() throws Exception {
Resource<?> result = null;
try {
result = decodeJob.decodeResultFromCache();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Exception decoding result from cache: " + e);
}
}
if (result == null) {
result = decodeJob.decodeSourceFromCache();
}
return result;
}
public Resource<Z> decodeResultFromCache() throws Exception {
if (!diskCacheStrategy.cacheResult()) {
//cacheResult ()判断枚举类DiskCacheStrategy的类型,由初始化时的类型决定布尔值,详见文章开头处
return null;//如果没有缓存数据,返回null
}
long startTime = LogTime.getLogTime();
Resource<T> transformed = loadFromCache(resultKey);//根据resultKey获取transform后的数据
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded transformed from cache", startTime);
}
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from cache", startTime);
}
return result;
}
loadFromCache方法加载缓存文件:
private Resource<T> loadFromCache(Key key) throws IOException {
File cacheFile = diskCacheProvider.getDiskCache().get(key);//根据key获取缓存文件
if (cacheFile == null) {
return null;
}
Resource<T> result = null;
try {
result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);//将文件解码为Resource
} finally {
if (result == null) {
diskCacheProvider.getDiskCache().delete(key);//若Resource为空则key无效,删除
}
}
return result;
}
public Resource<Z> decodeSourceFromCache() throws Exception {
if (!diskCacheStrategy.cacheSource()) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());//根据OriginalKey获取原文件
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return transformEncodeAndTranscode(decoded);
}
OriginalKey的构造方法如下,只含有id和signature两个属性
public OriginalKey(String id, Key signature) {
this.id = id;
this.signature = signature;
}
上述decodeFromSource()的源码如下:
public Resource<Z> decodeFromSource() throws Exception {
Resource<T> decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
decodeSource()方法最后调到了cacheAndDecodeSourceData方法:
private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
long startTime = LogTime.getLogTime();
SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);//根据OriginalKey存储图片原文件
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote source to cache", startTime);
}
startTime = LogTime.getLogTime();
Resource<T> result = loadFromCache(resultKey.getOriginalKey());
if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return result;
}
也就是说,decodeFromCache()和 decodeFromSource()最后都调用了transformEncodeAndTranscode方法,将原文件进行转换transform:
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
Resource<T> transformed = transform(decoded);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transformed resource from source", startTime);
}
writeTransformedToCache(transformed);//会判断是否需要缓存转换后的数据,根据diskCacheStrategy.cacheResult()结果决定
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}
如果diskCacheStrategy.cacheResult() 为true,此方法中的writeTransformedToCache方法会将数据缓存起来:
diskCacheProvider.getDiskCache().put(resultKey, writer);//根据resultKey存储transform之后的数据
这里的diskCacheProvider又从哪里来的呢?追踪到Engine里,
this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory);
而此类中getDiskCache()方法如下:
public DiskCache getDiskCache() {
if (diskCache == null) {
synchronized (this) {
if (diskCache == null) {
diskCache = factory.build();
}
if (diskCache == null) {
diskCache = new DiskCacheAdapter();
}
}
}
return diskCache;
}
继续找factory发现,上面这些方法中的get,put方法都是接口方法,具体factory从Glide.with(context)时传入:
with方法context不同时,调用方法相同,以activity为例:
public static RequestManager with(Activity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
上述方法最终创建了RequestManager对象,在其构造方法中
this.glide = Glide.get(context);
get方法调用
glide = builder.createGlide();
GlideBuilder类中createGlide()方法有这么几行:
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
InternalCacheDiskCacheFactory 类继承自DiskLruCacheFactory
这里就是factory的build方法了
@Override
public DiskCache build() {
File cacheDir = cacheDirectoryGetter.getCacheDirectory();
if (cacheDir == null) {
return null;
}
if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {
return null;
}
return DiskLruCacheWrapper.get(cacheDir, diskCacheSize);
}
最后的get方法返回一个DiskLruCacheWrapper对象,也就是getDiskCache()返回的对象了,用来缓存数据的方法都在这里了:
public static synchronized DiskCache get(File directory, int maxSize) {
// TODO calling twice with different arguments makes it return the cache for the same directory, it's public!
if (wrapper == null) {
wrapper = new DiskLruCacheWrapper(directory, maxSize);
}
return wrapper;
}
终于,上边获取缓存文件和存储缓存的方法都在DiskLruCacheWrapper类里了:
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);//返回一个DiskLruCache对象
}
return diskLruCache;
}
@Override
public File get(Key key) {
String safeKey = safeKeyGenerator.getSafeKey(key);
File result = null;
try {
final DiskLruCache.Value value = getDiskCache().get(safeKey);
if (value != null) {
result = value.getFile(0);
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to get from disk cache", e);
}
}
return result;
}
@Override
public void put(Key key, Writer writer) {
String safeKey = safeKeyGenerator.getSafeKey(key);
writeLocker.acquire(key);
try {
DiskLruCache.Editor editor = getDiskCache().edit(safeKey);
// Editor will be null if there are two concurrent puts. In the worst case we will just silently fail.
if (editor != null) {
try {
File file = editor.getFile(0);
if (writer.write(file)) {
editor.commit();
}
} finally {
editor.abortUnlessCommitted();
}
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to put to disk cache", e);
}
} finally {
writeLocker.release(key);
}
}
@Override
public void delete(Key key) {
String safeKey = safeKeyGenerator.getSafeKey(key);
try {
getDiskCache().remove(safeKey);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to delete from disk cache", e);
}
}
}
@Override
public synchronized void clear() {
try {
getDiskCache().delete();
resetDiskCache();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to clear disk cache", e);
}
}
}
这些get,put的方法又是调用的DiskLruCache中的get,put方法,方法返回Value对象
其中用到了Entry类来存储文件file,
Entry的构造方法如下:
private Entry(String key) {
this.key = key;
this.lengths = new long[valueCount];
cleanFiles = new File[valueCount];
dirtyFiles = new File[valueCount];
// The names are repetitive so re-use the same builder to avoid allocations.
StringBuilder fileBuilder = new StringBuilder(key).append('.');
int truncateTo = fileBuilder.length();
for (int i = 0; i < valueCount; i++) {
fileBuilder.append(i);
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}
get/put方法中,生成key的方法如下:
class SafeKeyGenerator {
private final LruCache<Key, String> loadIdToSafeHash = new LruCache<Key, String>(1000);
public String getSafeKey(Key key) {
String safeKey;
synchronized (loadIdToSafeHash) {
safeKey = loadIdToSafeHash.get(key);
}
if (safeKey == null) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
key.updateDiskCacheKey(messageDigest);
safeKey = Util.sha256BytesToHex(messageDigest.digest());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
synchronized (loadIdToSafeHash) {
loadIdToSafeHash.put(key, safeKey);
}
}
return safeKey;
}
}
其中,LruCache用了一个Map存储key及其对应的加密后的字符串,
LruCache的代码不多,都放上吧:
public class LruCache<T, Y> {
private final LinkedHashMap<T, Y> cache = new LinkedHashMap<T, Y>(100, 0.75f, true);//最后的最后,这里是用一个LinkedHashMap对key和缓存进行了存储。
private int maxSize;
private final int initialMaxSize;
private int currentSize = 0;
public LruCache(int size) {
this.initialMaxSize = size;
this.maxSize = size;
}
public void setSizeMultiplier(float multiplier) {
if (multiplier < 0) {
throw new IllegalArgumentException("Multiplier must be >= 0");
}
maxSize = Math.round(initialMaxSize * multiplier);
evict();
}
protected int getSize(Y item) {
return 1;
}
protected void onItemEvicted(T key, Y item) {
// optional override
}
public int getMaxSize() {
return maxSize;
}
public int getCurrentSize() {
return currentSize;
}
public boolean contains(T key) {
return cache.containsKey(key);
}
public Y get(T key) {
return cache.get(key);
}
public Y put(T key, Y item) {
final int itemSize = getSize(item);
if (itemSize >= maxSize) {
onItemEvicted(key, item);
return null;
}
final Y result = cache.put(key, item);
if (item != null) {
currentSize += getSize(item);
}
if (result != null) {
// TODO: should we call onItemEvicted here?
currentSize -= getSize(result);
}
evict();
return result;
}
public Y remove(T key) {
final Y value = cache.remove(key);
if (value != null) {
currentSize -= getSize(value);
}
return value;
}
public void clearMemory() {
trimToSize(0);
}
protected void trimToSize(int size) {
Map.Entry<T, Y> last;
while (currentSize > size) {
last = cache.entrySet().iterator().next();
final Y toRemove = last.getValue();
currentSize -= getSize(toRemove);
final T key = last.getKey();
cache.remove(key);
onItemEvicted(key, toRemove);
}
}
private void evict() {
trimToSize(maxSize);
}
}
另外:
public static String sha1BytesToHex(byte[] bytes) {
synchronized (SHA_1_CHARS) {
return bytesToHex(bytes, SHA_1_CHARS);
}
}
private static String bytesToHex(byte[] bytes, char[] hexChars) {
int v;
for (int j = 0; j < bytes.length; j++) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_CHAR_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
总结:
Glide的signature方法有效地添加了标识,通过简单的传入StringSignature即可实现判断图片信息是否为最新,从而加载最新的图片。
在自行开发过程中,可以更好的处理旧图片问题,把标识信息和图片分开存放,如有图片更新,可删除旧图片,只缓存新图片。当然,主要看是否需要保存旧图片。
P.S.仅对Glide一部分功能进行了简要分析,有兴趣的读者可以参见
https://github.com/bumptech/glide进行进一步研究,如上述内容有纰漏,多谢指正。