Android Bitmap 載入多張圖片的緩衝處理(精華二)

來源:互聯網
上載者:User

標籤:android   des   http   java   使用   os   

一般少量圖片是很少出現OOM異常的,除非單張圖片過~大~ 那麼就可以用教程一裡面的方法了
通常應用情境是listview列表載入多張圖片,為了提高效率一般要緩衝一部分圖片,這樣方便再次查看時能快速顯示~不用重新下載圖片
但是手機記憶體是很有限的~當緩衝的圖片越來越多,即使單張圖片不是很大,不過數量太多時仍然會出現OOM的情況了~
本篇則是討論多張圖片的處理問題

-----------------------------------------------------------------------

圖片緩衝的一般處理是
1.建立一個圖片緩衝池,用於存放圖片對應的bitmap對象
2.在顯示的時候,比如listview對應適配器的getView方法裡進行載入圖片的工作, 先從緩衝池通過url的key值取,如果取到圖片了直接顯示,
如果擷取不到再建立非同步線程去下載圖片(下載好後同時儲存至圖片緩衝池並顯示)

但是緩衝池不能無限大啊~不然就會異常了,所以通常我們要對緩衝池進行一定控制
需要有兩個特性: 
     總大小有個限制,不然裡面存放無限多的圖片時會記憶體溢出OOM異常
     當大小達到上限後,再添加圖片時,需要線程池能夠智能化的回收移除池內一部分圖片,這樣才能保證新圖片的顯示儲存


非同步線程下載圖片神馬的簡單,網上非同步下載任務的代碼一大堆,下載以後流資料直接decode成bitmap圖片即可
痛點在與這個圖片緩衝池的設計,現在網上的實現主要有兩種
1.軟引用/弱引用
2.LruCache

-----------------------------------------------------------------------

拓展: java中4種引用分類
官方資料串連:http://developer.android.com/reference/java/lang/ref/Reference.html
強引用
     平常使用的基本都是強引用,除非主動釋放(圖片的回收,或者==null賦值為空白等),否則會一直儲存對象到記憶體溢出為止~
軟引用     SoftReference
     在系統記憶體不夠時,會自動釋放部分軟引用所指對象~
弱引用     WeakReference
     系統偶爾回收掃描時發現弱引用則釋放對象,即和記憶體夠不夠的情況無關,完全看心情~
虛引用
     不用瞭解,其實我也不熟悉

架構基本都比較愛用這個軟應用儲存圖片作為緩衝池,這樣在圖片過多不足時,就會自動回收部分圖片,防止OOM
但是有缺點,無法控制記憶體不足時會回收哪些圖片,如果我只想回收一些不常用的,不要回收常用的圖片呢?


於是引入了二級緩衝的邏輯
即設定兩個緩衝池,一個強引用,一個軟引用, 強引用儲存常用圖片,軟應用儲存其他圖片~
強引用因為不會自動釋放對象,所以大小要進行一定限定,否則圖片過多會異常, 比如控制裡面只存放10張圖片,
然後每次往裡面添加圖片的時候,檢查如果數量超過10張這個閥值,臨界點值時,
就移除強引用裡面最不常用的那個圖片,並將其儲存至軟應用緩衝池中~


整個緩衝既作為一個整體(一級二級緩衝都是記憶體緩衝~每次顯示圖片前都要檢查整個緩衝池中有沒有圖片)
又有一定的區分(只回收二級緩衝軟引用中圖片,不回收一級緩衝中強引用的圖片~)




代碼實現
軟應用緩衝池類型作為二級緩衝: 
     HashMap<String, SoftReference<Bitmap>> mSecondLevelCache = new HashMap<String, SoftReference<Bitmap>>();
強引用作為一級緩衝,為了實現刪除最不常用對象,可以用 LinkedHashMap<String,Bitmap> 類

LinkedHashMap對象可以複寫一個removeEldestEntry,這個方法就是用來處理刪除最不常用對象邏輯的
按照之前的設計就可以這麼寫:

final int MAX_CAPACITY = 10; // 一級緩衝閾值

// 第一個參數是初始化大小
// 第二個參數0.75是載入因子為經驗值
// 第三個參數true則表示按照最近訪問量的高低排序,false則表示按照插入順序排序 
HashMap<String, Bitmap> mFirstLevelCache = new LinkedHashMap<String, Bitmap>( 
        MAX_CAPACITY / 2, 0.75f, true) { 
     // eldest 最老的對象,即移除的最不常用圖片對象
     // 傳回值 true即移除該對象,false則是不移除
    protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) { 
        if (size() > MAX_CAPACITY) {// 當緩衝池總大小超過閾值的時候,將老的值從一級緩衝搬到二級緩衝 
            mSecondLevelCache.put(eldest.getKey(), 
                    new SoftReference<Bitmap>(eldest.getValue())); 
            return true; 
        } 
        return false; 
    } 
};  


