如何理解與有效避免安卓加載Bitmap造成的OOM異常

結尾附加載Bitmap的方法

在Android應用裏,最耗費內存的就是圖片資源。而且在Android系統中,讀取位圖Bitmap時,移動設備會限制每個app所能夠使用的內存,早期的一些版本可能是16MB,不同的設備有不同的大小,總之會有限制,不會讓你無限制的使用。在andorid中圖片加載到內存中是以位圖的方式存儲的,在android2.3之後默認情況下使用ARGB_8888,這種方式下每個像素要使用4各字節來存儲。所以加載圖片是會佔用大量的內存。

所以,對於圖片的內存優化,就成爲了Android應用開發中比較重要的內容。
  1) 要及時回收Bitmap的內存
  Bitmap類有一個方法recycle(),從方法名可以看出意思是回收。這裏就有疑問了,Android系統有自己的垃圾回收機制,可以不定期的回收掉不使用的內存空間,當然也包括Bitmap的空間。那爲什麼還需要這個方法呢?
  Bitmap類的構造方法都是私有的,所以開發者不能直接new出一個Bitmap對象,只能通過BitmapFactory類的各種靜態方法來實例化一個Bitmap。仔細查看BitmapFactory的源代碼可以看到,生成Bitmap對象最終都是通過JNI調用方式實現的。所以,加載Bitmap到內存裏以後,是包含兩部分內存區域的。簡單的說,一部分是Java部分的,一部分是C部分的。這個Bitmap對象是由Java部分分配的,不用的時候系統就會自動回收了,但是那個對應的C可用的內存區域,虛擬機是不能直接回收的,這個只能調用底層的功能釋放。所以需要調用recycle()方法來釋放C部分的內存。從Bitmap類的源代碼也可以看到,recycle()方法裏也的確是調用了JNI方法了的。
  那如果不調用recycle(),是否就一定存在內存泄露呢?也不是的。Android的每個應用都運行在獨立的進程裏,有着獨立的內存,如果整個進程被應用本身或者系統殺死了,內存也就都被釋放掉了,當然也包括C部分的內存。
  Android對於進程的管理是非常複雜的。簡單的說,Android系統的進程分爲幾個級別,系統會在內存不足的情況下殺死一些低優先級的進程,以提供給其它進程充足的內存空間。在實際項目開發過程中,有的開發者會在退出程序的時候使用Process.killProcess(Process.myPid())的方式將自己的進程殺死,但是有的應用僅僅會使用調用Activity.finish()方法的方式關閉掉所有的Activity。
  經驗分享:
  Android手機的用戶,根據習慣不同,可能會有兩種方式退出整個應用程序:一種是按Home鍵直接退到桌面;另一種是從應用程序的退出按鈕或者按Back鍵退出程序。那麼從系統的角度來說,這兩種方式有什麼區別呢?按Home鍵,應用程序並沒有被關閉,而是成爲了後臺應用程序。按Back鍵,一般來說,應用程序關閉了,但是進程並沒有被殺死,而是成爲了空進程(程序本身對退出做了特殊處理的不考慮在內)。
  Android系統已經做了大量進程管理的工作,這些已經可以滿足用戶的需求。個人建議,應用程序在退出應用的時候不需要手動殺死自己所在的進程。對於應用程序本身的進程管理,交給Android系統來處理就可以了。應用程序需要做的,是儘量做好程序本身的內存管理工作。
   一般來說,如果能夠獲得Bitmap對象的引用,就需要及時的調用Bitmap的recycle()方法來釋放Bitmap佔用的內存空間,而不要等Android系統來進行釋放。
  下面是釋放Bitmap的示例代碼片段。

  // 先判斷是否已經回收
  if(bitmap != null && !bitmap.isRecycled()){
  // 回收並且置爲null
  bitmap.recycle();
  bitmap = null;
  }
  System.gc()

