Android开发经验谈Android开发Android进阶之路

Glide 加载大尺寸图片 OOM

2019-05-19  本文已影响10人  三流之路

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

图片所占内存计算

测试

原因

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 <em>not</em> {@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.

centerCrop 和 fitCenter 对尺寸的影响

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

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}

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

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

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

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

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

上一篇 下一篇

猜你喜欢

热点阅读