[Android] GIF播放优化

背景:针对APP中大量的GIF播放场景进行优化,指标以内存占用流畅性为主。

目前Anroid上比较流行的GIF播放方案有Glide和android-gif-drawable,下面我们逐一进行分析,以及如何对现有方案进行优化。

android-gif-drawable

基本使用

链接:https://github.com/koral--/android-gif-drawable

这个是比较流行的GIF播放方案了,它会提供一个创建GifDrawable的方法,我们使用GifDrawable的方案就可以播放GIF了。

GifDrawable的构造器

但是有一点需要注意GifDrawable的内存分配是用malloc()完成的,并且如果使用文件创建的GifDrawbale(很常见)的话,文件也是需要手动关闭的。

所以,GifDrawble需要在View的onDetchToWidow获取Activity的onDestory的时候调用recycle()释放资源,否则会造成内存和FD泄漏。

原理分析

下面简单看下GifDrawable的原理 。
GifDrawable是绘制是基于ANativeWindow机制,通过ANativeWindow不断修改GifDrable中的Bitmap,达到GIF播放的效果。

GifDrawable的初始化
每个GifDrawble在构造器中都会初始化一个GifInfoHandle的对象,这个对象保存了Native层GifInfo的地址,以后我们的每次操作,需要这个GifInfo的地址。

同时GifInfo对象记录了当前的Gif文件信息和读取方法。

final class GifInfoHandle {
	static {
		LibraryLoader.loadLibrary(null);
	}

	/**
	 * Pointer to native structure. Access must be synchronized, heap corruption may occur otherwise
	 * when {@link #recycle()} is called during another operation.
	 */
	private volatile long gifInfoPtr;
	
}

我们以打开文件为例

__unused JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_openFd(JNIEnv *env, jclass __unused handleClass, jobject jfd, jlong offset) {
	if (isSourceNull(jfd, env)) {
		return NULL_GIF_INFO;
	}
	jclass fdClass = (*env)->GetObjectClass(env, jfd);
	static jfieldID fdClassDescriptorFieldID = NULL;
	if (fdClassDescriptorFieldID == NULL) {
		fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
	}
	if (fdClassDescriptorFieldID == NULL) {
		return NULL_GIF_INFO;
	}
	const jint oldFd = (*env)->GetIntField(env, jfd, fdClassDescriptorFieldID);
	const int fd = dup(oldFd);
	if (fd == -1) {
		throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
		return NULL_GIF_INFO;
	}
	if (lseek64(fd, offset, SEEK_SET) != -1) {
		FILE *file = fdopen(fd, "rb");
		if (file == NULL) {
			throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
			close(fd);
			return NULL_GIF_INFO;
		}
		struct stat st;
		const long long sourceLength = fstat(fd, &st) == 0 ? st.st_size : -1;

        // 创建GifInfo
		GifInfo *const info = createGifInfoFromFile(env, file, sourceLength);
		if (info == NULL) {
			close(fd);
		}
		return (jlong) info;
	} else {
		throwGifIOException(D_GIF_ERR_OPEN_FAILED, env, true);
		close(fd);
		return NULL_GIF_INFO;
	}
}

// 这个时候回读取GIF的文件头,获取GIF的尺寸,颜色通道等信息。
static GifInfo *createGifInfoFromFile(JNIEnv *env, FILE *file, const long long sourceLength) {
	GifSourceDescriptor descriptor = {
			.rewindFunc = fileRewind,
			.sourceLength = sourceLength
	};
	// 读取GIF头部,fileRead为函数指针
	descriptor.GifFileIn = DGifOpen(file, &fileRead, &descriptor.Error);
	// 获取当前文件指针
	descriptor.startPos = ftell(file);

	return createGifInfo(&descriptor, env);
}

我们看下fileRead的实现

uint_fast8_t fileRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
	FILE *file = (FILE *) gif->UserData;
	return (uint_fast8_t) fread(bytes, 1, size, file);
}

其实DGifOpen的参数是一个函数指针,有点类似于Java中的接口。

/* func type to read gif data from arbitrary sources (TVT) */
typedef uint_fast8_t (*InputFunc)(GifFileType *, GifByteType *, uint_fast8_t);

也就是说,如果我们使用InputStream或者ByteBuffer创建GifDrawable,会有不同的读取方式。

