Glide 加载大尺寸图片 OOM

大尺寸图片,into 参数是 SimpleTarget,应用崩溃。

图片所占内存计算

测试

  • 如果 Target 是 ImageView

    • xml 中布局宽高自适应,且没有配置 override 参数,加载内存增加也就 3M 左右。
    • 如果 xml 中指定了更小的宽高或配置了 override 参数,那么内存会更小。
  • 如果 Target 不是 ImageView,比如 SimpleTarget

    • SimpleTarget 中设置了参数或者设置了 override,根据尺寸的不同,内存增大不一,但基本在可控范围,10M 以内。

    • 未在构造时传入指定尺寸或者 override

      Glide.with(getApplicationContext())
          .load(url)
          .asBitmap()
          .into(new SimpleTarget() {
      
              @Override
              public void onResourceReady(Bitmap bitmap, GlideAnimation glideAnimation) {
                  // imageView.setImageBitmap(bitmap);
              }
          });
      

      首先 into 方法将图片加载到内存中,然后回调 onResourceReady 这个方法,可见 Java 层内存飙升了 96M 左右,主要解码图片的操作。

      Glide 加载大尺寸图片 OOM_第1张图片
      屏幕快照 2019-03-04 上午11.16.10.png

      而假如再执行 imageView.setImageBitmap(bitmap) 上,Graphics 也出现一个峰值,增加了近 100M

      Glide 加载大尺寸图片 OOM_第2张图片
      屏幕快照 2019-03-04 上午11.26.56.png

    主要的区别就在于 Target。对于 View,一般来说,尺寸最大也就屏幕分辨率,所占内存终究有个限制,而不是 View,一些第三方的服务中的图片多大完全不知道。

原因

Target 尺寸计算

into() 方法会执行到 GenericRequest 类的 begin()

public void begin() {
    
    // ...
    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        onSizeReady(overrideWidth, overrideHeight);
    } else {
        target.getSize(this);
    }
    // ...
}

如果通过 override() 方法传入尺寸,会直接进入 onSizeReady(),若未设置,Target 是 View 的话,会去获取 View 显示出来的尺寸

public void getSize(SizeReadyCallback cb) {
    int currentWidth = getViewWidthOrParam();
    int currentHeight = getViewHeightOrParam();
    if (isSizeValid(currentWidth) && isSizeValid(currentHeight)) {
        cb.onSizeReady(currentWidth, currentHeight);
    } else {
        if (!cbs.contains(cb)) {
            cbs.add(cb);
        }
        if (layoutListener == null) {
            final ViewTreeObserver observer = view.getViewTreeObserver();
            layoutListener = new SizeDeterminerLayoutListener(this);
            observer.addOnPreDrawListener(layoutListener);
        }
    }
}

而如果是 SimpleTarget

public SimpleTarget() {
    this(SIZE_ORIGINAL, SIZE_ORIGINAL);
}

public SimpleTarget(int width, int height) {
    this.width = width;
    this.height = height;
}
    
public final void getSize(SizeReadyCallback cb) {
    if (!Util.isValidDimensions(width, height)) {
        throw new IllegalArgumentException("Width and height must both be > 0 or Target#SIZE_ORIGINAL, but given"
                + " width: " + width + " and height: " + height + ", either provide dimensions in the constructor"
                + " or call override()");
    }
    cb.onSizeReady(width, height);
}

可见如果 SimpleTarget 构造时没有传尺寸参数,宽高就是 SIZE_ORIGINAL,即 Integer 的最小值。最后也会执行到 onSizeReady()

采样压缩

GenericRequest$onSizeReady() -> EngineRunnable$run() --> EngineRunnable$decodeFromSource() --> DecodeJob$decodeFromSourceData() --> GifBitmapWrapperResourceDecoder$decode() --> GifBitmapWrapperResourceDecoder$decodeBitmapWrapper() --> ImageVideoBitmapDecoder$decode() --> StreamBitmapDecoder$decode() --> Downsampler$decode()

@Override
public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
    
    // 图片真实尺寸,会先 inJustDecodeBounds 设为 true 获取再重置 false
    final int[] inDimens = getDimensions(invalidatingStream, bufferedStream, options);
    final int inWidth = inDimens[0];
    final int inHeight = inDimens[1];

    // 图片旋转角度
    final int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
    final int sampleSize = getRoundedSampleSize(degreesToRotate, inWidth, inHeight, outWidth, outHeight);

    // 生成 Bitmap
    final Bitmap downsampled =
                    downsampleWithSize(invalidatingStream, bufferedStream, options, pool, inWidth, inHeight, sampleSize,
                            decodeFormat);
}

// 根据原图尺寸计算采样率
private int getRoundedSampleSize(int degreesToRotate, int inWidth, int inHeight, int outWidth, int outHeight) {
    int targetHeight = outHeight == Target.SIZE_ORIGINAL ? inHeight : outHeight;
    int targetWidth = outWidth == Target.SIZE_ORIGINAL ? inWidth : outWidth;

    final int exactSampleSize;
    if (degreesToRotate == 90 || degreesToRotate == 270) {
        // 如果有角度旋转,要转换宽高值
        exactSampleSize = getSampleSize(inHeight, inWidth, targetWidth, targetHeight);
    } else {
        exactSampleSize = getSampleSize(inWidth, inHeight, targetWidth, targetHeight);
    }

    final int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize);

    // 如果实际图片小于设定尺寸,powerOfTwoSampleSize 是 0,采样比是 1
    return Math.max(1, powerOfTwoSampleSize);
}