從上面的代碼可以看到,bitmap.recycle()方法用於回收該Bitmap所佔用的內存,接着將bitmap置空,最後使用System.gc()調用一下系統的垃圾回收器進行回收,可以通知垃圾回收器儘快進行回收。這裏需要注意的是,調用System.gc()並不能保證立即開始進行回收過程,而只是爲了加快回收的到來。
  如何調用recycle()方法進行回收已經瞭解了,那什麼時候釋放Bitmap的內存比較合適呢?一般來說,如果代碼已經不再需要使用Bitmap對象了,就可以釋放了。釋放內存以後,就不能再使用該Bitmap對象了,如果再次使用,就會拋出異常。所以一定要保證不再使用的時候釋放。比如,如果是在某個Activity中使用Bitmap,就可以在Activity的onStop()或者onDestroy()方法中進行回收。
  2) 捕獲異常
  因爲Bitmap是吃內存大戶,爲了避免應用在分配Bitmap內存的時候出現OutOfMemory異常以後Crash掉,需要特別注意實例化Bitmap部分的代碼。通常,在實例化Bitmap的代碼中,一定要對OutOfMemory異常進行捕獲。
  以下是代碼 

    Bitmap bitmap = null;
  try {
      // 實例化Bitmap
      bitmap = BitmapFactory.decodeFile(path);
  } catch (OutOfMemoryError e) {
      //
  }
  if (bitmap == null) {
      // 如果實例化失敗 返回默認的Bitmap對象
      return defaultBitmapMap;
  }

  初始化Bitmap對象過程中可能發生的OutOfMemory異常進行了捕獲。如果發生了OutOfMemory異常,應用不會崩潰,而是得到了一個默認的Bitmap圖。
  經驗分享:
  很多開發者會習慣性的在代碼中直接捕獲Exception。但是對於OutOfMemoryError來說,這樣做是捕獲不到的。因爲OutOfMemoryError是一種Error,而不是Exception。在此僅僅做一下提醒,避免寫錯代碼而捕獲不到OutOfMemoryError
  3) 緩存通用的Bitmap對象
  有時候,可能需要在一個Activity裏多次用到同一張圖片。比如一個Activity會展示一些用戶的頭像列表,而如果用戶沒有設置頭像的話,則會顯示一個默認頭像,而這個頭像是位於應用程序本身的資源文件中的。
  如果有類似上面的場景,就可以對同一Bitmap進行緩存。如果不進行緩存,儘管看到的是同一張圖片文件,但是使用BitmapFactory類的方法來實例化出來的Bitmap,是不同的Bitmap對象。緩存可以避免新建多個Bitmap對象,避免內存的浪費。
  經驗分享:
  Web開發者對於緩存技術是很熟悉的。其實在Android應用開發過程中,也會經常使用緩存的技術。這裏所說的緩存有兩個級別,一個是硬盤緩存,一個是內存緩存。比如說,在開發網絡應用過程中,可以將一些從網絡上獲取的數據保存到SD卡中,下次直接從SD卡讀取,而不從網絡中讀取,從而節省網絡流量。這種方式就是硬盤緩存。再比如,應用程序經常會使用同一對象,也可以放到內存中緩存起來,需要的時候直接從內存中讀取。這種方式就是內存緩存。
  4) 壓縮圖片
  如果圖片像素過大,使用BitmapFactory類的方法實例化Bitmap的過程中,需要大於8M的內存空間,就必定會發生OutOfMemory異常。這個時候該如何處理呢?如果有這種情況,則可以將圖片縮小,以減少載入圖片過程中的內存的使用,避免異常發生。
  使用BitmapFactory.Options設置inSampleSize就可以縮小圖片。屬性值inSampleSize表示縮略圖大小爲原始圖片大小的幾分之一。即如果這個值爲2,則取出的縮略圖的寬和高都是原始圖片的1/2,圖片的大小就爲原始大小的1/4。
  如果知道圖片的像素過大,就可以對其進行縮小。那麼如何才知道圖片過大呢?

   BitmapFactory.Options opts = new BitmapFactory.Options();
    // 設置inJustDecodeBounds爲true
    opts.inJustDecodeBounds = true;
  // 使用decodeFile方法得到圖片的寬和高
  BitmapFactory.decodeFile(path, opts);
  // 打印出圖片的寬和高
  Log.d("example", opts.outWidth + "," + opts.outHeight);

   項目中,可以利用上面的代碼,先獲取圖片真實的寬度和高度,然後判斷是否需要跑縮小。如果不需要縮小,設置inSampleSize的值爲1。如果需要縮小,則動態計算並設置inSampleSize的值,對圖片進行縮小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法實例化Bitmap對象前,別忘記將opts.inJustDecodeBound設置回false。否則獲取的bitmap對象還是null。
  經驗分享:
  如果程序的圖片的來源都是程序包中的資源,或者是自己服務器上的圖片,圖片的大小是開發者可以調整的,那麼一般來說,就只需要注意使用的圖片不要過大,並且注意代碼的質量,及時回收Bitmap對象,就能避免OutOfMemory異常的發生。
  如果程序的圖片來自外界,這個時候就特別需要注意OutOfMemory的發生。一個是如果載入的圖片比較大,就需要先縮小;另一個是一定要捕獲異常,避免程序Crash。

