APP图片缓存与Glide之signature的分析

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进行进一步研究,如上述内容有纰漏,多谢指正。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值