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

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

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

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

前面幾篇關(guān)于高效顯示圖片的文章已經(jīng)實現(xiàn)了一個三級緩存碘饼、后臺加載、裁剪大圖的圖片加載框架悲伶“眨框架大致如圖所示,還有部分知識點圖里沒有體現(xiàn)(如Activity重建時利用Fragment保存數(shù)據(jù))拢切,詳細情況可以查看之前的文章蒂萎。完整代碼可以點擊代碼下載。

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

管理bitmap內(nèi)存

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

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

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

不同Android版本對bitmap內(nè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)會拋出"Canvas: trying to use a recycled bitmap"的錯誤轿塔。所以調(diào)用recycle()方法之前一定要確認bitmap不會再使用了特愿。

下面提供了一個使用recycle()的代碼示例仲墨。我們使用了引用計數(shù)來判斷bitmap是否是被顯示或者被緩存。當一個bitmap不再被顯示也沒有被緩存時我們就調(diào)用bitmap的recycle()方法來釋放內(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è)置了這個字段,bitmap在加載數(shù)據(jù)時可以復用這個字段所指向的bitmap的內(nèi)存空間毒嫡。新增的這種內(nèi)存復用的特性癌蚁,可以優(yōu)化掉因舊bitmap內(nèi)存釋放和新bitmap內(nèi)存申請所帶來的性能損耗。但是兜畸,內(nèi)存能夠復用也是有條件的努释。比如,在Android 4.4(API level 19)之前咬摇,只有新舊兩個bitmap的尺寸一樣才能復用內(nèi)存空間伐蒂。Android 4.4開始只要舊bitmap的尺寸大于等于新的bitmap就可以復用了。

下面是bitmap內(nèi)存復用的代碼示例菲嘴。大致分兩步:1饿自、不用的bitmap用軟引用保存起來,以備復用龄坪;2昭雌、使用前面保存的bitmap來創(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()會去廢棄的bitmap中找一個能夠被復用的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()方法用來判斷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;
    }

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妓局,一起剝皮案震驚了整個濱河市总放,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌好爬,老刑警劉巖局雄,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異存炮,居然都是意外死亡炬搭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門穆桂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宫盔,“玉大人,你說我怎么就攤上這事享完∽瓢牛” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵般又,是天一觀的道長彼绷。 經(jīng)常有香客問我巍佑,道長,這世上最難降的妖魔是什么苛预? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任句狼,我火速辦了婚禮,結(jié)果婚禮上热某,老公的妹妹穿的比我還像新娘腻菇。我一直安慰自己,他們只是感情好昔馋,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布筹吐。 她就那樣靜靜地躺著,像睡著了一般秘遏。 火紅的嫁衣襯著肌膚如雪丘薛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天邦危,我揣著相機與錄音洋侨,去河邊找鬼。 笑死倦蚪,一個胖子當著我的面吹牛希坚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陵且,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼裁僧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了慕购?” 一聲冷哼從身側(cè)響起聊疲,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沪悲,沒想到半個月后获洲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡殿如,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年型檀,在試婚紗的時候發(fā)現(xiàn)自己被綠了茸时。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帚稠。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡向瓷,死狀恐怖栈幸,靈堂內(nèi)的尸體忽然破棺而出颖御,到底是詐尸還是另有隱情扎阶,我是刑警寧澤躲胳,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布蒜鸡,位于F島的核電站胯努,受9級特大地震影響牢裳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叶沛,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一蒲讯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧灰署,春花似錦判帮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肴茄,卻和暖如春晌畅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背寡痰。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工抗楔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拦坠。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓连躏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贪婉。 傳聞我的和親對象是個殘疾皇子反粥,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容