一. 三級緩存簡介
如上圖所示,目前App中UI界面經常會涉及到圖片,特別是像“今日關註”新聞這類app中,圖片運用的幾率十分頻繁。當手機上需要顯示大量圖片類似listView、gridView控件並且用戶會上下滑動,即將瀏覽過的圖片又加載一遍,若是不停的進行網絡請求,很快就會OOM,這時三級緩存顯得尤為重要,適時地利用資源,進行圖片緩存,下面就用一個新聞組圖demo進行圖片緩存演示。
1.三級緩存的順序vc3Ryb25nPjxiciAvPg0Ko6gxo6nE2rTmu7q05qO6ILHIyOfLtdDo0qq809TYzbzGrMqxo6zPtc2ztdrSu7K9sru74daxvdPN+MLnx+vH86OstvjKx8rXz8jV0rXa0ru8tru6tOYmbWRhc2g7xNq05ru6tOY8YnIgLz4NCqOoMqOpsb612Lu6tOajuiDI57n7xNq05ru6tObW0MO709CjrL7Nu+G007Xatv68tru6tOYmbWRhc2g7sb612Lu6s+WjqLy0c2S/qKOpPGJyIC8+DQqjqDOjqc34wue7urTmo7ogyOe5+7G+tdi7urTm1tDDu9PQo6y+zbvhtNPN+MLnu7q05tbQz8LU2M28xqyhozwvcD4NCjxwPjxpbWcgYWx0PQ==”這裡寫圖片描述” src=”/uploadfile/Collfiles/20160902/2016090209031358.png” title=”\” />
2. 三級緩存級別總結
(1)內存緩存: 速度快, 優先讀取
(2)本地緩存: 速度其次, 內存沒有,讀本地
(3)網絡緩存: 速度最慢, 本地也沒有,才訪問網絡
二. 代碼實現
關於這個三級緩存的實現,其實 Xutils開源項目中BitmapUtils已經替我們封裝好瞭,下面新建一個MyBitmapUtils,自己實現三級緩存。
1.網絡緩存(NetCacheUtils )
/** * 三個泛型意義: * 第一個泛型:doInBackground裡的參數類型 * 第二個泛型: onProgressUpdate裡的參數類型 * 第三個泛型: * onPostExecute裡的參數類型及doInBackground的返回類型 */ private class BitmapTask extends AsyncTask{ //1.預加載,運行在主線程 @Override protected void onPreExecute() { super.onPreExecute(); } //2.正在加載,運行在子線程(核心方法),可以直接異步請求 @Override protected Bitmap doInBackground(Object[] objects) { return null; } //3.更新進度的方法,運行在主線程 @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); } //4.加載結束,運行在主線程(核心方法),可以直接更新UI @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); } }
這裡的邏輯就是 doInBackground方法 異步網絡請求圖片,onPostExecute方法將圖片加載呈現出來。而 onPreExecute 的作用是預加載,使用不常。至於onProgressUpdate 可顯示出請求圖片過程中的進度,這兩個並非核心方法。
(1)doInBackground : 核心方法,請求網絡。大傢都知道請求網絡是一個耗時操作,需要在子線程中進行,這裡也確實如此,不過不需要我們再new 一個Thread ,查看源碼可知異步AsyncTask已經幫我們做到瞭。在這一步需要做的就是,獲得方法參數中的url,進行網絡請求,下載圖片獲得Bitmap.
(2)onPostExecute: 核心方法,圖片加載完成後,顯示在手機屏幕上。大傢也瞭解子線程中無法做UI更新,需要使用消息機制,給handler發送消息,在主線程中更新UI。這裡異步也都替我們做好瞭,查看源碼可知UI更新是在異步中的hanler中進行。在這一步需要做的就是,將請求獲得的Bitmap呈現到屏幕上。(更規范的是,還要將獲得的Bitmap存儲到內存和本地中,方便下次使用時可拿取緩存,不需重復請求網絡!!!)
/** * 網絡緩存工具類 * * */ public class NetCacheUtils { LocalCacheUtils mLocalCacheUtils; MemoryCacheUtils mMemoryCacheUtils; public NetCacheUtils(LocalCacheUtils localCacheUtils, MemoryCacheUtils memoryCacheUtils) { mLocalCacheUtils = localCacheUtils; mMemoryCacheUtils = memoryCacheUtils; } public void getBitmapFromNet(ImageView ivPic, String url) { BitmapTask task = new BitmapTask(); task.execute(new Object[] { ivPic, url }); } class BitmapTask extends AsyncTask { private ImageView imageView; private String url; /** * 返回的對象會自動回傳到onPostExecute裡面 */ @Override protected Bitmap doInBackground(Object... params) { imageView = (ImageView) params[0]; url = (String) params[1]; imageView.setTag(url); Bitmap bitmap = downloadBitmap(url); return bitmap; } @Override protected void onPostExecute(Bitmap result) { // 這裡的result就是doInBackground返回回來的對象 if (result != null) { String ivUrl = (String) imageView.getTag(); if (url.equals(ivUrl)) {// 確保imageview設置的是正確的圖片(因為有時候listview有重用機制,多個item會公用一個imageview對象,從而導致圖片錯亂) imageView.setImageBitmap(result); System.out.println("從網絡緩存讀取圖片"); // 向本地保存圖片文件 mLocalCacheUtils.putBitmapToLocal(url, result); // 向內存保存圖片對象 mMemoryCacheUtils.putBitmapToMemory(url, result); } } } } /** * 下載圖片 * * @param url * @return */ private Bitmap downloadBitmap(String url) { HttpURLConnection conn = null; try { conn = (HttpURLConnection) new URL(url).openConnection(); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setRequestMethod("GET"); conn.connect(); int responseCode = conn.getResponseCode(); if (responseCode == 200) { InputStream inputStream = conn.getInputStream(); //圖片壓縮 BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2;//表示壓縮比例,2表示寬高都壓縮為原來的二分之一, 面積為四分之一 options.inPreferredConfig = Config.RGB_565;//設置bitmap的格式,565可以降低內存占用 Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); return bitmap; } } catch (Exception e) { e.printStackTrace(); } finally { conn.disconnect(); } return null; } }
2. 本地緩存(LocalCacheUtils )
/** * 本地緩存工具類 * * */ public class LocalCacheUtils { private static final String LOCAL_PATH = Environment .getExternalStorageDirectory().getAbsolutePath() + "/zhbj_cache"; /** * 從本地讀取圖片 * * @param url * @return */ public Bitmap getBitmapFromLocal(String url) { try { String fileName = MD5Encoder.encode(url); File file = new File(LOCAL_PATH, fileName); if (file.exists()) { // 圖片壓縮 BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2;// 表示壓縮比例,2表示寬高都壓縮為原來的二分之一, 面積為四分之一 options.inPreferredConfig = Config.RGB_565;// 設置bitmap的格式,565可以降低內存占用 Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream( file), null, options); return bitmap; } else { return null; } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 向本地存圖片 * * @param url * @param bitmap */ public void putBitmapToLocal(String url, Bitmap bitmap) { try { String fileName = MD5Encoder.encode(url); File file = new File(LOCAL_PATH, fileName); File parent = file.getParentFile(); // 創建父文件夾 if (!parent.exists()) { parent.mkdirs(); } bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(file)); } catch (Exception e) { e.printStackTrace(); } } }
如上所示,這裡對於本地(sd卡)緩存的操作就兩個方法,比較單一,一個存儲緩存數據方法—putBitmapToLocal,一個拿取緩存數據方法—getBitmapToLocal
(1)putBitmapToLocal: 這裡我們將每個存儲圖片的文件名設為 圖片對應的url地址(MD5加密後的),判斷父文件是否存在,不存在則新建,存在則直接存儲進去。
(2)getBitmapToLocal: 先從方法參數中獲取到圖片對應的url,進行查找,若存在則將圖片的Bitmap返回回去(最好返回前先壓縮),不存在則返回null。
3. 內存緩存(LocalCacheUtils )重點!!!
3.1 HashMap版
/** * 內存緩存工具類 */ public class MemoryCacheUtils { HashMap mMemoryCache = new HashMap ; /** * 從內存讀取圖片 * * @param url * @return */ public Bitmap getBitmapFromMemory(String url) { Bitmap bitmap = mMemoryCache.get(url); return bitmap; } /** * 向內存存圖片 * * @param url * @param bitmap */ public void putBitmapToMemory(String url, Bitmap bitmap) { mMemoryCache.put(url, bitmap); } }
如上所示,這裡對於內存緩存的操作也是兩個方法,一個是設置內存緩存方法—putBitmapToLocal,一個是取內存緩存方法—getBitmapToLocal。用對象來存儲圖片,集合來存儲對象,集合都在內存裡面,所以決定用集合。
關於Android,集合就涉及到兩個,ArrayList用的多,但是取數據時必須要傳遞數組位置;但是Hashmap用的是鍵值對結構,隻要有瞭key,就可以找到對應的value。(而我們這裡的key就是每張圖片對應的url,value就是每個圖片 Bitmap對象)
3.2 軟引用版
你說以上就是內存緩存的重點?絕對不可能,Bitmap對象雖存在於集合中,但我們每次都 new 一個新的Bitmap,如果有大量的圖片,集合內存根本不夠,很快就會OOM,也就是內存溢出。也許你的手機內存很大,但是不管安卓設備總內存有多大,它隻給每個APP分配一定內存大小(16M),所以內存是非常有限的,而且在這裡 垃圾回收機制是不起作用的!
3.2.1 棧、堆、垃圾回收器
如上圖所示,內存緩存這裡涉及到棧和堆。java裡的棧一般存的是成員變量、方法聲明、引用之類的。堆裡存儲的是一個又一個的對象。(例如,new瞭一個 p,p存在棧裡,但是 person對象存儲在 堆中,p引用,指向一個person對象)。垃圾回收器會定時地從堆裡回收圾釋放內存。(例如上圖,隻要棧與堆中的連接斷掉,堆中的對象就是垃圾,回收站可進行回收。所以說,垃圾回收器有個特點:隻回收沒有引用的對象!)
再回到內存溢出上,我們
HashMap mMemoryCache = new HashMap ;
集合中有許多個對象,都被集合引用!這個引用一直在!垃圾回收器並不會回收,所以會導致內存溢出。以上隻是一方面,而且即使它會回收這些引用的集合,可它是隔一段時間才會回收,無法及時清理內存!
現在我們需要解決的是:能否在引用的情況下,垃圾回收器可以照樣回收?
3.2.2 內存緩存中的 引用級別
(1) 強引用 默認引用, 即使內存溢出,也不會回收
(2) 軟引用 SoftReference, 內存不夠時, 會考慮回收
(3) 弱引用 WeakReference 內存不夠時, 更會考慮回收
(4)虛引用 PhantomReference 內存不夠時, 最優先考慮回收!
像Person p = new Person();就屬於強引用。回收器斷然不會回收!而虛引用則太容易被回收,所以最常用的是軟引用 和 弱引用,在需求不強烈或內存實在是不夠的情況下,垃圾回收器才會回收引用的對象。我們主要回收的是Bitmap對象,對集合進行包裝,使用軟引用。
//用法舉例 Bitmap bitmap = new Bitmap(); SoftReference sBitmap = new SoftReference(bitmap); Bitmap bitmap2 = sBitmap.get();
( 軟引用版):
/** * 內存緩存工具類 */ public class MemoryCacheUtils { HashMap> mMemoryCache = new HashMap> ; /** * 從內存讀取圖片 * * @param url * @return */ public Bitmap getBitmapFromMemory(String url) { SoftReference softBitmap = mMemoryCache.get(url); if(softReference != null){ Bitmap bitmap = softReference.get(); return bitmap; } return null; } /** * 向內存存圖片 * * @param url * @param bitmap */ public void putBitmapToMemory(String url, Bitmap bitmap) { SoftReference softBitmap = new SoftReference(bitmap); mMemoryCache.put(url, bitmap); } }
3.3 LruCache 版(重點!!!)
可是自從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。這麼說來即使內存很充分的情況下,也有優先回收弱引用和軟引用。
官方文檔的截圖:
https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html官方鏈接
翻譯: 在過去,我們經常會使用一種非常流行的內存緩存技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。 但是現在已經不再推薦使用這種方式瞭,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象, 這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數據會存儲在本地的內存當中,因而無法用一種可預見的方式將其釋放, 這就有潛在的風險造成應用程序的內存溢出並崩潰。所以看到還有很多相關文章還在推薦用軟引用或弱引用 (SoftReference or WeakReference),就有點out瞭。
所以為瞭解決這個問題,google為我們推薦瞭LruCache類,這個類在 V4包 下,非常適合用來緩存圖片。
3.3.1 LruCache
Lru定義 :least recentlly used 最近最少使用的算法。(比如說,先後使用A、B、C、A、C、D對象,這時會回收的則是B對象。)
LruCache : 可以將最近最少使用的對象回收掉, 從而保證內存不會超出范圍!
3.3.2 分配空間
獲得分配給App最大的內存大小 —— 16M(16777216/1024)
long maxMemory = Runtime.getRuntime().maxMemory(); mMemoryCache = new LruCache((int) (maxMemory / 8))
但是在分配內存的過程中,切不可一次分配全部內存出去,畢竟這隻是App的一部分模塊,其餘部分還需要空間。(分配1/8 —— 2M)
3.3.2 重寫LruCache 的 sizeOf方法
這個方法要返回每個對象的大小。Lru要控制內存的總大小,所以它需要知道每個Bitmap有多大。所以需要重寫這個方法,讓開發者自己計算,返回大小。
protected int sizeOf(String key, Bitmap value) { // int byteCount = value.getByteCount(); int byteCount = value.getRowBytes() * value.getHeight();// 計算圖片大小:每行字節數*高度 return byteCount; }
( LruCache版):
private LruCache mMemoryCache; public MemoryCacheUtils() { long maxMemory = Runtime.getRuntime().maxMemory();// 獲取分配給app的內存大小 System.out.println("maxMemory:" + maxMemory); mMemoryCache = new LruCache((int) (maxMemory / 8)) { // 返回每個對象的大小 @Override protected int sizeOf(String key, Bitmap value) { // int byteCount = value.getByteCount();//有版本兼容問題 int byteCount = value.getRowBytes() * value.getHeight();// 計算圖片大小:每行字節數*高度 return byteCount; } }; } /** * 寫緩存 */ public void setMemoryCache(String url, Bitmap bitmap) { mMemoryCache.put(url, bitmap); } /** * 讀緩存 */ public Bitmap getMemoryCache(String url) { return mMemoryCache.get(url); }
4. 工具類,將以上三級緩存封裝起來
/** * 自定義三級緩存圖片加載工具 */ public class MyBitmapUtils { private NetCacheUtils mNetCacheUtils; private LocalCacheUtils mLocalCacheUtils; private MemoryCacheUtils mMemoryCacheUtils; public MyBitmapUtils() { mMemoryCacheUtils = new MemoryCacheUtils(); mLocalCacheUtils = new LocalCacheUtils(); mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils, mMemoryCacheUtils); } public void display(ImageView imageView, String url) { // 設置默認圖片 imageView.setImageResource(R.drawable.pic_item_list_default); // 優先從內存中加載圖片, 速度最快, 不浪費流量 Bitmap bitmap = mMemoryCacheUtils.getMemoryCache(url); if (bitmap != null) { imageView.setImageBitmap(bitmap); System.out.println("從內存加載圖片啦"); return; } // 其次從本地(sdcard)加載圖片, 速度快, 不浪費流量 bitmap = mLocalCacheUtils.getLocalCache(url); if (bitmap != null) { imageView.setImageBitmap(bitmap); System.out.println("從本地加載圖片啦"); // 寫內存緩存 mMemoryCacheUtils.setMemoryCache(url, bitmap); return; } // 最後從網絡下載圖片, 速度慢, 浪費流量 mNetCacheUtils.getBitmapFromNet(imageView, url); } }
以上,將工具類封裝號之後,我們可以不使用 Xutils裡的方法,使用我們自定義的MyBitmapUtils,以下代碼為調用過程。
class PhotoAdapter extends BaseAdapter { //private BitmapUtils mBitmapUtils; private MyBitmapUtils mBitmapUtils; public PhotoAdapter() { mBitmapUtils = new MyBitmapUtils(); //mBitmapUtils = new BitmapUtils(mActivity); // mBitmapUtils // .configDefaultLoadingImage(R.drawable.pic_item_list_default); }
三. 結果呈現
呈現出來的順序就是:
這是我測試之後的,如果是第一次打開這個模塊,最先使用的隻能是網絡緩存,一旦第一次進行網絡緩存後,本地緩存和內存緩存就會有相應的數據。下一次再打開此模塊時,首先加載的是本地緩存,得到Bitmap**對象後,之後進行的都是 內存緩存**瞭。
四. LruCache擴展及源碼分析(重點)
Lru 就像我們傢用的洗漱池裡小開口,水龍頭流出來的水就像是內存,所以我們的洗漱池會堵嗎?不會!如果你把口子給堵起來防水,很快水就會滿出來,就像是 內存溢出。這裡,我們來看下 V4 包下的 LruCache 源碼 。
public class LruCache { private final LinkedHashMap map;
點進去一看,Lrucache是一個泛型,它維護瞭一個 LinkedHashMap,將來在存圖片的時候,底層也是存在一個HashMap裡。
1. LruCache 的 put 方法
public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; //!!!!!!! size += safeSizeOf(key, value); //!!!!!!! previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } //!!!!!!! trimToSize(maxSize); return previous; }
我們去找它的一個put 方法。
previous = map.put(key, value);標記感嘆號地方 的 map 就是 一開始的 LinkedHashMap,底層就是對HashMap的封裝。
size += safeSizeOf(key, value);
全局維護瞭一個變量size,時時在統計集合目前對象大小。它走的就是sizeOf方法。但是源碼中方法返回的是1,
protected int sizeOf(K key, V value) { return 1; }
所以我們需要去重寫它的sizeOf方法。所以LruCache 在put 的時候都會把總大小計算出來,然後調用trimToSize(maxSize);方法,來看下此方法源碼
public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } //!!!!!!! if (size <= maxSize || map.isEmpty()) { break; } //!!!!!!! Map.Entry toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } //!!!!!!! entryRemoved(true, key, value, null); } }
一上來就是一個While循環,先不看拋出異常,直接看if判斷if (size <= maxSize || map.isEmpty()) { break; },如果內存正常,則break出去,否則
Map.Entry toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++;
通過map 拿到迭代器的第一個對象,再直接拿到key,再remove出去,所以總內存大小就減少瞭。這時繼續While循環,因為減少一個不一定符合大小,所以一直減少直到內存大小少於規定值為止!
所以LruCache所謂的算法:可以將最近最少使用的對象回收掉, 從而保證內存不會超出范圍。
其中的核心原理就在這裡,不停的刪掉開頭的key,這就是最近最少用的對象。
2. LruCache 的 get 方法
public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; }
這裡的get方法則更簡單,參數將 key 傳過來,直接從map中get出對象,再return出來就行瞭。
3. LruCache 的 核心
最核心的地方其實就是維護一個 HashMap,再設置瞭一個全局變量 size來計算變量的總大小。一旦超出大小,就開始刪除對象,從而保證內存量在規定范圍內!
呼~這篇文章總算寫完瞭,拖瞭好多天,希望對你們有幫助 :)