Bitmap 的分析與使用

Bitmap的分析與使用

  • Bitmap的創建

    • 創建Bitmap的時候,Java不提供new Bitmap()的形式去創建,而是通過BitmapFactory中的靜態方法去創建,如:BitmapFactory.decodeStream(is);//通過InputStream去解析生成Bitmap(這裏就不貼BitmapFactory中創建Bitmap的方法了,大家可以自己去看它的源碼),我們跟進BitmapFactory中創建Bitmap的源碼,最終都可以追溯到這幾個native函數

          private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
                      Rect padding, Options opts);
          private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
                  Rect padding, Options opts);
          private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
          private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,
                  int length, Options opts);

      Bitmap又是Java對象,這個Java對象又是從native,也就是C/C++中產生的,所以,在Android中Bitmap的內存管理涉及到兩部分,一部分是native,另一部分是dalvik,也就是我們常說的java堆(如果對java堆與棧不瞭解的同學可以戳),到這裏基本就已經瞭解了創建Bitmap的一些內存中的特性(大家可以使用adb shell dumpsys meminfo去查看Bitmap實例化之後的內存使用情況)。

  • Bitmap的使用

    • 我們已經知道了BitmapFactory是如何通過各種資源創建Bitmap了,那麼我們如何合理的使用它呢?以下是幾個我們使用Bitmap需要關注的點

      1. Size

        • 這裏我們來算一下,在Android中,如果採用Config.ARGB_8888的參數去創建一個Bitmap這是Google推薦的配置色彩參數,也是Android4.4及以上版本默認創建Bitmap的Config參數(Bitmap.Config.inPreferredConfig的默認值),那麼每一個像素將會佔用4byte,如果一張手機照片的尺寸爲1280×720,那麼我們可以很容易的計算出這張圖片佔用的內存大小爲 1280x720x4 = 3686400(byte) = 3.5M,一張未經處理的照片就已經3.5M了! 顯而易見,在開發當中,這是我們最需要關注的問題,否則分分鐘OOM!
        • 那麼,我們一般是如何處理Size這個重要的因素的呢?,當然是調整Bitmap的大小到適合的程度啦!辛虧在BitmapFactory中,我們可以很方便的通過BitmapFactory.Options中的options.inSampleSize去設置Bitmap的壓縮比,官方給出的說法是

          If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory….For example, inSampleSize == 4 returns
          an image that is 1/4 the width/height of the original, and 1/16 the
          number of pixels. Any value <= 1 is treated the same as 1.

          很簡潔明瞭啊!也就是說,只要按計算方法設置了這個參數,就可以完成我們Bitmap的Size調整了。那麼,應該怎麼調整姿勢才比較舒服呢?下面先介紹其中一種通過InputStream的方式去創建Bitmap的方法,上一段從Gallery中獲取照片並且將圖片Size調整到合適手機尺寸的代碼:

              static final int PICK_PICS = 9;
          
              public void startGallery(){
                  Intent i = new Intent();
                  i.setAction(Intent.ACTION_PICK);
                  i.setType("image/*");
                  startActivityForResult(i,PICK_PICS);
              }
          
               private int[] getScreenWithAndHeight(){
                  WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
                  DisplayMetrics dm = new DisplayMetrics();
                  wm.getDefaultDisplay().getMetrics(dm);
                  return new int[]{dm.widthPixels,dm.heightPixels};
              }
          
              /**
               *
               * @param actualWidth 圖片實際的寬度,也就是options.outWidth
               * @param actualHeight 圖片實際的高度,也就是options.outHeight
               * @param desiredWidth 你希望圖片壓縮成爲的目的寬度
               * @param desiredHeight 你希望圖片壓縮成爲的目的高度
               * @return
               */
              private int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
                  double wr = (double) actualWidth / desiredWidth;
                  double hr = (double) actualHeight / desiredHeight;
                  double ratio = Math.min(wr, hr);
                  float n = 1.0f;
                  //這裏我們爲什麼要尋找 與ratio最接近的2的倍數呢?
                  //原因就在於API中對於inSimpleSize的註釋:最終的inSimpleSize應該爲2的倍數,我們應該向上取與壓縮比最接近的2的倍數。
                  while ((n * 2) <= ratio) {
                      n *= 2;
                  }
          
                  return (int) n;
              }
          
              @Override
              protected void onActivityResult(int requestCode, int resultCode, Intent data) {
                  if(resultCode == RESULT_OK){
                      switch (requestCode){
                          case PICK_PICS:
                              Uri uri = data.getData();
                              InputStream is = null;
                              try {
                                  is = getContentResolver().openInputStream(uri);
                              } catch (FileNotFoundException e) {
                                  e.printStackTrace();
                              }
          
                              BitmapFactory.Options options = new BitmapFactory.Options();
                              //當這個參數爲true的時候,意味着你可以在解析時候不申請內存的情況下去獲取Bitmap的寬和高
                              //這是調整Bitmap Size一個很重要的參數設置
                              options.inJustDecodeBounds = true;
                              BitmapFactory.decodeStream( is,null,options );
          
                              int realHeight = options.outHeight;
                              int realWidth = options.outWidth;
          
                              int screenWidth = getScreenWithAndHeight()[0];
          
                              int simpleSize = findBestSampleSize(realWidth,realHeight,screenWidth,300);
                              options.inSampleSize = simpleSize;
                              //當你希望得到Bitmap實例的時候,不要忘了將這個參數設置爲false
                              options.inJustDecodeBounds = false;
          
                              try {
                                  is = getContentResolver().openInputStream(uri);
                              } catch (FileNotFoundException e) {
                                  e.printStackTrace();
                              }
          
                              Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);
          
                              iv.setImageBitmap(bitmap);
          
                              try {
                                  is.close();
                                  is = null;
                              } catch (IOException e) {
                                  e.printStackTrace();
                              }
          
                              break;
                      }
                  }
                  super.onActivityResult(requestCode, resultCode, data);
              }

      我們來看看這段代碼的功效:
      壓縮前:壓縮前
      壓縮後:壓縮後
      對比條件爲:1080P的魅族Note3拍攝的高清無碼照片

      1. Reuse
        上面介紹了BitmapFactory通過InputStream去創建Bitmap的這種方式,以及BitmapFactory.Options.inSimpleSizeBitmapFactory.Options.inJustDecodeBounds的使用方法,但將單個Bitmap加載到UI是簡單的,但是如果我們需要一次性加載大量的圖片,事情就會變得複雜起來。Bitmap是吃內存大戶,我們不希望多次解析相同的Bitmap,也不希望可能不會用到的Bitmap一直存在於內存中,所以,這個場景下,Bitmap的重用變得異常的重要。
        在這裏只介紹一種BitmapFactory.Options.inBitmap的重用方式,下一篇文章會介紹使用三級緩存來實現Bitmap的重用。

        根據官方文檔在Android 3.0 引進了BitmapFactory.Options.inBitmap,如果這個值被設置了,decode方法會在加載內容的時候去重用已經存在的bitmap. 這意味着bitmap的內存是被重新利用的,這樣可以提升性能, 並且減少了內存的分配與回收。然而,使用inBitmap有一些限制。特別是在Android 4.4 之前,只支持同等大小的位圖。
        我們看來看看這個參數最基本的運用方法。

        new BitmapFactory.Options options = new BitmapFactory.Options();
        //inBitmap只有當inMutable爲true的時候是可用的。
        options.inMutable = true;
        Bitmap reusedBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.reused_btimap,options);
        options.inBitmap = reusedBitmap;

        這樣,當你在下一次decodeBitmap的時候,將設置了options.inMutable=true以及options.inBitmapOptions傳入,Android就會複用你的Bitmap了,具體實例:

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(reuseBitmap());
        }
        
        private LinearLayout reuseBitmap(){
            LinearLayout linearLayout = new LinearLayout(this);
            linearLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            linearLayout.setOrientation(LinearLayout.VERTICAL);
        
            ImageView iv = new ImageView(this);
            iv.setLayoutParams(new ViewGroup.LayoutParams(500,300));
        
            options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            //inBitmap只有當inMutable爲true的時候是可用的。
            options.inMutable = true;
            BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options);
        
            //壓縮Bitmap到我們希望的尺寸
            //確保不會OOM
            options.inSampleSize = findBestSampleSize(options.outWidth,options.outHeight,500,300);
            options.inJustDecodeBounds = false;
        
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options);
            options.inBitmap = bitmap;
        
            iv.setImageBitmap(bitmap);
        
            linearLayout.addView(iv);
        
            ImageView iv1 = new ImageView(this);
            iv1.setLayoutParams(new ViewGroup.LayoutParams(500,300));
            iv1.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options));
            linearLayout.addView(iv1);
        
            ImageView iv2 = new ImageView(this);
            iv2.setLayoutParams(new ViewGroup.LayoutParams(500,300));
            iv2.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options));
            linearLayout.addView(iv2);
        
        
            return linearLayout;
        }

        以上代碼中,我們在解析了一次一張1080P分辨率的圖片,並且設置在options.inBitmap中,然後分別decode了同一張圖片,並且傳入了相同的options。最終只佔用一份第一次解析Bitmap的內存。

      2. Recycle
        一定要記得及時回收Bitmap,否則如上分析,你的native以及dalvik的內存都會被一直佔用着,最終導致OOM

      // 先判斷是否已經回收
      if(bitmap != null && !bitmap.isRecycled()){
          // 回收並且置爲null
          bitmap.recycle();
          bitmap = null;
      }
      System.gc();
    • Enjoy Android :) 如果有誤,輕噴,歡迎指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章