如果我们使用的byte[],或者ByteBuffer创建的GifDrawble,那么InputFunc的参数分别为

// 读取byte[]的方法
uint_fast8_t byteArrayRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
	ByteArrayContainer *bac = gif->UserData;
	JNIEnv *env = getEnv();
	if (env == NULL) {
		return 0;
	}
	if (bac->position + size > bac->length) {
		size -= bac->position + size - bac->length;
	}
	(*env)->GetByteArrayRegion(env, bac->buffer, (jsize) bac->position, size, (jbyte *) bytes);
	bac->position += size;
	return size;
}

// 读取ByteBuffer的方法
uint_fast8_t directByteBufferRead(GifFileType *gif, GifByteType *bytes, uint_fast8_t size) {
	DirectByteBufferContainer *dbbc = gif->UserData;
	if (dbbc->position + size > dbbc->capacity) {
		size -= dbbc->position + size - dbbc->capacity;
	}
	memcpy(bytes, dbbc->bytes + dbbc->position, (size_t) size);
	dbbc->position += size;
	return size;
}

核心播放逻辑

__unused JNIEXPORT jlong JNICALL
Java_pl_droidsonroids_gif_GifInfoHandle_renderFrame(JNIEnv *env, jclass __unused handleClass, jlong gifInfo, jobject jbitmap) {
	GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
	if (info == NULL)
		return -1;

	long renderStartTime = getRealTime();
	void *pixels;
	// lock bitmap pixel
	if (lockPixels(env, jbitmap, info, &pixels) != 0) {
		return 0;
	}
	// 读取一帧GIF的数据
	DDGifSlurp(info, true, false);
	if (info->currentIndex == 0) {

        // 将Pixel数据清空
		prepareCanvas(pixels, info);
	}

    // 将一帧的数据生成到bitmap上
	const uint_fast32_t frameDuration = getBitmap(pixels, info);
	unlockPixels(env, jbitmap);
	return calculateInvalidationDelay(info, renderStartTime, frameDuration);
}

每次绘制的时候,都会去通过这个方法数据中去获取这一帧的数据。

DDGifSlurp(info, true, false);

DDGifSlurp是Gif解码的核心逻辑,里面涉及到解析Extension,颜色表之类的数据,内容比较复杂,并且需要些GIF文件的知识,暂时先分析到这里。

这里我们可以看到GifDrawable的核心逻辑是使用在Native层创建GifInfo对象,然后不断从GIF文件中获取下一帧的数据,这个和视频的播放很像。

由此可以看出,构造GifDrawable的最好方式是使用文件或者InputStream,如果使用ByteBuffer或者byte[]的话,整个Gif文件必须存在在内存里,会占用较大的内存。使用文件或者输入流可以保证内存里只存在一帧和共享区域的数据。

Glide

Glide是Android开发中非常常见的图片加载库,在Glide中,集成了播放Gif的功能,Glide在解码资源的时候会根据文件头进行判断,这样省去了我们代码中判断文件是否是Gif的逻辑。

原理分析

Glide 在调用构造器的时候加入了Gif的ResourceDecoder,这个ResourceDecoder会先于BitmapResourceDecoder执行,如果判断到文件头是Gif ,那么就会当做Gif进行处理。

下面我们看下代码验证下这个结论。

    Glide(
      @NonNull Context context,
      @NonNull Engine engine,
      @NonNull MemoryCache memoryCache,
      @NonNull BitmapPool bitmapPool,
      @NonNull ArrayPool arrayPool,
      @NonNull RequestManagerRetriever requestManagerRetriever,
      @NonNull ConnectivityMonitorFactory connectivityMonitorFactory,
      int logLevel,
      @NonNull RequestOptions defaultRequestOptions,
      @NonNull Map<Class<?>, TransitionOptions<?, ?>> defaultTransitionOptions,
      @NonNull List<RequestListener<Object>> defaultRequestListeners) {
      
      // 省略其他的.....
      // 注册GIF解码组件
      registry
        /* GIFs */
        .append(
            Registry.BUCKET_GIF,
            InputStream.class,
            GifDrawable.class,
            new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool))

        // 这些BUCKET_GIF会注册在Registry的最前面,所以会使用优先判断GIF
        .append(Registry.BUCKET_GIF, ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
        .append(GifDrawable.class, new GifDrawableEncoder())
        /* GIF Frames */
        // Compilation with Gradle requires the type to be specified for UnitModelLoader here.
        .append(
            GifDecoder.class, GifDecoder.class, UnitModelLoader.Factory.<GifDecoder>getInstance())
        .append(
            Registry.BUCKET_BITMAP,
            GifDecoder.class,
            Bitmap.class,
            new GifFrameResourceDecoder(bitmapPool))
        /* Drawables */
        .append(Uri.class, Drawable.class, resourceDrawableDecoder)
        .append(
            Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitmapPool))
            
    }    