每次圖片顯示時即使用時,如果存在與緩衝中,則先將對象從緩衝中刪除,然後重新添加到一級緩衝中的最前端
會有三種情況
1.如果圖片是從一級緩衝中取出來的,則相當於把對象移到了一級緩衝池的最前端(相當於最近使用的一張圖片)~
2.如果圖片是從二級緩衝中取出來的,則會存到一級緩衝池最前端並檢測,如果超過閥值,則將最不常用的一個對象移動到二級緩衝中
3.如果緩衝中沒有,那就網上下載圖片,下載好以後儲存至一級緩衝中,同樣再進行檢測是否要移除一個對象至二級緩衝中

-----------------------------------------------------------------------

結合現執行個體子理解下(如果以上邏輯瞭解可以跳過):
美國籃球,比如有一個最高水平的聯賽NBA,還有一個次一級的聯賽NBDL~
一級聯賽NBA的排名按最近一次拿冠軍的時間由近到遠排列,
我們規定,每一季度比賽都要產生一個冠軍,冠軍可能是已有的任何一個隊伍也可能是一個民間來的新隊伍~
而當一個隊伍擷取冠軍的時候就給他加到一級隊伍NBA裡~ 由於是最近一次拿冠軍,所以加進去的時候也是排名第一

NBA作為最高水平,我們對數量是有限制的,所以每次有新冠軍產生的時候我們都做一次檢測,
如果隊伍總數量超過20支,那麼就移除排名最低即離上次獲冠軍時間最長的那個最差隊伍.


如果每季度比賽拿冠軍相當於一次圖片使用操作,那上面三種情況就對應我們例子中的:
1.NBA的隊伍拿冠軍,相當於這個隊伍排名變成了第一名~但NBA隊伍總數不變,沒有新加入來的
2.NBDL二級聯賽拿冠軍,則加入到NBA裡面,且變成了第一名~由於NBA隊伍相當於增加了一個,那就要檢測一下是否超過20支並將最差成績的擠到NBDL中
3.民間來大神了虐了全部的隊伍拿了冠軍,那直接加入NBA然後變成第一名,同樣,檢測NBA球隊數量判斷是否要擠出去一隊

NBDL球隊相當於軟應用的二級緩衝池, 不限定數量~ 多少都可以, 直到美國籃聯維護全部NBA NBDL球隊的資金不夠了(相當於圖片過多應用記憶體不足了)
則自動解散一部分球隊,落入民間,直到下一次擷取總冠軍再加入進來(相當於圖片從緩衝中移除了,下次使用要重新下載)~
那NBA就相當於一級緩衝,經常拿冠軍(相當於高頻率使用的圖片),那我們就不想因為資金不足隨機解散幾個球隊恰好就解散了NBA隊伍,
則規定資金不夠時只解散二級聯賽NBDL的隊伍~因為他們擷取比賽幾率低一點~

民間隊伍存在與聯賽系統之外(相當於不存在緩衝中的圖片), 而任何一個NBA NBDL聯賽球隊我們都可以理解為都是民間晉級過來的~
只不過從民間擷取總冠軍並加入聯賽需要一個取名字啊登記啊等等的辦手續過程(下載圖片),比較麻煩,所以我們要儘可能的少辦手續~
而聯賽隊伍(包括NBA NBDL)擷取總冠軍則不需要麻煩的手續,可以直接參加比賽去拿冠軍(直接擷取顯示)


兩個聯賽,一個常用的限定數量,一個不常用的不限定數量,但是資金不足時自動回收部分二級球隊~
相當於圖片的二級緩衝

-----------------------------------------------------------------------

Disk緩衝
可以簡單的理解為將圖片緩衝到sd卡中~

由於記憶體緩衝在程式關閉第二次進入時就清空了,對於一個十分常用的圖片比如頭像一類的~
我們希望不要每次進入應用都重新下載一遍,那就要用到disk緩衝了,直接圖片存到了本地,開啟應用時直接擷取顯示~

網上擷取圖片的大部分邏輯順序是
記憶體緩衝中擷取顯示(強引用緩衝池->弱引用緩衝池)  -> 記憶體中找不到時從sd卡緩衝中擷取顯示 -> 緩衝中都沒有再建立非同步線程下載圖片,下載完成後儲存至緩衝中

按照擷取圖片擷取效率的速度,由快到慢的依次嘗試幾個方法

以檔案的形式緩衝到SD卡中,優點是SD卡容量較大,所以可以緩衝很多圖片,且多次開啟應用都可以使用緩衝,
缺點是檔案讀寫操作會耗費一點時間,
雖然速度沒有從記憶體緩衝中擷取速度快,但是肯定比重新下載一張圖片的速度快~而且還不用每次都下載圖片浪費流量~
所以使用優先順序就介於記憶體緩衝和下載圖片之間了

注意:
sd卡緩衝一般要提前進行一下是否裝載sd卡的檢測, 還要檢測sd卡剩餘容量是否夠用的情況
程式裡也要添加註明相應的許可權

