作者:宇是我


一、前言

项目是一个图片类应用,内存占用一直居高不下。尤其在一些低端机型上,内存问题尤为突出。项目中使用Glide图片加载器,基于4.9.0 版本,默认图片加载格式是ARGB_8888。对于ARGB_8888的Bitmap,一个像素点存放了Alpha、Red、Green、Blue四种信息,总共4个字节。对于一些低端机型,为了降低内存占用,提升应用可用性,设置图片加载格式为RGB_565,其占用2个字节,故对比起来可以节省一半的内存。代价是会损失一定的三色彩,还好这在可接受的范围内。

二、添加图片格式设置

通过自定义GlideModule,我们可以对图片加载器进行一些全局配置,在这里,可以设置加载的图片格式。如下代码所示,设置当为低端机型时,图片格式为RGB_565;高端机型时,设置为ARGB_8888。

@GlideModule
public class CustomGlideModule extends AppGlideModule {
private final RequestOptions requestOptions = new RequestOptions();

@Override
public void applyOptions(Context context, GlideBuilder builder) {
if(DeviceUtils.isLowLevelDevice()){
builder.setDefaultRequestOptions((RequestOptions)this.requestOptions.format(DecodeFormat.PREFER_RGB_565));
} else {
builder.setDefaultRequestOptions((RequestOptions)this.requestOptions.format(DecodeFormat.PREFER_ARGB_8888));
}
}

// 针对V4用户可以提升速度
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}

打印加载图片的格式,发现有的是RGB_565,有的是ARGB_8888。排除图片加载重新设置图片格式的情况,理论上所有图片应该都是RGB_565的格式,但是从实际结果来看,和预期是不相符的。看下一下源码注释:

/**
* Bitmaps decoded from image formats that support and/or use alpha (some types of PNGs, GIFs etc)
* should return {@link android.graphics.Bitmap.Config#ARGB_8888} for
* {@link android.graphics.Bitmap#getConfig()}. Bitmaps decoded from formats that don't support or
* use alpha should return {@link android.graphics.Bitmap.Config#RGB_565} for
* {@link android.graphics.Bitmap#getConfig()}.
*
* <p>On Android O+, this format will will use ARGB_8888 only when it's not possible to use
* {@link android.graphics.Bitmap.Config#HARDWARE}.
*/
PREFER_RGB_565;

PREFER_RGB_565的注释,有这么一段内容。翻译过来大致意思:支持alpha通道的返回ARGB_8888,不支持alpha通道的返回RGB_565。

为什么会这样子,接下来我们从Glide的图片创建流程来分析看看。

三、Glide图片创建流程

最终查看的是Glide获取的bitmap的图片格式,所以分析看下Glide的创建bitmap的主要流程。这个流程,主要在​​com.bumptech.glide.load.resource.bitmap.Downsampler.java​​​类中完成。在这个类中,主要流程在​​DownSample.decodeFromWrappedStreams()​​方法中,主要包含如下流程:


  1. 读取原始图片信息:获取图片尺寸、类型等相关信息
  2. 计算目标图片尺寸和解码配置
  3. 设置inBitmap
  4. 从原始图片输入流获取目标bitmap

3.1 读取原始图片信息

在getDimensions方法中,通过设置options属性

options.inJustDecodeBounds = true,只读取图片信息,并不真的创建bitmap。此时outXxx 字段会被设置对应的图片属性值 ,

如 : outWidth 输出图像的 宽度 , outHeight 输出高度 , outMimeType 输出类型 ,

outConfig 像素格式 , outColorSpace 输出颜色空间。

private static int[] getDimensions(InputStream is, BitmapFactory.Options options,
DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException {
options.inJustDecodeBounds = true;
decodeStream(is, options, decodeCallbacks, bitmapPool);
options.inJustDecodeBounds = false;
return new int[] { options.outWidth, options.outHeight };
}

3.2 计算目标图片尺寸和解码配置

尺寸计算这部分本篇内容并不关心,重点看下解码配置部分。

private void calculateConfig(
InputStream is,
DecodeFormat format,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired,
BitmapFactory.Options optionsWithScaling,
int targetWidth,
int targetHeight) {

if (hardwareConfigState.setHardwareConfigIfAllowed(
targetWidth,
targetHeight,
optionsWithScaling,
format,
isHardwareConfigAllowed,
isExifOrientationRequired)) {
return;
}

// Changing configs can cause skewing on 4.1, see issue #128.
if (format == DecodeFormat.PREFER_ARGB_8888
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
return;
}

boolean hasAlpha = false;
try {
hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot determine whether the image has alpha or not from header"
+ ", format " + format, e);
}
}

