问题描述
最近在用glide加载gif图时遇到了一个圆角黑边的问题,就像这样:
通过查看源码发现是因为我们项目采用了自定义glidemodule,在这里将图片的解码格式设置为RGB_565导致的。
public class CustomGlideModule extends GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
}
}
解决方法
对于想快速解决问题的人,可以跳过后面的原理分析,直接看解决方法。最简单的方法是在加载时将解码格式手动设置为ARGB_8888。
Glide.with(requestOption.getContext())
...
.set(GifOptions.DECODE_FORMAT,PREFER_ARGB_8888)
...
原理分析
虽然解决了问题,但是可能也有人会好奇为什么将解码格式设置为RGB_565会导致带有圆角的gif出现边角黑边?本着知其然也要知其所以然的原则,我们来探索下其中的缘由。
我先简单说下结论,导致问题的原因是glide在解码gif时会从bitmap缓存池取出空闲的bitmap然后写入当前帧的像素,如果缓存池没有bitmap就会创建一张空白的bitmap,大家都知道创建bitmap会传一个Bitmap.Config,它定义了图片的解码格式,如果解码格式是RGB_565,那么就没有透明通道,创建的bitmap是一张黑色的图片,glide拿到这张黑色的图片往上面写入gif图当前帧像素时因为圆角的原因边角部分不会被覆盖,所以边角就会出现黑边。
下面是源码分析(只列出关键代码):
首先看下解码gif的代码,StandardGifDecoder负责解码gif,讲解都在注释里了。
StandardGifDecoder:
//加载gif下一帧的方法
public synchronized Bitmap getNextFrame() {
。。。。。
// 这里调用setPixels方法,该方法负责解码当前帧数据,然后写入到一张bitmap里
return setPixels(currentFrame, previousFrame);
}
private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) {
//中间省略的一大堆代码做的事情主要是解码当前帧数据并且存到dest数组中
。。。。
// 从bitmap缓存池获取一张bitmap
Bitmap result = getNextBitmap();
// 往这张bitmap写入当前帧的像素
result.setPixels(dest, 0, downsampledWidth, 0, 0, downsampledWidth, downsampledHeight);
return result;
}
private Bitmap getNextBitmap() {
Bitmap.Config config = isFirstFrameTransparent == null || isFirstFrameTransparent
? Bitmap.Config.ARGB_8888 : bitmapConfig;
// 从缓存池获取空闲bitmap,注意这里传了个config,如果我们通过自定义glidemodule把解码格式设置RGB_565,那么这里config的解码格式就会是RGB_565
Bitmap result = bitmapProvider.obtain(downsampledWidth, downsampledHeight, config);
result.setHasAlpha(true);
return result;
}
从上述代码我们知道glide解码gif时,会把gif的帧数据存入到一个数组中,然后从bitmap缓存池里面拿到一张空闲的bitmap,再往这张bitmap写入当前帧的像素,这样就不需要每一帧都用一张bitmap来绘制,可以节省不少内存。
下面我们再来看下怎么从bitmap缓存池获取bitmap。
GifBitmapProvider:
public Bitmap obtain(int width, int height, @NonNull Bitmap.Config config) {
return bitmapPool.getDirty(width, height, config);
}
LruBitmapPool:
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
//从缓存池里获取空闲bitmap
Bitmap result = getDirtyOrNull(width, height, config);
if (result == null) {
//如果缓存池没有则自己创建一张空白bitmap,注意config在这里用到了,也就是会以config指定的解码格式创建bitmap
result = createBitmap(width, height, config);
}
return result;
}
private synchronized Bitmap getDirtyOrNull(
int width, int height, @Nullable Bitmap.Config config) {
assertNotHardwareConfig(config);
// Config will be null for non public config types, which can lead to transformations naively
// passing in null as the requested config here. See issue #194.
final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
。。。。
return result;
}
SizeConfigStrategy:
public Bitmap get(int width, int height, Bitmap.Config config) {
// 计算目标图片占用内存大小
int size = Util.getBitmapByteSize(width, height, config);
Key bestKey = findBestKey(size, config);
// 从缓存获取bitmap,这里glide用到了一个策略,根据bitmap占用的内存和采用的解码格式从缓存池查找合适的bitmap返回,找到的bitmap尺寸不一定和目标尺寸一样,所以后面会再做一个尺寸转换
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
// Decrement must be called before reconfigure.
decrementBitmapOfSize(bestKey.size, result);
//将bitmap转成指定的目标宽高尺寸
result.reconfigure(width, height, config);
}
return result;
}
glide会从bitmap缓存池查找合适的bitmap返回,如果找不到就创建一张空白bitmap,空白bitmap会按照config指定的解码格式创建。
在这里说下glide是如何从缓存池查找合适bitmap的,首先会根据下一帧的宽高和解码格式计算其占用内存大小然后和解码格式组成一个key,缓存池会根据这个key找到对应的bitmap,也就是只要缓存池里存在内存大小和下一帧一样并且解码格式也相同的bitmap那么就返回,因为这不能保证宽高是对的,所以还需要把bitmap的宽高调整成和下一帧一样的宽高。因为bitmap缓存池的存在会为加载gif图节省不少内存开销。
好的,我的讲解就到这里,如果对你有帮助记得点个赞鼓励下我吧!!!