Registry的构造器

 public Registry() {
    this.modelLoaderRegistry = new ModelLoaderRegistry(throwableListPool);
    this.encoderRegistry = new EncoderRegistry();
    this.decoderRegistry = new ResourceDecoderRegistry();
    this.resourceEncoderRegistry = new ResourceEncoderRegistry();
    this.dataRewinderRegistry = new DataRewinderRegistry();
    this.transcoderRegistry = new TranscoderRegistry();
    this.imageHeaderParserRegistry = new ImageHeaderParserRegistry();
    // Registry的构造器中,GIF注册在最前面
    setResourceDecoderBucketPriorityList(
        Arrays.asList(BUCKET_GIF, BUCKET_BITMAP, BUCKET_BITMAP_DRAWABLE));
  }

之所以需要注册这么多组件,这个跟Glide的加载逻辑有关,简单介绍下Glide获取数据的顺序。

这个时序图是从DecodeJob加载网络数据的流程,DecodeJob的Glide的核心逻辑,主要负责从网络或者磁盘缓存中读取图片。

  1. 如果需要从网络上获取图片,那么图片会被成DataFetcer解析成InputStream,ButeBuffer之类的类型,如果该次请求支持磁盘缓存的话,那么数据会被写入DiskCache,然后开始下一步。

  2. Glide根据加载出来的数据类型InputStream,ByteBuffer,File等,和要解析成的数据类型Bitmap,Drawable,BitmapDrawable构造LoadPath,然后根据LoadPath寻找ResourceDecoder解码成需要的类型(RequestBuilder的类型)。

  3. RequestBuilder的默认类型是Drawable,即没有调用as(xxx.class)更改RequestBuilder的类型,会先由GifStreamDecoder判断是否是Gif文件,如果是Gif,那么处理,否则交由其他LoadPath的ResourceDecoder进行处理。

整体加载流程如此,我们看下GifStreamEncoder这块的逻辑,看下如何对GIF进行处理的。

public class StreamGifDecoder implements ResourceDecoder<InputStream, GifDrawable> {

  // 省略....
  @Override
  public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException {
    return !options.get(GifOptions.DISABLE_ANIMATION)
        && ImageHeaderParserUtils.getType(parsers, source, byteArrayPool) == ImageType.GIF;
  }
  // 省略....
}
  

这个handles表示是否需要处理这个数据,可以看出主要是根据Flag和文件头判断的,如果是GIF,那么就使用这个Decoder进行解码。

如果handles,返回true了,那么接下来会回调decode方法,这个decode会将Data类型解析成对应的Resource类型,即Resource<GifDrawable>

最终会调用到GifByteBufferDecoder类的decode(),我们看下实现

 @Nullable
  private GifDrawableResource decode(
      ByteBuffer byteBuffer, int width, int height, GifHeaderParser parser, Options options) {
    long startTime = LogTime.getLogTime();
    try {
      final GifHeader header = parser.parseHeader();
      if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) {
        // If we couldn't decode the GIF, we will end up with a frame count of 0.
        return null;
      }

      Bitmap.Config config = options.get(GifOptions.DECODE_FORMAT) == DecodeFormat.PREFER_RGB_565
          ? Bitmap.Config.RGB_565 : Bitmap.Config.ARGB_8888;

      int sampleSize = getSampleSize(header, width, height);
      GifDecoder gifDecoder = gifDecoderFactory.build(provider, header, byteBuffer, sampleSize);
      gifDecoder.setDefaultBitmapConfig(config);
      gifDecoder.advance();
      Bitmap firstFrame = gifDecoder.getNextFrame();
      if (firstFrame == null) {
        return null;
      }

      Transformation<Bitmap> unitTransformation = UnitTransformation.get();
      // 创建GifDrawable
      GifDrawable gifDrawable =
          new GifDrawable(context, gifDecoder, unitTransformation, width, height, firstFrame);

      return new GifDrawableResource(gifDrawable);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Decoded GIF from stream in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }

可以看出,上面创建了GifDrawable对象,并且从ByteBuffer中获取了首帧。那Gif是如何动起来的呢?

我们看到ImageViewTarget中有这么一段逻辑。

// onResourceReady会调用这段逻辑
 private void setResourceInternal(@Nullable Z resource) {
    // Order matters here. Set the resource first to make sure that the Drawable has a valid and
    // non-null Callback before starting it.
    setResource(resource);
    maybeUpdateAnimatable(resource);
  }

// 判断是否是Animatable接口实例,如果是调用start()
 private void maybeUpdateAnimatable(@Nullable Z resource) {
    if (resource instanceof Animatable) {
      animatable = (Animatable) resource;
      animatable.start();
    } else {
      animatable = null;
    }
  }

GifDrawble当然实现了这个接口,我们看下start()的相关实现

// ~ GifDrawable中


  @Override
  public void start() {
    isStarted = true;
    resetLoopCount();
    if (isVisible) {
      startRunning();
    }
  }

private void startRunning() {
    Preconditions.checkArgument(!isRecycled, "You cannot start a recycled Drawable. Ensure that"
        + "you clear any references to the Drawable when clearing the corresponding request.");
    // If we have only a single frame, we don't want to decode it endlessly.
    if (state.frameLoader.getFrameCount() == 1) {
      invalidateSelf();
    } else if (!isRunning) {
      isRunning = true;
      // 这个地方FrameLoader会逐渐解析每一帧
      state.frameLoader.subscribe(this);
      invalidateSelf();
    }
  }

start()最终会调用到GifFrameLoader的start(),图就不画了,逻辑也不是很多。

 private void loadNextFrame() {
    if (!isRunning || isLoadPending) {
      return;
    }
    if (startFromFirstFrame) {
      Preconditions.checkArgument(
          pendingTarget == null, "Pending target must be null when starting from the first frame");
      gifDecoder.resetFrameIndex();
      startFromFirstFrame = false;
    }
    if (pendingTarget != null) {
      DelayTarget temp = pendingTarget;
      pendingTarget = null;
      onFrameReady(temp);
      return;
    }
    isLoadPending = true;
    // Get the delay before incrementing the pointer because the delay indicates the amount of time
    // we want to spend on the current frame.
    int delay = gifDecoder.getNextDelay();
    long targetTime = SystemClock.uptimeMillis() + delay;

    gifDecoder.advance();
    next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
    requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);// 此处重新启动Bitmap加载流程
  }

最终看到,还是通过Glide去加载了下一帧,最后还回去走DecodeJob那一套逻辑。只不过model换成了GifDecoder。

最后一行又通过Glide进行加载,不过Model变成了GifDecoder,Target变成了DelayTraget,ResourceType是Bitmap。

GifFrameLoader加载完一帧后,会调用onFrameReady()通知GifDrawable重绘,这样GifDrawable会调用draw()方法重新渲染Bitmap。

@Override
  public void draw(@NonNull Canvas canvas) {
    if (isRecycled) {
      return;
    }

    if (applyGravity) {
      Gravity.apply(GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), getDestRect());
      applyGravity = false;
    }

    Bitmap currentFrame = state.frameLoader.getCurrentFrame();
    canvas.drawBitmap(currentFrame, null, getDestRect(), getPaint());
  }

最后用一张时序图总结下上面的流程。

方案比较

Glide的优劣

Glide中ModelLoader机制会将图片数据解析成ByteBuffer,如果这个GIF比较大的话,就比较费内存了,
但是依赖于BitmapPools机制,不会创建大量临时对象。

同时Glide有优秀的内存缓存和文件缓存,可以复用已经创建过的GifDrawable对象,不需要重新再解码。

android-gif-drawable的优劣

android-gif-drawable可以从文件中逐帧读取GIF,这个在GIF文件巨大时特别有用,
这在内存中保留当前帧和公共区域的数据,而非全部GIF数据。

android-gif-drawable需要自己去做缓存机制,并且需要在何时的时候调用recycle()防止内存泄漏。

经过测试android-gif-drawable的GIF播放质量优于Glide。

验证结论

使用同样的手机,播放了一个40MB的GIF文件,查看内存占用情况。

