从Glide 源码中学习如何优化Bitmap

1,148 阅读4分钟

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 减少占用的磁盘空间