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<Bitmap>() {
      
              @Override
              public void onResourceReady(Bitmap bitmap, GlideAnimation<? super Bitmap> glideAnimation) {
                  // imageView.setImageBitmap(bitmap);
              }
          });
      

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

      屏幕快照 2019-03-04 上午11.16.10.png

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

      屏幕快照 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 <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.

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

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 的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容