GifDrawable播放内存占用20多MB,Glide播放占用60多MB,可以看出Glide确实是将整个GIF加载到内存里了。大家可以自行试下。

结合两种方案进行优化

目前打算将Glide和GifDrawble相结合,GifDrawable可以使用Glide的内存和文件缓存,同时可以利用Glide的生命周期避免内存泄漏。

其实就是实现如下调用:

Glide.with(getApplicationContext())
                .as(pl.droidsonroids.gif.GifDrawable.class)
                .load(URL)
                .into(img1);

或者能自动判断文件头,如果是GIF的话 ,直接创建GIFDrawable实例,不是的话使用Glide加载。
当然得设置个Flag,如果有的话,才这样加载。

Glide.with(getApplicationContext())
                .load(URL)
                .set(GlideOptions.USE_PL_GIF_IF_NEEDED, true)
                .into(img1);

实现

1. 构建LoadPath

Glide的解码核心逻辑是构建LoadPath,我们只要定义自己的LoadPath,放在Glide的默认方案之前就行。

这里使用的GifDrawable的构造器是File,即我们需要通过文件创建这个GifDrawable,其他使用ByteBuffer和InputStream也可以,但是效果不好。

但是我们这个File是临时的,Glide播放其他Gif的时候,这个文件可以拿来复用。

所以我们定义这个类为FileBridge,为File的包装类。

/**
 *
 * @author yangtianrui
 * @date 2019/1/5
 */

public class FileBridge {

    private File mFile;
    private boolean mIsRecycle;
    private StreamFileDecoder mDecoder;

    public FileBridge(StreamFileDecoder decoder, File file) {
        mFile = file;
        mDecoder = decoder;
    }

    public File getFile() {
        return mFile;
    }

    public void setFile(File file) {
        mFile = file;
    }

    public boolean isRecycle() {
        return mIsRecycle;
    }

    public void setRecycle(boolean recycle) {
        mIsRecycle = recycle;
        if (recycle) {
            mDecoder.recycleFileBridge(this);
        }
    }


    @Override
    public String toString() {
        return "FileBridge{" +
                "mFile=" + mFile.getName() +
                ", mIsRecycle=" + mIsRecycle +
                '}';
    }
}

由此而知,我们的LoadPath为ByteBuffer->FileBridge->pl.droidsonroids.gif.GifDrawable。

先定义一个InputStream到FileBridge的Decoder。



/**
 * 将InputStream转为FileBridge类型,便于GifDrawable处理.
 *
 * @author yangtianrui
 * @date 2019/1/5
 */

public class StreamFileDecoder implements ResourceDecoder<InputStream, FileBridge> {

    private final ArrayPool mArrayPool;
    private final Registry mRegistry;
    private final StreamEncoder mStreamEncoder;
    private final File mDir;

    private List<FileBridge> mRecyclers;

    public StreamFileDecoder(Glide glide, File dir) {
        mRegistry = glide.getRegistry();
        mArrayPool = glide.getArrayPool();
        mStreamEncoder = new StreamEncoder(mArrayPool);
        mDir = dir;
    }


    @Override
    public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException {
        final List<ImageHeaderParser> parsers = mRegistry.getImageHeaderParsers();
        //Log.d("<ytr>", "isGifFile ? " + isGifFile + " hasOptions ? " + hasOptions(options));
        return ImageHeaderParserUtils.getType(parsers, source, mArrayPool) == ImageHeaderParser.ImageType.GIF;
    }


