Glide 加載大尺寸圖片 OOM

大尺寸圖片志衣,into 參數(shù)是 SimpleTarget岳服,應(yīng)用崩潰。

圖片所占內(nèi)存計(jì)算

測試

  • 如果 Target 是 ImageView

    • xml 中布局寬高自適應(yīng)吆视,且沒有配置 override 參數(shù)典挑,加載內(nèi)存增加也就 3M 左右。
    • 如果 xml 中指定了更小的寬高或配置了 override 參數(shù)啦吧,那么內(nèi)存會更小您觉。
  • 如果 Target 不是 ImageView,比如 SimpleTarget

    • SimpleTarget 中設(shè)置了參數(shù)或者設(shè)置了 override授滓,根據(jù)尺寸的不同琳水,內(nèi)存增大不一,但基本在可控范圍般堆,10M 以內(nèi)在孝。

    • 未在構(gòu)造時(shí)傳入指定尺寸或者 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 方法將圖片加載到內(nèi)存中,然后回調(diào) onResourceReady 這個方法淮摔,可見 Java 層內(nèi)存飆升了 96M 左右浑玛,主要解碼圖片的操作。

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

      而假如再執(zhí)行 imageView.setImageBitmap(bitmap) 上噩咪,Graphics 也出現(xiàn)一個峰值顾彰,增加了近 100M

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

    主要的區(qū)別就在于 Target。對于 View胃碾,一般來說涨享,尺寸最大也就屏幕分辨率,所占內(nèi)存終究有個限制仆百,而不是 View厕隧,一些第三方的服務(wù)中的圖片多大完全不知道。

原因

Target 尺寸計(jì)算

into() 方法會執(zhí)行到 GenericRequest 類的 begin()

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

如果通過 override() 方法傳入尺寸俄周,會直接進(jìn)入 onSizeReady()吁讨,若未設(shè)置,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 構(gòu)造時(shí)沒有傳尺寸參數(shù)建丧,寬高就是 SIZE_ORIGINAL,即 Integer 的最小值波势。最后也會執(zhí)行到 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) {
    
    // 圖片真實(shí)尺寸,會先 inJustDecodeBounds 設(shè)為 true 獲取再重置 false
    final int[] inDimens = getDimensions(invalidatingStream, bufferedStream, options);
    final int inWidth = inDimens[0];
    final int inHeight = inDimens[1];

    // 圖片旋轉(zhuǎn)角度
    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);
}

// 根據(jù)原圖尺寸計(jì)算采樣率
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) {
        // 如果有角度旋轉(zhuǎn)尺铣,要轉(zhuǎn)換寬高值
        exactSampleSize = getSampleSize(inHeight, inWidth, targetWidth, targetHeight);
    } else {
        exactSampleSize = getSampleSize(inWidth, inHeight, targetWidth, targetHeight);
    }

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

    // 如果實(shí)際圖片小于設(shè)定尺寸拴曲,powerOfTwoSampleSize 是 0,采樣比是 1
    return Math.max(1, powerOfTwoSampleSize);
}

int targetHeight = outHeight == Target.SIZE_ORIGINAL ? inHeight : outHeight; 在 SimpleTarget 方式中凛忿,outHeight 就是 Target.SIZE_ORIGINAL澈灼,這樣 targetWidth,targetHeight 就是圖片原尺寸店溢。而假設(shè)外界設(shè)置寬高為 500x400叁熔,那么 targetWidth 為 500,targetHeight 為 400逞怨。

其中 getSampleSize() 是抽象方法者疤,內(nèi)部有個靜態(tài)實(shí)例 AT_LEAST,此時(shí)用的就是它(StreamBitmapDecoder 初始化時(shí)傳的叠赦,具體邏輯未看)

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);
    }
    // ...
};

因?yàn)?inSampleSize 需要是 2 的指數(shù)驹马,所以執(zhí)行 Integer.highestOneBit(exactSampleSize); 將二進(jìn)制最高位后面的全變成 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 未設(shè)置寬高规揪,加載原圖尺寸
  2. 設(shè)置的寬高比原圖尺寸還要大度苔,加載原圖尺寸
  3. 設(shè)置的寬高比原圖尺寸小,用原圖尺寸除以設(shè)置寬高暂吉,取最小值取整再向下取 2 的指數(shù)胖秒。因此最終獲得的圖片尺寸可能會比設(shè)置尺寸稍大缎患。

結(jié)論

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 被標(biāo)記為過時(shí)的,并且多了一些注釋:

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 的時(shí)候一定要先通過 override 設(shè)置尺寸阎肝,或者構(gòu)造時(shí)傳入尺寸挤渔。
  • 雖然實(shí)際圖片尺寸可能比設(shè)置尺寸更大,但這樣終究會有一個限制风题,限制在一定范圍內(nèi)判导。
  • 假設(shè)要顯示的控件尺寸 20x20,圖片尺寸 80x80沛硅,沒有設(shè)置尺寸雖然不太可能導(dǎo)致 OOM眼刃,但終究也是對內(nèi)存不必要的浪費(fèi)。

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 是一個接口摇肌,默認(rèn)的 transformation 是 UnitTransformation擂红,它的 transform 就是直接返回資源

@Override
public Resource<T> transform(Resource<T> resource, int outWidth, int outHeight) {
    // 如果沒有設(shè)置 centerCrop 或 fitCenter,圖片的寬高比會保持原樣
    return resource;
}

而如果配置了 centerCrop() 的話朦蕴,這個 transformation 是 GifBitmapWrapperTransformation 實(shí)例篮条,從它的 transform 進(jìn)而執(zhí)行到 BitmapTransformation 的 transform() 方法,然后會到 CenterCrop 類的 transform吩抓,區(qū)別主要在這里涉茧,尺寸會變成 500x400 的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疹娶,一起剝皮案震驚了整個濱河市伴栓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雨饺,老刑警劉巖钳垮,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異额港,居然都是意外死亡饺窿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門移斩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肚医,“玉大人,你說我怎么就攤上這事向瓷〕μ祝” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵猖任,是天一觀的道長你稚。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么刁赖? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任搁痛,我火速辦了婚禮,結(jié)果婚禮上乾闰,老公的妹妹穿的比我還像新娘落追。我一直安慰自己,他們只是感情好涯肩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巢钓,像睡著了一般病苗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上症汹,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天硫朦,我揣著相機(jī)與錄音,去河邊找鬼背镇。 笑死咬展,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瞒斩。 我是一名探鬼主播破婆,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼胸囱!你這毒婦竟也來了祷舀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤烹笔,失蹤者是張志新(化名)和其女友劉穎裳扯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谤职,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饰豺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了允蜈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冤吨。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖陷寝,靈堂內(nèi)的尸體忽然破棺而出锅很,到底是詐尸還是另有隱情,我是刑警寧澤凤跑,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布爆安,位于F島的核電站,受9級特大地震影響仔引,放射性物質(zhì)發(fā)生泄漏扔仓。R本人自食惡果不足惜褐奥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望翘簇。 院中可真熱鬧撬码,春花似錦、人聲如沸版保。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彻犁。三九已至叫胁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汞幢,已是汗流浹背驼鹅。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留森篷,地道東北人输钩。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像仲智,于是被迫代替她去往敵國和親买乃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容