Glide 显示视频缩略图及遇到的坑

Glide 显示视频缩略图及遇到的坑

实现原理

Glide支持视频格式的文件,但是在3.x里会有些欠缺。其底层是通过 MediaMetadataRetriever实现的。

MediaMetadataRetriever拥有获取视频的第几帧的能力,Glide获取视频里的第n帧的代码如下:

VideoBitmapDecoder.java

@Override
public Bitmap decode(ParcelFileDescriptor resource, BitmapPool bitmapPool, int outWidth, int outHeight,
        DecodeFormat decodeFormat)
        throws IOException {
    MediaMetadataRetriever mediaMetadataRetriever = factory.build();
    mediaMetadataRetriever.setDataSource(resource.getFileDescriptor());
    Bitmap result;
    if (frame >= 0) {
      result = mediaMetadataRetriever.getFrameAtTime(frame);
    } else {
      result = mediaMetadataRetriever.getFrameAtTime();
    }
    mediaMetadataRetriever.release();
    resource.close();
    return result;
}

提取核心代码

mediaMetadataRetriever获取Bitmap的代码:

val file = FileInputStream(File(path))
val s = MediaMetadataRetriever()
s.setDataSource(file.fd)
file.close()
val bitmap = s.getFrameAtTime(-1)
imageView.setImageBitmap(bitmap)
s.release()

Glide3.x的一个bug

但3.x的Glide缓存策略不能是Source我们来分析各种缓存策略Glide的内部的逻辑

Result缓存策略

EngineRunnable.java

private Resource decode() throws Exception {
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}

EngineRunnable分走两次,第一次走isDecodingFromCache

private Resource decodeFromCache() throws Exception {
    Resource result = null;
    //...ignore code

    //先从result缓存里获取
        result = decodeJob.decodeResultFromCache();
    if (result == null) {
    //再从源文件缓存里获取
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}

第一次加载显示decodeResultFromCache会为空,我们略过,重点看decodeSourceFromCache

public Resource decodeSourceFromCache() throws Exception {
    //如果不是cacheSource直接返回空
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }

  //...ignore code
}

decode失败以后会把EngineRunnable 扔给另外一个线程

  private void onLoadFailed(Exception e) {
        if (isDecodingFromCache()) {
            stage = Stage.SOURCE;
            manager.submitForSource(this);
        } else {
            manager.onException(e);
        }
    }

因此它会再一次走到run()方法里的decode,而此次走的是decodeFromSource

