最近在项目中使用到了Glide3.7,在通过GlideModel定制图片缓存到SD卡时,遇到了一个非常诡异的问题:
在Glide中使用缓存策略为Source或者All时,图片无法展示出来,并且也无法缓存到SD卡中:
只有在设置为Result时才能够正常的显示,但是同样无法缓存到SD中;
diskCacheStrategy(DiskCacheStrategy.RESULT);
代码里找了很久,都找不到问题原因,所以只能走一波源码了;
通过翻找源码,可以发现glide所以的图片请求任务都是通过EngineRunnable来执行的,在run方法中会调用decode方法来获取数据源:
@Override
public void run() {
if (isCancelled) {
return;
}
Exception exception = null;
Resource<?> resource = null;
try {
//请求图片数据
resource = decode();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}
if (isCancelled) {
if (resource != null) {
resource.recycle();
}
return;
}
if (resource == null) {
onLoadFailed(exception);
} else {
onLoadComplete(resource);
}
}
由于初始请求没有缓存,所有会执行decodeFromSource方法;
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
return decodeFromCache();
} else {
//初始从网络等获取源数据
return decodeFromSource();
}
}
private Resource<?> decodeFromSource() throws Exception {
return decodeJob.decodeFromSource();
}
接下来进入DecodeJob中开始获取、解析、缓存数据:
public Resource<Z> decodeFromSource() throws Exception {
Resource<T> decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
private Resource<T> decodeSource() throws Exception {
Resource<T> decoded = null;
try {
long startTime = LogTime.getLogTime();
//这一步从fetcher拿到图片源数据
final A data = fetcher.loadData(priority);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Fetched data", startTime);
}
if (isCancelled) {
return null;
}
decoded = decodeFromSourceData(data);
} finally {
fetcher.cleanup();
}
return decoded;
}
private Resource<T> decodeFromSourceData(A data) throws IOException {
final Resource<T> decoded;
if (diskCacheStrategy.cacheSource()) {
//如果缓存策略是SOURCE,则往这走
decoded = cacheAndDecodeSourceData(data);
} else {
long startTime = LogTime.getLogTime();
decoded = loadProvider.getSourceDecoder().decode(data, width, height);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded from source", startTime);
}
}
return decoded;
}
在diskCacheStrategy是Source的情况:
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);
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;
}
在diskCacheStrategy是Result的情况:
private Resource<T> decodeFromSourceData(A data) throws IOException {
final Resource<T> decoded;
if (diskCacheStrategy.cacheSource()) {
decoded = cacheAndDecodeSourceData(data);
} else {
//在diskCacheStrategy是Result的情况,并没有进行缓存操作,而是先对数据进行裁剪
long startTime = LogTime.getLogTime();
decoded = loadProvider.getSourceDecoder().decode(data, width, height);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded from source", startTime);
}
}
return decoded;
}
然后回到DecodeJob最初被调用的方法:decodeFromSource,此时diskCacheStrategy是Source的情况,已经进行了缓存操作,并且缓存失败,返回null;而Result的情况,则还没进行缓存,只是对源数据进行了裁剪,返回裁剪后的数据;
public Resource<Z> decodeFromSource() throws Exception {
Resource<T> decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
结束调用:transformEncodeAndTranscode
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);
}
//在这里,diskCacheStrategy是Result的情况进行了图片缓存
//同样,缓存会失败,但并不影响本方法的返回数据
writeTransformedToCache(transformed);
startTime = LogTime.getLogTime();
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}
//diskCacheStrategy是Result的情况进行了图片缓存
//同样,缓存会失败
private void writeTransformedToCache(Resource<T> transformed) {
if (transformed == null || !diskCacheStrategy.cacheResult()) {
return;
}
long startTime = LogTime.getLogTime();
SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
diskCacheProvider.getDiskCache().put(resultKey, writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote transformed from source to cache", startTime);
}
}
到此,可以知道为什么Result的情况能够显示图片,而Source或者All的情况不能:
因为Source的情况下,Glide先将图片缓存到本地,然后再从本地取出缓存,进行展示,如果缓存失败,则拿不到数据,相应的图片肯定展示不出来!
而Result的情况,Glide缓存和数据展示是分开的 ,即使缓存失败,也不会影响到数据展示。
那么,问题来了,为什么缓存会失败呢?
对比其他手机的情况,发现只有在6.0手机上出现这个问题,并且查找到SD卡中,发现缓存目录并没有创建成功。
思来想去,创建失败应该是没有权限导致,而项目中权限是在引导页动态获取的,经过无数次测试,检查……最后发现,原来是项目自己挖下的坑 /(ㄒoㄒ)/~~
由于是音乐项目,在Application启动时会调起通知栏,而通知栏的音乐封面是用Glide加载的,这样就会导致一个问题:
在Glide第一次被使用时,会去扫描清单文件:
//Glide.java
public static Glide get(Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
Context applicationContext = context.getApplicationContext();
List<GlideModule> modules = new ManifestParser(applicationContext).parse();
GlideBuilder builder = new GlideBuilder(applicationContext);
for (GlideModule module : modules) {
module.applyOptions(applicationContext, builder);
}
glide = builder.createGlide();
for (GlideModule module : modules) {
module.registerComponents(applicationContext, glide);
}
}
}
}
return glide;
}
而我自己定制的GlideModel此时就会被扫进来,我的SD缓存就配置在其中:
public class CustomGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
ViewTarget.setTagId(R.id.glide_tag_id); // 设置别的get/set tag id,以免占用View默认的
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888); // 设置图片质量为高质量
int cacheSize = 100*1000*1000;
builder.setDiskCache(new DiskLruCacheFactory(new DiskLruCacheFactory.CacheDirectoryGetter() {
@Override
public File getCacheDirectory() {
//设置SD卡缓存目录
String downloadDirectoryPath = Environment.getExternalStorageDirectory().getPath() + File.separator + "xxx"
+ File.separator + "AlbumArt";
File file = new File(downloadDirectoryPath);
if (!file.exists()) {
boolean mkdirs = file.mkdirs();
return mkdirs ? file : null;
}
return file;
}
}, cacheSize));
}
@Override
public void registerComponents(Context context, Glide glide) {
// 注册我们的ImageFidLoader
glide.register(MusicInfo.class, InputStream.class, new CoverUrlLoader.Factory());
}
}
此时,可能在引导页还没有获取到权限,而Glide已经扫描完清单文件,并且进行图片加载。
再走一波源码,可以更清楚!
由于没有SD卡权限,那么创建文件夹的操作就会失败,而在DiskLruCacheFactory的build方法中可以看到:当缓存目录为空时,return null;
//DiskLruCacheFactory.java
@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);
}
看它的调用者Engine类的内部类LazyDiskCacheProvider,可以发现Glide在diskCache为空的情况下,会返回一个DiskCacheAdapter;
private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider {
private final DiskCache.Factory factory;
private volatile DiskCache diskCache;
public LazyDiskCacheProvider(DiskCache.Factory factory) {
this.factory = factory;
}
@Override
public DiskCache getDiskCache() {
if (diskCache == null) {
synchronized (this) {
if (diskCache == null) {
diskCache = factory.build();
}
if (diskCache == null) {
diskCache = new DiskCacheAdapter();
}
}
}
return diskCache;
}
}
而DiskCacheAdapter只是一个空壳:
public class DiskCacheAdapter implements DiskCache {
@Override
public File get(Key key) {
// no op, default for overriders
return null;
}
@Override
public void put(Key key, Writer writer) {
// no op, default for overriders
}
@Override
public void delete(Key key) {
// no op, default for overriders
}
@Override
public void clear() {
// no op, default for overriders
}
}
至此一切真相大白! \ (≧▽≦)/
解决方案也很简单,只需把Application中调起通知栏的操作延迟到引导页流程之后即可:
//延迟通知栏的加载至应用获取到写SDCard权限之后,这样Glide才能成功创建缓存目录
registerActivityLifecycleCallbacks(new SimpleActivityLifecycleCallbacks(){
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
super.onActivityCreated(activity, savedInstanceState);
if (activity instanceof MainActivity) {
//发起通知...
unregisterActivityLifecycleCallbacks(this);
}
}
});
总的来说,过程很艰辛,但结果也收获颇丰!