1.前言
Glide 是一个极其优秀的图片加载框架 , 在android 中图片以Bitmap形式展示 , 对Bitmap的优化是图片加载框架的重头戏
2.内存占用优化
2.1如何计算bitmap占用内存
android 4.4以前直接通过 bitmap.getHeight() * bitmap.getRowBytes() 计算, 4.4之后通过 bitmap.getAllocationByteCount() 计算占用内存
com.bumptech.glide.util.Util#getBitmapByteSize
public static int getBitmapByteSize(@NonNull Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// Workaround for KitKat initial release NPE in Bitmap, fixed in MR1. See issue #148.
try {
return bitmap.getAllocationByteCount();
} catch (
@SuppressWarnings("PMD.AvoidCatchingNPE")
NullPointerException e) {
// Do nothing.
}
}
return bitmap.getHeight() * bitmap.getRowBytes();
}
width * height * Bitmap.Config计算占用内存大小
com.bumptech.glide.util.Util#getBitmapByteSize
public static int getBitmapByteSize(int width, int height, @Nullable Bitmap.Config config) {
return width * height * getBytesPerPixel(config);
}
private static int getBytesPerPixel(@Nullable Bitmap.Config config) {
// A bitmap by decoding a GIF has null "config" in certain environments.
if (config == null) {
config = Bitmap.Config.ARGB_8888;
}
int bytesPerPixel;
switch (config) {
case ALPHA_8:
bytesPerPixel = 1;
break;
//RGB_565 2字节
case RGB_565:
case ARGB_4444:
bytesPerPixel = 2;
break;
case RGBA_F16:
bytesPerPixel = 8;
break;
//ARGB_8888 4字节
case ARGB_8888:
default:
bytesPerPixel = 4;
break;
}
return bytesPerPixel;
}
2.2 inSampleSize 和三件套优化占用内存
native方法中,bitmap 的width和height 由inTargetDensity,inTargetDensity,inSampleSize决定 , 所以只要优化inTargetDensity,inTargetDensity,inSampleSize便能优化bitmap占用内存
mBitmapWidth = mOriginalWidth * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize
mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inTargetDensity) * 1/inSampleSize
设置options.inJustDecodeBounds = true,不分配内存只获取图片的原始宽度和高度sourceWidth,sourceHeight
private static int[] getDimensions(
ImageReader imageReader,
BitmapFactory.Options options,
DecodeCallbacks decodeCallbacks,
BitmapPool bitmapPool)
throws IOException {
options.inJustDecodeBounds = true;
decodeStream(imageReader, options, decodeCallbacks, bitmapPool);
options.inJustDecodeBounds = false;
return new int[] {options.outWidth, options.outHeight};
}
通过ViewTreeObserver.OnPreDrawListener#onPreDraw获取图片的目标宽高targetWidth,targetHeight
SizeDeterminerLayoutListener#onPreDraw
public boolean onPreDraw() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this);
}
SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
if (sizeDeterminer != null) {
sizeDeterminer.checkCurrentDimens();
}
return true;
}
private int getTargetWidth() {
int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight();
LayoutParams layoutParams = view.getLayoutParams();
int layoutParamSize = layoutParams != null ? layoutParams.width : PENDING_SIZE;
return getTargetDimen(view.getWidth(), layoutParamSize, horizontalPadding);
}
private int getTargetDimen(int viewSize, int paramSize, int paddingSize) {
int adjustedParamSize = paramSize - paddingSize;
//ParamSize>0 返回ParamSize (layoutParams.width)
if (adjustedParamSize > 0) {
return adjustedParamSize;
}
//还没有布局等待size
if (waitForLayout && view.isLayoutRequested()) {
return PENDING_SIZE;
}
int adjustedViewSize = viewSize - paddingSize;
//ViewSize>0 ViewSize(view.getWidth())
if (adjustedViewSize > 0) {
return adjustedViewSize;
}
//如果为WRAP_CONTENT则目标宽高为屏幕宽高的最大值
if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) {
return getMaxDisplayLength(view.getContext());
}
return PENDING_SIZE;
}
通过源宽高和目标宽高计算缩放
private static void calculateScaling(
ImageType imageType,
ImageReader imageReader,
DecodeCallbacks decodeCallbacks,
BitmapPool bitmapPool,
DownsampleStrategy downsampleStrategy,
int degreesToRotate,
int sourceWidth,
int sourceHeight,
int targetWidth,
int targetHeight,
BitmapFactory.Options options)
throws IOException {
int orientedSourceWidth = sourceWidth;
int orientedSourceHeight = sourceHeight;
if (isRotationRequired(degreesToRotate)) {
orientedSourceWidth = sourceHeight;
orientedSourceHeight = sourceWidth;
}
//exactScaleFactor 为 targetWidth/orientedSourceWidth与targetHeight/orientedSourceHeight的最大值
final float exactScaleFactor =
downsampleStrategy.getScaleFactor(
orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
//SampleSizeRounding.QUALITY 质量优先
//倾向于向下取整样本大小,以便将图像向下采样到大于目标的大小以保持质量,但会增加内存使用量。
SampleSizeRounding rounding =
downsampleStrategy.getSampleSizeRounding(
orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight);
//获取输出的宽高
int outWidth = round(exactScaleFactor * orientedSourceWidth);
int outHeight = round(exactScaleFactor * orientedSourceHeight);
int widthScaleFactor = orientedSourceWidth / outWidth;
int heightScaleFactor = orientedSourceHeight / outHeight;
//内存优先则取较大的ScaleFactor
int scaleFactor =
rounding == SampleSizeRounding.MEMORY
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize;
if (Build.VERSION.SDK_INT <= 23
&& NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
//SDK_INT <= 23 并且是wbmp,x-ico格式的图片不支持inSampleSize
powerOfTwoSampleSize = 1;
} else {
//Integer.highestOneBit 得到小于等于参数的最大2的幂 ,比如是7则取4,9则取8
powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
if (rounding == SampleSizeRounding.MEMORY
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
//如果是MEMORY并且powerOfTwoSampleSize < (1.f / exactScaleFactor)
//powerOfTwoSampleSize左移一位 *2 , 内存减少1/4
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
//设置inSampleSize
options.inSampleSize = powerOfTwoSampleSize;
//处理inTargetDensity ,inDensity
int powerOfTwoWidth;
int powerOfTwoHeight;
if (imageType == ImageType.JPEG) {
int nativeScaling = Math.min(powerOfTwoSampleSize, 8);
//powerOfTwoWidth 等于原宽度除以powerOfTwoSampleSize 和8的最小值
powerOfTwoWidth = (int) Math.ceil(orientedSourceWidth / (float) nativeScaling);
//powerOfTwoHeight 等于原高度除以powerOfTwoSampleSize 和8的最小值
powerOfTwoHeight = (int) Math.ceil(orientedSourceHeight / (float) nativeScaling);
int secondaryScaling = powerOfTwoSampleSize / 8;
if (secondaryScaling > 0) {
powerOfTwoWidth = powerOfTwoWidth / secondaryScaling;
powerOfTwoHeight = powerOfTwoHeight / secondaryScaling;
}
} /...省略PNG WEBP 等powerOfTwoWidth计算方式.../
//已调整的缩放因子等于powerOfTwoWidth/targetWidth 和 powerOfTwoHeight/targetHeight 的最大值
double adjustedScaleFactor =
downsampleStrategy.getScaleFactor(
powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);
//4.4以下版本不支持三件套inTargetDensity,inDensity,inScaled
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
options.inDensity = getDensityMultiplier(adjustedScaleFactor);
}
if (isScaling(options)) {
options.inScaled = true;
} else {
options.inDensity = options.inTargetDensity = 0;
}
}
2.3 Config优化占用内存
如果不更改配置成RGB_565 , Glide默认只使用ARGB_8888 , ARGB_8888 比 RGB_565多了Alpha , 多占用一倍内存
private void calculateConfig(
ImageReader imageReader,
DecodeFormat format,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired,
BitmapFactory.Options optionsWithScaling,
int targetWidth,
int targetHeight) {
if (format == DecodeFormat.PREFER_ARGB_8888
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
//配置为DecodeFormat.PREFER_ARGB_8888则只使用ARGB_8888
optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
return;
}
boolean hasAlpha = false;
try {
hasAlpha = imageReader.getImageType().hasAlpha();
} catch (IOException e) {
}
//有Alpha则使用ARGB_8888,没有则使用RGB_565
optionsWithScaling.inPreferredConfig =
hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
optionsWithScaling.inDither = true;
}
}
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;
}
}
2.4 inBitmap 重用bitmap
为了避免每次新建bitmap所造成的内存抖动 , 可以通过inBtmap对Bitmap复用
com.bumptech.glide.load.resource.bitmap.Downsampler#setInBitmap()方法
private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
@Nullable Bitmap.Config expectedConfig = null;
// Avoid short circuiting, it appears to break on some devices.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (options.inPreferredConfig == Config.HARDWARE) {
//26版本以上并且 HARDWARE 不支持设置inBitmap
return;
}
expectedConfig = options.outConfig;
}
if (expectedConfig == null) {
expectedConfig = options.inPreferredConfig;
}
// expectedConfig 默认为 ARGB_8888
//此处获取未擦除的bitmap ,BitmapFactory会擦除bitmap
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
//LruBitmapPool#getDirty 获取未擦除的bitmap
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result == null) {
//为空则创建bitmap
result = createBitmap(width, height, config);
}
return result;
}
//获取擦除干净的bitmap
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result != null) {
result.eraseColor(Color.TRANSPARENT);
} else {
result = createBitmap(width, height, config);
}
return result;
}
3. 磁盘占用优化
把图片 bitmap存磁盘缓存中 , 不用每次都从网络获取,节约用户流量 , 但bitmap 内存占用很大 ,需使用compress 压缩图片质量再进行存入
com.bumptech.glide.load.resource.bitmap.BitmapEncoder#encode()
@Override
public boolean encode(
@NonNull Resource<Bitmap> resource, @NonNull File file, @NonNull Options options) {
final Bitmap bitmap = resource.get();
Bitmap.CompressFormat format = getFormat(bitmap, options);
try {
long start = LogTime.getLogTime();
//压缩质量默认为90
int quality = options.get(COMPRESSION_QUALITY);
boolean success = false;
OutputStream os = null;
try {
os = new FileOutputStream(file);
if (arrayPool != null) {
os = new BufferedOutputStream(os, arrayPool);
}
//把bitmap进行质量压缩并写入file中
bitmap.compress(format, quality, os);
os.close();
success = true;
} catch (IOException e) {
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
// Do nothing.
}
}
}
return success;
} finally {
GlideTrace.endSection();
}
}
4. 总结
Glide对Bitmap优化 , 通过调整 inSampleSize 和 inTargetDensity,inTargetDensity 降低Bitmap所占用的内存 , 通过compress 减少占用的磁盘空间