    @Nullable
    @Override
    public Resource<FileBridge> decode(@NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException {
        final FileBridge fileBridge = getFileBridge();
        final boolean result = mStreamEncoder.encode(source, fileBridge.getFile(), options);
        Log.d("<ytr>", "decode: " + result);
        if (result) {
            return new FileBridgeResource(fileBridge);
        }
        return null;
    }

    private FileBridge getFileBridge() throws IOException {
        inflateFileBridgeIfNeeded();
        FileBridge fileBridge = mRecyclers.isEmpty() ? null : mRecyclers.remove(0);
        if (fileBridge == null) {
            File file = new File(mDir, String.valueOf(System.currentTimeMillis()));
            fileBridge = new FileBridge(this, file);
        }
        // 检查文件是否存在
        File file = fileBridge.getFile();
        if (!file.exists()) {
            final boolean success = file.createNewFile();
            if (!success) {
                throw new IOException("can not create file bridge in " + mDir.getAbsolutePath());
            }
        }
        fileBridge.setRecycle(false);
        return fileBridge;
    }

    private void inflateFileBridgeIfNeeded() {
        if (mRecyclers != null) {
            return;
        }
        mRecyclers = new ArrayList<>();
        File[] files = mDir.listFiles();
        if (files == null) {
            return;
        }
        for (File file : files) {
            FileBridge fileBridge = new FileBridge(this, file);
            mRecyclers.add(fileBridge);
        }
        Log.d("<ytr>", "inflateFileBridgeIfNeeded: " + mRecyclers);
    }


    public void recycleFileBridge(FileBridge fileBridge) {
        if (mRecyclers != null) {
            mRecyclers.add(fileBridge);
        }
    }
}

当然也需要支持ByteBuffer。

public class BufferFileDecoder implements ResourceDecoder<ByteBuffer, FileBridge> {

    private final StreamFileDecoder mStreamFileDecoder;

    public BufferFileDecoder(StreamFileDecoder streamFileDecoder) {
        mStreamFileDecoder = streamFileDecoder;
    }

    @Override
    public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
        Log.d("<ytr>", "handles: BufferFileDecoder");
        return hasOptions(options) && mStreamFileDecoder.handles(ByteBufferUtil.toStream(source), options);
    }

    @Nullable
    @Override
    public Resource<FileBridge> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
        return mStreamFileDecoder.decode(ByteBufferUtil.toStream(source), width, height, options);
    }

    private static boolean hasOptions(Options options){
        final Boolean hasOptions = options.get(GlideOptions.USE_PL_GIF_IF_NEEDED);
        return hasOptions != null && hasOptions;
    }

}

这两个ResourceDecoder都将Glide的数据类型转为了FileBridge,下面我们直接使用FileBridge创建GifDrawable即可。

定义一个Transcoder。

public class GifDrawableTranscoder implements ResourceTranscoder<FileBridge, GifDrawable> {


    @Nullable
    @Override
    public Resource<GifDrawable> transcode(@NonNull Resource<FileBridge> toTranscode, @NonNull Options options) {
        Log.d("<ytr>", "transcode: GifDrawableTranscoder");
        final File file = toTranscode.get().getFile();
        try {
            return new GifDrawableResource(new GifDrawable(file));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

现在我们定义的LoadPath已经完成了,我们把它们注册到Glide里面。

//~ GlideModule中
//省略......
  @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
        registerPLGifDrawable(glide, registry);
    }


    private void registerPLGifDrawable(@NonNull Glide glide, @NonNull Registry registry) {
        final String temp_dir = "glide_temp";
        StreamFileDecoder streamFileDecoder = new StreamFileDecoder(glide,
                glide.getContext().getDir(temp_dir, Context.MODE_PRIVATE));
        registry.prepend(InputStream.class, FileBridge.class, streamFileDecoder);
        registry.prepend(ByteBuffer.class, FileBridge.class, new BufferFileDecoder(streamFileDecoder));
        registry.register(FileBridge.class, GifDrawable.class, new GifDrawableTranscoder());
    }
    //省略......

这样就已经完成了,之前我们提到的内存泄漏问题也需要处理下,只需要实现Resource的onRecycle方法即可。

/*
 * 图片被移除内存缓存时调用,此时释放GifDrawable的资源
 *
 * @author yangtianrui
 * @date 2019/1/2
 */

public class GifDrawableResource extends SimpleResource<GifDrawable> {


    public GifDrawableResource(@NonNull GifDrawable data) {
        super(data);
    }

    @Override
    public void recycle() {
        final GifDrawable drawable = get();
        drawable.recycle();
    }
}

FileBridge不使用时,也需要回收

public class FileBridgeResource extends SimpleResource<FileBridge> {


    public FileBridgeResource(@NonNull FileBridge data) {
        super(data);
    }

    @Override
    public void recycle() {
        super.recycle();
        get().setRecycle(true);
    }
}

这样就完成了GifDrawable和Glide的结合,这样目前看起来是个不错的方式,Glide里面缓存能给GifDrawable用,GifDrawable能 利用Glide的生命周期避免内存泄漏。并且能够减少内存占用,优化GIF播放质量。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页