【構建Android緩存模塊】(一)吐槽與原理分析

摘要

在我翻譯的 Google 官方系列教程中,Bitmap 系列由淺入深地介紹瞭如何正確的解碼 Bitmap ,異步線程操作以及使用 Fragments 重用等技術,並且在最後給出了非常強大的獨家祕笈:BitmapFun ,讓猿媛們得以一窺究竟 Google 的攻城師們是如何高屋建瓴地秒殺 OOM 的。

前言

在下載到 BitmapFun.rar 這個神聖的壓縮包以後,我是雙手顫抖,似乎是打開上古祕藏一般,心情激動導致久久不能自已。我還記得那天上海下着小雨,我當時霍然起身,佇立在 23 樓的窗臺,仰着頭向江水對岸的東方明珠望去,似乎這樣我鬱積已久的眼淚就不能掉下來。說到這裏,Ryan 又暗自抹了一把眼淚。短暫地忘記了過去的黑暗時光,那一個漫長的被 OOM 的淫威所折磨的盛夏。。。

最後在 Boss詫異的目光中,我回到辦公桌,按捺着內心洶涌的情緒波動,然後小心翼翼的打開 BitmapFun.rar 。當那些在洪荒時代就活躍在Android平臺的大師們書寫的篇章呈現在我眼前時,我的表情與阿寶從師父手裏得到 Dragon Scroll 時一般,永久的定格在了極度天真的期待與眼角一抽一抽的狀態。

那些泛黃的代碼在我看去,通篇只有一句話:老子看不懂!

自力更生,構建自己的緩存模塊

Google 的這個 demo 堪稱詳盡,考慮極其周詳,自然是極好的。但是當原理被層層的“特殊情況”包裝起來,原本簡單的例子變得異常複雜,幾個類之間的關係錯綜複雜,堪比吸血鬼日記幾個帥哥美女之間的關係。要理解清楚每一句代碼的含義,你一定要有理解 Matt 那人老珠黃的老孃和他和失落的好朋友 Taylor 搞在一起的覺悟。

好了,吐槽一下就收,千萬不要懷疑 Google ,人家已經仁至義盡了。 BitmapFun 中在下載後將 Bitmap 緩存起來,緩存做了兩份:LruCache 和 DiskLruCache ,分別是內存緩存和硬盤緩存。此外兩個至關重要的類是:

1
2
3
4
BitmapWorkerTask(ImageView imageView)

AsyncDrawable extends BitmapDrawable
    AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask)

BitmapWorkerTask 持有一個 WeakReference<ImageView> imageViewReference ,弱引用 ImageView ,用作異步處理加載圖片的任務。AsyncDrawable 巧妙的引用持有弱引用 WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference ,是 BitmapDrawable 的子類,這樣就可以 setImageBitmap(AsyncDrawable)

關係:AsyncDrawable 中弱引用 BitmapWorkerTask 。其實是圖片引用 ImageView 的關係,而ImageView.getDrawable 又可以獲得圖片。這種高妙的思想不是正值得我們學習麼? 

當然,這節課並不是講解官方Demo的,在講解它之前,我們先來學習一個更加簡單的緩存實現方案,使用最簡單的方式快速構建自己應用的緩存模塊,有效避免 OOM 異常。它的難度非常小也很方便理解,可以在這個緩存實現的基礎上,我們再去理解更加高妙的 BitmapFun 的緩存實現方案。

如何解決OOM

Bitmap 之所以容易引起OOM異常,原因已經在 Bitmap 系列教程中說的明明白白。但是我們至少清楚一點:一個手機屏幕再大,合理尺寸的 Bitmap 也不至於耗空所有內存,那要怎麼做才能避免 OOM 呢?

  • 加載合理尺寸的 Bitmap 
  • 避免反覆解碼、重複加載 Bitmap
  • 控制 Bitmap 的生命週期,合理回收

此外網上也有不少歪門邪道,我個人認爲是不可取的,使用這些簡單粗暴的方法,後期會爲你帶來更大的麻煩:

  • 減損圖片質量(使用過高的 inSampleSize 值)
  • 使用 decodeStream (繞過 Java 層,直接調用 JNI )
  • 強制增加heap size
  • 其他

控制Bitmap的生命週期纔是正解, BitmapFun 使用的 LruCache 是將它將最近被引用到的對象存儲在一個強引用的 LinkedHashMap 中,並且在緩存超過了指定大小之後將最近不常使用的對象釋放掉。

