Android中高效的顯示圖片 - Bitmap的內(nèi)存模型

相對(duì)于文字來(lái)說(shuō),圖片的表達(dá)更直接、更有沖擊力、更容易吸引用戶的眼球。設(shè)計(jì)師們也理所當(dāng)然的喜歡用圖片來(lái)傳達(dá)信息。但是對(duì)于開發(fā)者來(lái)說(shuō),圖片就意味著大量的內(nèi)存開銷。要想APP在性能上有更好的表現(xiàn),我們必須處理好顯示圖片所需要的每個(gè)環(huán)節(jié)。

(本文出處:http://www.itdecent.cn/p/eadb0ef271b0)

Android中高效的顯示圖片 - 總結(jié)

前面幾篇關(guān)于高效顯示圖片的文章已經(jīng)實(shí)現(xiàn)了一個(gè)三級(jí)緩存、后臺(tái)加載、裁剪大圖的圖片加載框架??蚣艽笾氯鐖D所示,還有部分知識(shí)點(diǎn)圖里沒(méi)有體現(xiàn)(如Activity重建時(shí)利用Fragment保存數(shù)據(jù)),詳細(xì)情況可以查看之前的文章。完整代碼可以點(diǎn)擊代碼下載。

  1. 計(jì)算合適的加載尺寸,避免內(nèi)存浪費(fèi) (加載大圖);
  2. 使用后臺(tái)線程將圖片數(shù)據(jù)加載到內(nèi)存中 (非UI線程加載)
  3. 通過(guò)緩存提高加載后的圖片數(shù)據(jù)的使用率 (圖片緩存);
  4. 確認(rèn)圖片不再使用后應(yīng)盡快釋放其所占用的內(nèi)存空間。
圖片加載流程.png

管理bitmap內(nèi)存

上面第4條之所以沒(méi)有鏈接,是因?yàn)樗褪潜竟?jié)要講述的內(nèi)容。加載圖片時(shí)所申請(qǐng)的內(nèi)存位于哪里,當(dāng)圖片不再使用時(shí)這部分已經(jīng)申請(qǐng)的內(nèi)存能否被其他需要加載的圖片直接復(fù)用,當(dāng)內(nèi)存確實(shí)需要釋放時(shí)又是如何回收的?這些疑問(wèn)都會(huì)在本節(jié)內(nèi)容中找到答案。

隨著Android系統(tǒng)版本的不斷的更新,Android團(tuán)隊(duì)在圖片內(nèi)存管理方面也做了一些優(yōu)化。

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

不同Android版本對(duì)bitmap內(nèi)存管理方式不同,我們應(yīng)對(duì)癥下藥的來(lái)優(yōu)化不同版本上bitmap的內(nèi)存使用。

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

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

注意:recycle()方法是不可逆的,bitmap調(diào)用了recycle()之后就不能再使用了。使用recycle()之后的bitmap系統(tǒng)會(huì)拋出"Canvas: trying to use a recycled bitmap"的錯(cuò)誤。所以調(diào)用recycle()方法之前一定要確認(rèn)bitmap不會(huì)再使用了。

下面提供了一個(gè)使用recycle()的代碼示例。我們使用了引用計(jì)數(shù)來(lái)判斷bitmap是否是被顯示或者被緩存。當(dāng)一個(gè)bitmap不再被顯示也沒(méi)有被緩存時(shí)我們就調(diào)用bitmap的recycle()方法來(lái)釋放內(nèi)存。

    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字段。如果設(shè)置了這個(gè)字段,bitmap在加載數(shù)據(jù)時(shí)可以復(fù)用這個(gè)字段所指向的bitmap的內(nèi)存空間。新增的這種內(nèi)存復(fù)用的特性,可以優(yōu)化掉因舊bitmap內(nèi)存釋放和新bitmap內(nèi)存申請(qǐng)所帶來(lái)的性能損耗。但是,內(nèi)存能夠復(fù)用也是有條件的。比如,在Android 4.4(API level 19)之前,只有新舊兩個(gè)bitmap的尺寸一樣才能復(fù)用內(nèi)存空間。Android 4.4開始只要舊bitmap的尺寸大于等于新的bitmap就可以復(fù)用了。

下面是bitmap內(nèi)存復(fù)用的代碼示例。大致分兩步:1、不用的bitmap用軟引用保存起來(lái),以備復(fù)用;2、使用前面保存的bitmap來(lái)創(chuàng)建新的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. 使用現(xiàn)有的廢棄bitmap創(chuàng)建新的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()會(huì)去廢棄的bitmap中找一個(gè)能夠被復(fù)用的bitmap設(shè)置到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()方法用來(lái)判斷bitmap是否能夠被復(fù)用。

    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;
    }

完整代碼可以點(diǎn)擊代碼下載。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容