DecodeJob.java

 public Resource decodeFromSource() throws Exception {
        Resource decoded = decodeSource();
        return transformEncodeAndTranscode(decoded);
    }
    private Resource decodeSource() throws Exception {
        Resource decoded = null;
        try {
           // 通过fetcher去加载Source
            final A data = fetcher.loadData(priority);
          //...ignore code
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }

DecodeJob.java

private Resource decodeFromSourceData(A data)  {
    final Resource decoded;
    if (diskCacheStrategy.cacheSource()) {
        decoded = cacheAndDecodeSourceData(data);
    } else {
        //因为缓存策略是Result,所以走的是该处
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
        //...ignore code
    }
    return decoded;
}

SourceDecode其实就是ImageVideoBitmapDecoder

public Resource decode(ImageVideoWrapper source
                            , int width, int height){
    Resource result = null;
    InputStream is = source.getStream();
    if (is != null) {
            //通过streamDecoder去decode图,它是StreamBitmapDecoder
            result = streamDecoder.decode(is, width, height);
    }

    if (result == null) {
        ParcelFileDescriptor fileDescriptor = source.getFileDescriptor();
        if (fileDescriptor != null) {
             //通过fileDescriptorDecoder去decode图,它是FileDescriptorBitmapDecoder
            result = fileDescriptorDecoder.decode(fileDescriptor, width, height);
        }
    }
    return result;
}

ImageVideoBitmapDecoder里有两种decoder

  1. StreamBitmapDecoder直接将输入流转成Bitmap
  2. FileDescriptorBitmapDecoder能将流通过VideoBitmapDecoder去转Bitmap

而VideoBitmapDecoder底层就是通过MediaMetadataRetriever去获取第一帧

如果源文件是视频,它将先通过StreamBitmapDecoder去decode,结果decode失败result=null
然后通过FileDescriptorBitmapDecoder去decode,可见这里Glide内部并不知道源文件是图片还是视频,所以先用图片的方式解
而图片方式解的时候也是去读它的头文件:

@Override
public Resource decode(InputStream source, int width, int height) {
  //通过downsampler去decode
    Bitmap bitmap = downsampler.decode(source, bitmapPool, width, height, decodeFormat);
    return BitmapResource.obtain(bitmap, bitmapPool);
}
@Override
public Bitmap decode(InputStream is, BitmapPool pool, int outWidth,
        int outHeight, DecodeFormat decodeFormat) {
    final ByteArrayPool byteArrayPool = ByteArrayPool.get();
    final byte[] bytesForOptions = byteArrayPool.getBytes();
    final byte[] bytesForStream = byteArrayPool.getBytes();
    //此处用了Option池,值得学习!
    final BitmapFactory.Options options = getDefaultOptions();
    //...ignore code

       final Bitmap downsampled =
       downsampleWithSize(invalidatingStream, bufferedStream, 
       options, pool, inWidth, inHeight, sampleSize,
                                decodeFormat);

 private static Bitmap decodeStream(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream,
            BitmapFactory.Options options) {
         if (options.inJustDecodeBounds) {
             is.mark(MARK_POSITION);
         } else {
             bufferedStream.fixMarkLimit();
         }
         //如果是视频文件此处无法decode出Bitmap
        final Bitmap result = BitmapFactory.decodeStream(is, null, options);
        //...ignore code

        return result;
    }

第一种decode失败会用第二种,也就是最终会走到VideoBitmapDecoder去解析

Source缓存策略

如果缓存策略是Source

private Resource decodeSource() throws Exception {
    Resource decoded = null;
    try {
        final A data = fetcher.loadData(priority);
        //...ignore code
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}
private Resource decodeFromSourceData(A data)  {
    final Resource decoded;
    //因为是缓存Source因为走cacheAndDecodeSourceData方法
    if (diskCacheStrategy.cacheSource()) {
        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;
}
private Resource cacheAndDecodeSourceData(A data) {
    SourceWriter writer = new SourceWriter(loadProvider.getSourceEncoder(), data);
    //通过DisckCache把源文件缓存
    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
    //然后通过loadFromCache去加载缓存
    Resource result = loadFromCache(resultKey.getOriginalKey());
    return result;
}

而加载缓存的Decoder是CacheDecoder,非常不幸它不是ImageVideoBitmapDecoder


private Resource loadFromCache(Key key)  {
    File cacheFile = diskCacheProvider.getDiskCache().get(key);
    if (cacheFile == null) {
        return null;
    }

    Resource result = null;
    try {
        result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
    } finally {
        if (result == null) {
            diskCacheProvider.getDiskCache().delete(key);
        }
    }
    return result;
}

它是FileToStreamDecoder,以下是截图
Glide 显示视频缩略图及遇到的坑_第1张图片

而FileToStreamDecoder的Decoder是StreamBitmapDecoder,最终它是把该文件当成图片解析
因此导致它失败,如果result==null,它还会走

diskCacheProvider.getDiskCache().delete(key);

所以如果是视频流,缓存策略是Source,它会很让你失望,不停的copy文件,再解,解失败再删除,一直这样下去

如果该源文件很大,会影响Glide的性能!

为了验证这一猜想,我们可以通过抓包工具看一下发生了什么:

Glide 显示视频缩略图及遇到的坑_第2张图片

结果它确实把这个视频存下来了,不过发现decode失败就会把它删除

所以,显示视频图的时候,一定切记不要用Source缓存策略,它不仅无法加载成功,而且会给你带来很大隐患

结束!

你可能感兴趣的:(UI,android,kotlin)