Glide在6.0系统下加载图片失败问题

最近在项目中使用到了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);
        }
    }
});

总的来说,过程很艰辛,但结果也收获颇丰!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值