Android中高效的顯示圖片 - Bitmap的內存模型

相對于文字來說,圖片的表達更直接、更有沖擊力、更容易吸引用戶的眼球。設計師們也理所當然的喜歡用圖片來傳達信息。但是對于開發者來說,圖片就意味著大量的內存開銷。要想APP在性能上有更好的表現,我們必須處理好顯示圖片所需要的每個環節。

(本文出處:http://www.lxweimin.com/p/eadb0ef271b0)

Android中高效的顯示圖片 - 總結

前面幾篇關于高效顯示圖片的文章已經實現了一個三級緩存、后臺加載、裁剪大圖的圖片加載框架。框架大致如圖所示,還有部分知識點圖里沒有體現(如Activity重建時利用Fragment保存數據),詳細情況可以查看之前的文章。完整代碼可以點擊代碼下載。

  1. 計算合適的加載尺寸,避免內存浪費 (加載大圖)
  2. 使用后臺線程將圖片數據加載到內存中 (非UI線程加載)
  3. 通過緩存提高加載后的圖片數據的使用率 (圖片緩存)
  4. 確認圖片不再使用后應盡快釋放其所占用的內存空間。
圖片加載流程.png

管理bitmap內存

上面第4條之所以沒有鏈接,是因為它就是本節要講述的內容。加載圖片時所申請的內存位于哪里,當圖片不再使用時這部分已經申請的內存能否被其他需要加載的圖片直接復用,當內存確實需要釋放時又是如何回收的?這些疑問都會在本節內容中找到答案。

隨著Android系統版本的不斷的更新,Android團隊在圖片內存管理方面也做了一些優化。

  • 在Android 2.2 (API level 8)及其以下版本上,垃圾回收線程工作時,APP線程就得暫停,這一特性無疑會降低APP的性能。 Android 2.3開始實現了并發垃圾回收,這意味著一個bitmap對象不再任何被引用持有時,它所占有的內存空間會很快的被回收。
  • 在Android 2.3.3 (API level 10)及其以下版本上,bitmap的ARGB數據(backing pixel data)是存在native內存里的,而bitmap對象本身是存在Dalvik的堆里的。當bitmap對象不再被引用時,Dalvik的堆里的內存可以被垃圾回收期回收,但是native部分的內存卻不會同步被回收。如果需要頻繁的加載很多bitmap到內存中,即使Java層已經及時的釋放掉不用bitmap,依舊有可能引起OOM。幸運的是從Android 3.0 (API level 11)開始,bitmap的ARGB數據和bitmap對象一起存在Dalvik的堆里了。這樣bitmap對象和它的ARGB數據就可以同步回收了。
Android2.3上bitmap的內存模型
Android3.0上bitmap的內存模型

不同Android版本對bitmap內存管理方式不同,我們應對癥下藥的來優化不同版本上bitmap的內存使用。

Android 2.3.3 (API level 10)及其以下版本

在Android 2.3.3 (API level 10)及其以下版本上,Android開發文檔推薦我們使用 recycle()方法。recycle()方法可以使APP盡可能快的回收bitmap所使用的native內存。

注意:recycle()方法是不可逆的,bitmap調用了recycle()之后就不能再使用了。使用recycle()之后的bitmap系統會拋出"Canvas: trying to use a recycled bitmap"的錯誤。所以調用recycle()方法之前一定要確認bitmap不會再使用了。

下面提供了一個使用recycle()的代碼示例。我們使用了引用計數來判斷bitmap是否是被顯示或者被緩存。當一個bitmap不再被顯示也沒有被緩存時我們就調用bitmap的recycle()方法來釋放內存。

    private int mCacheRefCount = 0;
    private int mDisplayRefCount = 0;
    ...
    // Notify the drawable that the displayed state has changed.
    // Keep a count to determine when the drawable is no longer displayed.
    public void setIsDisplayed(boolean isDisplayed) {    
        synchronized (this) {        
            if (isDisplayed) {            
                mDisplayRefCount++;            
                mHasBeenDisplayed = true;        
            } else {            
                mDisplayRefCount--;        
            }    
        }    

        // Check to see if recycle() can be called.    
        checkState();
    }

    // Notify the drawable that the cache state has changed.
    // Keep a count to determine when the drawable is no longer being cached.
    public void setIsCached(boolean isCached) {    
        synchronized (this) {        
            if (isCached) {            
                mCacheRefCount++;        
            } else {            
                mCacheRefCount--;        
            }    
        }    

        // Check to see if recycle() can be called.    
        checkState();
    }

    private synchronized void checkState() {    
    // If the drawable cache and display ref counts = 0, and this drawable has been displayed, then recycle.    
        if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) {        
            getBitmap().recycle();    
        }
    }

    private synchronized boolean hasValidBitmap() {    
        Bitmap bitmap = getBitmap();    
        return bitmap != null && !bitmap.isRecycled();
    }
