Android Bitmap 全面解析(二)加載多張圖片的緩存處理 ...

一般少量圖片是很少出現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大小

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

出處http://www.eoeandroid.com/thread-332399-1-1.html


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

發佈了17 篇原創文章 · 獲贊 3 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章