int targetHeight = outHeight == Target.SIZE_ORIGINAL ? inHeight : outHeight; 在 SimpleTarget 方式中,outHeight 就是 Target.SIZE_ORIGINAL,这样 targetWidth,targetHeight 就是图片原尺寸。而假设外界设置宽高为 500x400,那么 targetWidth 为 500,targetHeight 为 400。

其中 getSampleSize() 是抽象方法,内部有个静态实例 AT_LEAST,此时用的就是它(StreamBitmapDecoder 初始化时传的,具体逻辑未看)

public static final Downsampler AT_LEAST = new Downsampler() {
    @Override
    protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) {
        // min{8688/400, 5792/500}=11
        return Math.min(inHeight / outHeight, inWidth / outWidth);
    }
    // ...
};

因为 inSampleSize 需要是 2 的指数,所以执行 Integer.highestOneBit(exactSampleSize); 将二进制最高位后面的全变成 0,这样 11 就变成了 8。

private Bitmap downsampleWithSize(MarkEnforcingInputStream is, RecyclableBufferedInputStream  bufferedStream,
        BitmapFactory.Options options, BitmapPool pool, int inWidth, int inHeight, int sampleSize,
        DecodeFormat decodeFormat) {
    Bitmap.Config config = getConfig(is, decodeFormat);
    options.inSampleSize = sampleSize; // 采样率是 8 了
    options.inPreferredConfig = config;
    if ((options.inSampleSize == 1 || Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) && shouldUsePool(is)) {
        // inWidth 原图宽 5792,sampleSize 8,所以最后生成的图片宽 742
        int targetWidth = (int) Math.ceil(inWidth / (double) sampleSize);
        // 高 1086
        int targetHeight = (int) Math.ceil(inHeight / (double) sampleSize);
        setInBitmap(options, pool.getDirty(targetWidth, targetHeight, config));
    }
    return decodeStream(is, bufferedStream, options);
}

可见有三种情况:

  1. SimpleTarget 未设置宽高,加载原图尺寸
  2. 设置的宽高比原图尺寸还要大,加载原图尺寸
  3. 设置的宽高比原图尺寸小,用原图尺寸除以设置宽高,取最小值取整再向下取 2 的指数。因此最终获得的图片尺寸可能会比设置尺寸稍大

结论

Using Target.SIZE_ORIGINAL can be very inefficient or cause OOMs if your image sizes are large enough. As an alternative, You can also pass in a size to your Target’s constructor and provide those dimensions to the callback——Custom Targets

在 Glide 4 中 SimpleTarget 被标记为过时的,并且多了一些注释:

Always try to provide a size when using this class. Use {@link SimpleTarget#SimpleTarget(int, int)} whenever possible with values that are not {@link Target#SIZE_ORIGINAL}. Using {@link Target#SIZE_ORIGINAL} is unsafe if you're loading large images or are running your application on older or memory constrained devices because it can cause Glide to load very large images into memory. In some cases those images may throw {@link OutOfMemoryError} and in others they may exceed the texture limit for the device, which will prevent them from being rendered.

  • 所以在使用 SimpleTarget 的时候一定要先通过 override 设置尺寸,或者构造时传入尺寸。
  • 虽然实际图片尺寸可能比设置尺寸更大,但这样终究会有一个限制,限制在一定范围内。
  • 假设要显示的控件尺寸 20x20,图片尺寸 80x80,没有设置尺寸虽然不太可能导致 OOM,但终究也是对内存不必要的浪费。

centerCrop 和 fitCenter 对尺寸的影响

图片生成后会返回到 DecodeJob 的 decodeFromSource() 方法

public Resource decodeFromSource() throws Exception {
    Resource decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}

private Resource transformEncodeAndTranscode(Resource decoded) {
    long startTime = LogTime.getLogTime();
    Resource transformed = transform(decoded);
    // ...
}

private Resource transform(Resource decoded) {
    Resource transformed = transformation.transform(decoded, width, height);
    // ...
}

Transformation 是一个接口,默认的 transformation 是 UnitTransformation,它的 transform 就是直接返回资源

@Override
public Resource transform(Resource resource, int outWidth, int outHeight) {
    // 如果没有设置 centerCrop 或 fitCenter,图片的宽高比会保持原样
    return resource;
}

而如果配置了 centerCrop() 的话,这个 transformation 是 GifBitmapWrapperTransformation 实例,从它的 transform 进而执行到 BitmapTransformation 的 transform() 方法,然后会到 CenterCrop 类的 transform,区别主要在这里,尺寸会变成 500x400 的。

你可能感兴趣的:(Glide 加载大尺寸图片 OOM)