Android 3.0 (API level 11)及其以上版本

Android 3.0 開始引入了BitmapFactory.Options.inBitmap字段。如果設置了這個字段,bitmap在加載數據時可以復用這個字段所指向的bitmap的內存空間。新增的這種內存復用的特性,可以優化掉因舊bitmap內存釋放和新bitmap內存申請所帶來的性能損耗。但是,內存能夠復用也是有條件的。比如,在Android 4.4(API level 19)之前,只有新舊兩個bitmap的尺寸一樣才能復用內存空間。Android 4.4開始只要舊bitmap的尺寸大于等于新的bitmap就可以復用了。

下面是bitmap內存復用的代碼示例。大致分兩步:1、不用的bitmap用軟引用保存起來,以備復用;2、使用前面保存的bitmap來創建新的bitmap。

  1. 保存廢棄的bitmap
        Set<SoftReference<Bitmap>> mReusableBitmaps;
        private LruCache<String, BitmapDrawable> mMemoryCache;

        // If you're running on Honeycomb or newer, create a
        // synchronized HashSet of references to reusable bitmaps.
        if (Utils.hasHoneycomb()) {    
            mReusableBitmaps = Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
        }

        mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {    
            // Notify the removed entry that is no longer being cached.    
            @Override    
            protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) {        
                if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {            
                    // The removed entry is a recycling drawable, so notify it            
                    // that it has been removed from the memory cache.            
                    ((RecyclingBitmapDrawable) oldValue).setIsCached(false);        
                } else {            
                    // The removed entry is a standard BitmapDrawable.            
                    if (Utils.hasHoneycomb()) {                
                        // We're running on Honeycomb or later, so add the bitmap                
                        // to a SoftReference set for possible use with inBitmap later.                
                        mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));            
                    }        
                }    
            }
        ....
        }
  1. 使用現有的廢棄bitmap創建新的bitmap
        public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight, ImageCache cache) {    
            final BitmapFactory.Options options = new BitmapFactory.Options();    
            ...    
            BitmapFactory.decodeFile(filename, options);    
            ...    
            // If we're running on Honeycomb or newer, try to use inBitmap.    
            if (Utils.hasHoneycomb()) {        
                addInBitmapOptions(options, cache);    
            }    
            ...    
            return BitmapFactory.decodeFile(filename, options);
        }

上面代碼片段中使用的addInBitmapOptions()會去廢棄的bitmap中找一個能夠被復用的bitmap設置到inBitmap字段。

    private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {    
        // inBitmap only works with mutable bitmaps, so force the decoder to return mutable bitmaps.    
        options.inMutable = true;    

        if (cache != null) {        
            // Try to find a bitmap to use for inBitmap. 
            Bitmap inBitmap = cache.getBitmapFromReusableSet(options);        
            if (inBitmap != null) {            
                // If a suitable bitmap has been found, set it as the value of inBitmap.            
                options.inBitmap = inBitmap;        
            }    
        }
    }

    // This method iterates through the reusable bitmaps, looking for one to use for inBitmap:
    protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {        
      Bitmap bitmap = null;    
      if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {        
          synchronized (mReusableBitmaps) {            
              final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();            
              Bitmap item;            
              while (iterator.hasNext()) {                
                  item = iterator.next().get();                
                  if (null != item && item.isMutable()) {                    
                      // Check to see it the item can be used for inBitmap.   
                      if (canUseForInBitmap(item, options)) {                        
                          bitmap = item;                        
                          // Remove from reusable set so it can't be used again.                        
                          iterator.remove();                        
                          break;                    
                      }                
                  } else {                    
                      // Remove from the set if the reference has been cleared.                    
                      iterator.remove();                
                  }            
              }        
          }    
      }    

      return bitmap;
    }

canUseForInBitmap()方法用來判斷bitmap是否能夠被復用。

    static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) {    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {        
            // From Android 4.4 (KitKat) onward we can re-use if the byte size of        
            // the new bitmap is smaller than the reusable bitmap candidate allocation byte count.        
            int width = targetOptions.outWidth / targetOptions.inSampleSize;        
            int height = targetOptions.outHeight / targetOptions.inSampleSize;        
            int byteCount = width * height * getBytesPerPixel(candidate.getConfig());        

            return byteCount <= candidate.getAllocationByteCount();    
        }    

        // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1    
        return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1;
    }

    /** 
      * A helper function to return the byte usage per pixel of a bitmap based on its configuration. 
      */
    static int getBytesPerPixel(Config config) {    
        if (config == Config.ARGB_8888) {        
            return 4;    
        } else if (config == Config.RGB_565) {        
            return 2;    
        } else if (config == Config.ARGB_4444) {        
            return 2;    
        } else if (config == Config.ALPHA_8) {        
            return 1;    
        }    
        return 1;
    }

完整代碼可以點擊代碼下載。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容