optionsWithScaling.inPreferredConfig =
hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
optionsWithScaling.inDither = true;
}
}

总的看来,主要包含以下几点逻辑:


  • 如果设置支持硬件解码,则无需关注图片格式的设置
  • 如果设置DecodeFormat为PREFER_ARGB_8888或者SDK版本为16,则使用PREFER_ARGB_8888
  • 如果上述条件都不成立(设置为PREFER_RGB_565),判断图片是否支持alpha通道。若不支持alpha,设置为RGB_565;否则,设置为ARGB_8888。

那么,Glide是如何知道当前图片是否支持alpha通道的呢?

答案就是EXIF。EXIF,英文Exchangeable image file format的简称,翻译为可交换图像文件格式。是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。Glide正是基于EXIF,从而获取不同的图片格式,如PNG、JPEG等。具体逻辑在​​DefaultImageHeaderParser.java​​中实现。

@NonNull
private ImageType getType(Reader reader) throws IOException {
final int firstTwoBytes = reader.getUInt16();

// JPEG.
if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
return JPEG;
}

final int firstFourBytes = (firstTwoBytes << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
// PNG.
if (firstFourBytes == PNG_HEADER) {
// See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha
// -color-type
reader.skip(25 - 4);
int alpha = reader.getByte();
// A RGB indexed PNG can also have transparency. Better safe than sorry!
return alpha >= 3 ? PNG_A : PNG;
}

// GIF from first 3 bytes.
if (firstFourBytes >> 8 == GIF_HEADER) {
return GIF;
}

// WebP (reads up to 21 bytes). See https://developers.google.com/speed/webp/docs/riff_container
// for details.
if (firstFourBytes != RIFF_HEADER) {
return UNKNOWN;
}
// Bytes 4 - 7 contain length information. Skip these.
reader.skip(4);
final int thirdFourBytes =
(reader.getUInt16() << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
if (thirdFourBytes != WEBP_HEADER) {
return UNKNOWN;
}
final int fourthFourBytes =
(reader.getUInt16() << 16 & 0xFFFF0000) | (reader.getUInt16() & 0xFFFF);
if ((fourthFourBytes & VP8_HEADER_MASK) != VP8_HEADER) {
return UNKNOWN;
}
if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_EXTENDED) {
// Skip some more length bytes and check for transparency/alpha flag.
reader.skip(4);
return (reader.getByte() & WEBP_EXTENDED_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP;
}
if ((fourthFourBytes & VP8_HEADER_TYPE_MASK) == VP8_HEADER_TYPE_LOSSLESS) {
// See chromium.googlesource.com/webm/libwebp/+/master/doc/webp-lossless-bitstream-spec.txt
// for more info.
reader.skip(4);
return (reader.getByte() & WEBP_LOSSLESS_ALPHA_FLAG) != 0 ? ImageType.WEBP_A : ImageType.WEBP;
}
return ImageType.WEBP;
}

返回的图片类型,是一个枚举类型。定义如下:

enum ImageType {
GIF(true),
JPEG(false),
RAW(false),
/** PNG type with alpha. */
PNG_A(true),
/** PNG type without alpha. */
PNG(false),
/** WebP type with alpha. */
WEBP_A(true),
/** WebP type without alpha. */
WEBP(false),
/** Unrecognized type. */
UNKNOWN(false);

private final boolean hasAlpha;

ImageType(boolean hasAlpha) {
this.hasAlpha = hasAlpha;
}

public boolean hasAlpha() {
return hasAlpha;
}
}

类型中包含是否有alpha通道信息。因此,当图片类型确定之后,是否有alpha通道也就确定了。

3.3 设置inBitmap

主要逻辑在 setInBitmap(BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height)中,设置option的inBitmap字段。此部分逻辑不影响最终获取bitmap的图片格式,暂不深入分析。

3.4 解码获取目标bitmap

接下来,decodeFromWrappedStreams方法中调用如下:

Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);

在decodeStream方法中,调用了:

result = BitmapFactory.decodeStream(is, null, options);

此时,options中的inPreferredConfig字段数据,为在第二步计算配置赋值所得。将基于inPreferredConfig配置,解码获取目标bitmap。

四、总结

通过上述分析,可以发现当设置图片格式为RGB_565的时候,并不是所有图片都会按照这个格式进行输出。在Glide内部,会读取原始图片的EXIF头信息,获取当前图片的格式。若当前格式的图片支持alpha通道,则还是会设置为ARGB_8888的格式。