在寫程序中儘量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource直接使用圖片路徑來設置一張大圖,因爲這些函數在完成decode後,最終都是通過Java層的createBitmap來完成的,需要消耗更多內存。改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再調用上述方法將其設爲ImageView的 source。decodeStream最大的祕密在於其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。

下面爲大家整理下圖片的壓縮方法

/**
     * 從指定路徑加載圖片(紙有最大的摺疊限制,圖片同理)
     * @param pathName
     * @return
     */
public static Bitmap createBitmap(String pathName) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(pathName, options);
        if (options.outWidth < 4800 && options.outHeight < 4800) {
            if (options.outWidth < 3600 && options.outHeight < 3600) {
                if (options.outWidth < 2400 && options.outHeight < 2400) {
                    if (options.outWidth < 1200 && options.outHeight < 1200) {
                        options.inSampleSize = 1;
                    } else {
                        options.inSampleSize = 2;
                    }
                } else {
                    options.inSampleSize = 4;
                }
            } else {
                options.inSampleSize = 4;
            }
        } else {
            options.inSampleSize = 8;
        }

        options.inJustDecodeBounds = false;

        try {
            Bitmap bitmap = BitmapFactory.decodeFile(pathName, options);
            return bitmap;
        } catch (OutOfMemoryError var4) {
            var4.printStackTrace();
            return null;
        }
    }

/**
     * 質量壓縮
     *
     * @param image
     * @param maxkb
     * @return
     * @author 2015-1-5 下午1:29:58
     */
    public static Bitmap compressBitmap(Bitmap image, int maxkb) {
        // L.showlog(壓縮圖片);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 50, baos);// 質量壓縮方法,這裏100表示不壓縮,把壓縮後的數據存放到baos中
        int options = 100;
        // Log.i(test,原始大小 + baos.toByteArray().length);
        while (baos.toByteArray().length / 1024 > maxkb) { // 循環判斷如果壓縮後圖片是否大於(maxkb)50kb,大於繼續壓縮
            // Log.i(test,壓縮一次!);
            baos.reset();// 重置baos即清空baos
            options -= 10;// 每次都減少10
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);// 這裏壓縮options%,把壓縮後的數據存放到baos中
        }
        // Log.i(test,壓縮後大小 + baos.toByteArray().length);
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把壓縮後的數據baos存放到ByteArrayInputStream中
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream數據生成圖片
        return bitmap;
    }
   /**
     * http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
     * 官網:獲取壓縮後的圖片
     *
     * @param res
     * @param resId
     * @param reqWidth  所需圖片壓縮尺寸最小寬度
     * @param reqHeight 所需圖片壓縮尺寸最小高度
     * @return
     */
    public static Bitmap decodeSampledBitmapFromResource(Resources res,
                                                         int resId, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
 /**
     * 官網:獲取壓縮後的圖片
     *
     * @param reqWidth  所需圖片壓縮尺寸最小寬度
     * @param reqHeight 所需圖片壓縮尺寸最小高度
     * @return
     */
    public static Bitmap decodeSampledBitmapFromFile(String filepath,
                                                     int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(filepath, options);

        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(filepath, options);
    }
 public static Bitmap decodeSampledBitmapFromBitmap(Bitmap bitmap,
                                                       int reqWidth, int reqHeight) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 90, baos);
        byte[] data = baos.toByteArray();

        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(data, 0, data.length, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeByteArray(data, 0, data.length, options);
    }
 public static int calculateInSampleSize(BitmapFactory.Options options,
                                            int reqWidth, int reqHeight) {

        final int picheight = options.outHeight;
        final int picwidth = options.outWidth;

        int targetheight = picheight;
        int targetwidth = picwidth;
        int inSampleSize = 1;

        if (targetheight > reqHeight || targetwidth > reqWidth) {
            while (targetheight >= reqHeight && targetwidth >= reqWidth) {
                inSampleSize += 1;
                targetheight = picheight / inSampleSize;
                targetwidth = picwidth / inSampleSize;
            }
        }

        return inSampleSize;
    }

    public static Bitmap convertViewToBitmap(View view) {
        view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        view.buildDrawingCache();
        Bitmap bitmap = view.getDrawingCache();

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