-----------------------------------------------------------------------

使用LruCache處理圖片緩衝

以上基本完全掌握了,每一張圖最好再進行一下教程(一)裡面介紹的單張縮放處理,那基本整個圖片緩衝技術就差不多了
但隨著android sdk的更新,新版本其實提供了更好的解決方案,下面介紹一下

先看老版本用的軟引用官方文檔
http://developer.android.com/reference/java/lang/ref/SoftReference.html
摘取段對軟引用的介紹
Avoid Soft References for Caching

In practice, soft references are inefficient for caching. The runtime doesn‘t have enough information on which references to clear and which to keep. Most fatally, it doesn‘t know what to do when given the choice between clearing a soft reference and growing the heap.
The lack of information on the value to your application of each reference limits the usefulness of soft references. References that are cleared too early cause unnecessary work; those that are cleared too late waste memory.

Most applications should use an android.util.LruCache instead of soft references. LruCache has an effective eviction policy and lets the user tune how much memory is allotted.

簡單翻譯一下

我們要避免用軟引用去處理緩衝 

在實踐中,軟引用在緩衝的處理上是沒有效率的。運行時沒有足夠的資訊用於判斷哪些引用要清理回收還有哪些要儲存。
最致命的,當同時面對清理軟引用和增加堆記憶體兩種選擇時它不知道做什麼。  
對於你應用的每一個引用都缺乏有價值的資訊,這一點限制了軟引用讓它的可用性十分有限。
過早清理回收的引用導致了無必要的工作; 而那些過晚清理掉的引用又浪費了記憶體。

大多數應用程式應該使用一個android.util。LruCache代替軟引用。LruCache有一個有效回收機制,讓使用者能夠調整有多少記憶體配置。



簡而言之,直接使用軟引用緩衝的話效果不咋滴~推薦使用LruCache
level12以後開始引入的,為了相容更早版本,android-support-v4包內也添加了一個LruCache類,
所以在12版本以上用的話發現有兩個包內都有這個類,其實都是一樣的~

那麼這個類是做啥的呢~這裡是官方文檔
http://developer.android.com/reference/android/util/LruCache.html

LRU的意思是Least Recently Used 即近期最少使用演算法~
眼熟吧,其實之前的二級緩衝中的那個強引用LinkedHashMap的處理邏輯其實就是一個LRU演算法
而我們查看LruCache這個類的原始碼時發現裡面其實也有一個LinkedHashMap,大概掃了眼,邏輯和我們之前自己寫的差不多
核心功能基本都是: 當添加進去新資料且達到限制的閥值時,則移除一個最少使用的資料


根據這個新的類做圖片載入的話,網上大部分的做法還是二級緩衝處理
只不過將LinkedHashMap+軟引用
替換成了LruCache+軟引用
都是二級緩衝,強引用+軟引用的結構~因為LruCache和LinkedHashMap都是差不多的處理邏輯
沒有移除軟引用的使用,而是將兩者結合了起來

根據官網的介紹來看其實軟引用效果不大,二級緩衝的處理的話,雖然能提高一點效果,但是會浪費對記憶體的消耗~
所以要不要加個軟引用的二級緩衝,具體選擇就看自己理解和實際應用情境了吧

LruCache我理解是犧牲一小部分效率,換取部分記憶體~我個人也是傾向於只使用LruCache的實現不用軟引用了,也比較簡單~


結合前面舉得例子可以理解為,直接取消NBDL二級聯賽(軟引用)~ 這樣能省下好大一筆錢(記憶體)然後投資聯賽其他方面(處理其他邏輯)
並擴充下NBA一級聯賽(LruCache)的規模~保證複用效率

-----------------------------------------------------------------------

LruCache的具體用法

之前對LinkedHashMap有了一定瞭解了,其實LruCache也差不多
類似於removeEldestEntry方法的回收邏輯,在這個類裡面已經處理好了
一般我們只需要處理對閥值的控制就行了

閥值控制的核心方法是sizeOf()方法, 該方法的意思是返回每一個value對象的大小size~
預設返回的是1~即當maxSize(通過構造方法傳入)設為10的時候就相當於限制緩衝池只保留10個對象了~
和上面LinkedHashMap的例子一個意思


但是由於圖片的大小不一,一般限定所有圖片的總大小更加合適,那我們就可以對這個sizeOf方法進行複寫
@Override
protected int sizeOf(String key, Bitmap value) {
     return value.getRowBytes() * value.getHeight();
}
這樣的話,相當於緩衝池裡每一個對象的大小都是計算它的位元組數,則在建立LruCache的時候傳入一個總size值就行了,
一般傳入應用可用記憶體的1/8大小

-----------------------------------------------------------------------

本篇是討論對於圖片數量的控制問題, 再結合教程(一)中的方法對每一張圖片進行相應處理~OOM的情況基本就可以避免了~

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.