Fight with Bitmap OOM problem.

Reference

Description

為了買大樂透不想每次都開 APP 顯示 QR Code 很慢, 所以做了 MAX Brightness 這個 Widget.
做好之後, 的確可以方便的把亮度調最大再開 QR Code 的 screenshot, 掃完圖後也可以方便的恢復原本亮度.
朋友聽完這個 Widget 之後就問: 幹嘛不乾脆做一個功能直接把圖秀出來亮度最亮就好?
真是一語驚醒夢中人... 於是就開始做 BrightImage 這個 APP.
需求就很簡單: 選一張圖顯示, 顯示的時候調最亮, 選過一次就不用重選.

Development Note

整個 APP 開發起來很簡單, 比較麻煩的是 Bitmap 這個東西.
由於功能包含可以選圖呈現, 所以測試的時候就也亂選圖, 結果選到一張手機拍的照片後程式就 crash 了... Orz
看 log 發現是 OutOfMemory, 引發 OutOfMemory 的圖有 3264*1826 這麼大.
程式的寫法是

Bitmap bitmap = BitmapFactory.decodeFile(picturePath);
imageView.setImageBitmap(bitmap);

這樣的寫法, 小圖沒問題, 大圖就爆了. 就算沒爆, 只要橫放&直放手機或者重複幾次就會爆.
後來上網查, 發現要 recycle, 所以我就在 onPause 去把 Bitmap recycle 掉, 結果沒那麼簡單. recycle 的時機很重要, 有時候 recycle 了, 底層還沒 recycle 完, 有時是 imageView 預設的圖在佔空間.
試了多種組合都沒用. 由於每天大概只做半小時吧, 加上最近忙家裡有事, 這個問題擺了幾週..

這幾週中有試到一種方式不會 OutOfMemory.

Display display = getWindowManager().getDefaultDisplay();
imageView.setImageBitmap(Bitmap.createScaledBitmap(BitmapFactory.decodeFile(picturePath),display.getWidth(), display.getHeight(), true));

這樣的寫法, 雖然圖片在螢幕翻轉的時候圖片會被 scale 成怪怪的樣子, 但不會 OOM.
當時沒發現原因, 直到今天才突然想到: 啊! 其實存在 imageView 裡面的 Bitmap size 比較小所以不會 OOM.
加 log 去檢查發現果然沒錯 => 雖然很簡單的規則卻過很久才想到 Orz

一開始想說是不是要自己算寬高, 可是這樣程式會比較雜. 想說難道沒有相關的 API 嗎?
看 Bitmap 有個 API: Bitmap#getScaledWidth(targetDensity:int), 就去查 Density 甚麼意思.
然後發現 Bitmap#getScaledWidth(targetDensity:int) 似乎可以達到把圖縮小的目的.
重要的是不用自己去算寬高, 感覺太雜了.

一開始縮小圖片的方式只有

bitmap.getScaledHeight(DisplayMetrics.DENSITY_LOW);

一開始測試沒問題, 結果上傳到 Google Play 後自己測試又遇到 OOM!!
看 log 發現原來原本的 density 是 240, DENSITY_LOW 是 120, 用 DENSITY_LOW 去縮小圖卻沒用, 因為圖片還是太大, 還是會遇到 OOM.
想是不是還是得自己算寬高的時候, 突然想到一個很瞎的方式: catch OOM 然後縮小 targetDensity 來縮小記憶體用量.

for ( int i = 1; i < 10; i++ ) {
  int targetDensity = bitmap.getDensity() / i;
  try {
    int h = bitmap.getScaledHeight(DisplayMetrics.DENSITY_LOW);
    int w = bitmap.getScaledWidth(targetDensity);
    Log.i(getClass().getName(), "reduce density to " + targetDensity);
    imageView.setImageBitmap(Bitmap.createScaledBitmap(bitmap, w, h, true));
    break;
  } catch (OutOfMemoryError e) {
    Log.w(getClass().getName(), "OOM when targetDensity:" + targetDensity);
  }    
}

結果這個方法有用. 想想應該不會有情況是把圖片的精細度縮 1024 倍還看不到, 就設定只要測10次就好了.

PS. 這次有加可以選圖的功能, 上網查詢後發現意外的簡單.

private void selectPicture() {
  Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
  startActivityForResult(i, RESULT_LOAD_IMAGE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && null != data) {
    Uri selectedImage = data.getData();
    String[] filePathColumn = { MediaStore.Images.Media.DATA };
  
    Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
    cursor.moveToFirst();
  
    int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
    String picturePath = cursor.getString(columnIndex);
    cursor.close();
                       
    loadImage();
  }
}

沒有留言:

張貼留言

別名演算法 Alias Method

 題目 每個伺服器支援不同的 TPM (transaction per minute) 當 request 來的時候, 系統需要馬上根據 TPM 的能力隨機找到一個適合的 server. 雖然稱為 "隨機", 但還是需要有 TPM 作為權重. 解法 別名演算法...