Memory Cache 的 Size 是受限的,因此加入 DiskLruCache ,雖然在訪問速度上遜於 Memory Cache ,但是速度也是相當可觀的。

借鑑 Google 的做法,我也將緩存做了兩份,一份是 Memory Cache ,使用弱引用的 WeakHashMap 來控制 Bitmap 的生命週期,後面會有詳細解釋。另一份嚴格來說不能算是緩存,直接將文件存儲在 SDCard 上,避免重複下載。

佛說引用,既非引用,是名引用。

關於引用,或許對於小菜鳥們不是很好理解(我碰到過太多 Java 都沒學好來做 Android 的,基礎很重要!)。我使用金剛經的著名三段論來解釋它:佛說XX,既非XX,是名XX。

這句話什麼意思呢?比如佛說大米,既可以說它不是大米,只是名字叫做大米罷了。不會因爲你爲它改名叫做大麥而改變它的本質,你叫它做水,吃到嘴裏的還是原來的味道。

關於引用,跟這個有着非常相似的共性。引用就相當於實際對象的名字,比如下面的例子:

1
2
3
4
Person p1 = new Person();
Person p2 = null;
p2 = p1;
p1 = null;

new Person()這個對象的名是 p1 ,而後你將名字改成了 p2 ,對象還是那個對象,不會因爲你將 p1 的大名蓋在 null 的頭上而改變它的本質。以上的 p1 和 p2 都是引用,它們都不過是名。

在瞭解到引用的含義後,虛擬機會告訴你,被引用的對象處於可獲得( reachable )狀態,它是你的好管家,既然你要用它,它就不會回收它。(你想想如果你正在吃一隻烤鴨,人家突然一把搶了過去扔垃圾桶了你什麼感覺。)

如果在上面的那段程序後面加上 p2 = null,Person 這個對象就沒有任何引用指向它了,垃圾回收器會在不確定的時間進行回收。(你都把東西扔了,總不能不讓人家收破爛吧?)

如果你想繼續持有這個對象的引用,希望可以繼續訪問,但是也允許垃圾回收器進行回收,該怎麼辦呢?(你想減肥,告訴你的好朋友說,如果察覺到你太胖了,就將你嘴裏的烤鴨搶去扔了。如果你很餓,身材也不錯,你要繼續吃。)

這個時候,我們需要藉助 Java 提供的軟/弱/虛引用。我們平時使用的如 p1 和 p2 這樣的叫做強引用(Strong Reference)。要使垃圾回收器能在內存不夠的時候,主動搶下你嘴裏的烤鴨,進行回收,需要使用這些:

  • 軟引用:SoftReference
  • 弱引用:WeakReference
  • 虛引用:PhantomReference

它們按照由強到弱的引用關係排列,虛引用相當於幾乎沒有引用。文藝青年常說的若即若離用來形容它再恰當不過了。

關於這三個引用的具體學習,詳見我提供的參考資料。這裏只是向你解釋爲什麼使用弱引用可以起到防止 Bitmap 過多而導致內存緊張的作用。

在這裏,由於我需要使用 Bitmap 和名字的 key-value 對應關係,我使用 Java 提供的 WeakHashMap(String key, Bitmap value) ,顧名思義,它用來保存 WeakReference ,並且確保每個 key 只對應一個值,在內存不夠的時候,垃圾回收器會進行回收。當 key 值索引不到 Bitmap ,再進行其他的操作。

原理示意圖

我將原理畫成圖,以便大家的理解。主體有三個,分別是 UI ,緩存模塊和數據源。它們之間的關係如下:

原理示意圖原理示意圖

① UI:請求數據,使用唯一的 Key 值索引 Memory Cache 中的 Bitmap 。

② 內存緩存:緩存搜索,如果能找到 Key 值對應的 Bitmap ,則返回數據。否則執行第三步。

③ 硬盤存儲:使用唯一 Key 值對應的文件名,檢索 SDCard 上的文件。

④ 如果有對應文件,使用 BitmapFactory.decode* 方法,解碼 Bitmap 並返回數據,同時將數據寫入緩存。如果沒有對應文件,執行第五步。

⑤ 下載圖片:啓動異步線程,從數據源下載數據(Web)。

⑥ 若下載成功,將數據同時寫入硬盤和緩存,並將 Bitmap 顯示在 UI 中。

總結:這節課除了吐槽,主要的還是原理分析。如果你有更好的緩存方案,歡迎提出。下節課將講解具體的 Memory Cache 和 FileCache 如何實現。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章