背景:针对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的核心逻辑,主要负责从网络或者磁盘缓存中读取图片。
-
如果需要从网络上获取图片,那么图片会被成DataFetcer解析成InputStream,ButeBuffer之类的类型,如果该次请求支持磁盘缓存的话,那么数据会被写入DiskCache,然后开始下一步。
-
Glide根据加载出来的数据类型InputStream,ByteBuffer,File等,和要解析成的数据类型Bitmap,Drawable,BitmapDrawable构造LoadPath,然后根据LoadPath寻找ResourceDecoder解码成需要的类型(RequestBuilder的类型)。
-
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播放质量。