Android 之 三級快取(記憶體!!!、本地、網路)及記憶體LruCache擴充套件 及原始碼分析--- 學習和程式碼講解
一. 三級快取簡介
如上圖所示,目前App中UI介面經常會涉及到圖片,特別是像“今日關注”新聞這類app中,圖片運用的機率十分頻繁。當手機上需要顯示大量圖片類似listView、gridView控制元件並且使用者會上下滑動,即將瀏覽過的圖片又載入一遍,若是不停的進行網路請求,很快就會OOM,這時三級快取顯得尤為重要,適時地利用資源,進行圖片快取,下面就用一個新聞組圖demo進行圖片快取演示。
1.三級快取的順序
(1)記憶體快取: 比如說需要載入圖片時,系統第一步不會直接網路請求,而是首先找第一級快取—記憶體快取
(2)本地快取: 如果記憶體快取中沒有,就會從第二級快取—本地緩衝(即sd卡)
(3)網路快取: 如果本地快取中沒有,就會從網路快取中下載圖片。
2. 三級快取級別總結
(1)記憶體快取: 速度快, 優先讀取
(2)本地快取: 速度其次, 記憶體沒有,讀本地
(3)網路快取: 速度最慢, 本地也沒有,才訪問網路
二. 程式碼實現
關於這個三級快取的實現,其實 Xutils開源專案中BitmapUtils已經替我們封裝好了,下面新建一個MyBitmapUtils,自己實現三級快取。
1.網路快取(NetCacheUtils )
/**
* 三個泛型意義:
* 第一個泛型:doInBackground裡的引數型別
* 第二個泛型: onProgressUpdate裡的引數型別
* 第三個泛型:
* onPostExecute裡的引數型別及doInBackground的返回型別
*/
private class BitmapTask extends AsyncTask<Object, Integer,Bitmap>{
//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<Object, Void, Bitmap> {
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<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap> ;
/**
* 從記憶體讀取圖片
*
* @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<String,Bitmap> mMemoryCache = new HashMap<String, Bitmap> ;
集合中有許多個物件,都被集合引用!這個引用一直在!垃圾回收器並不會回收,所以會導致記憶體溢位。以上只是一方面,而且即使它會回收這些引用的集合,可它是隔一段時間才會回收,無法及時清理記憶體!
現在我們需要解決的是:能否在引用的情況下,垃圾回收器可以照樣回收?
3.2.2 記憶體快取中的 引用級別
(1) 強引用 預設引用, 即使記憶體溢位,也不會回收
(2) 軟引用 SoftReference, 記憶體不夠時, 會考慮回收
(3) 弱引用 WeakReference 記憶體不夠時, 更會考慮回收
(4)虛引用 PhantomReference 記憶體不夠時, 最優先考慮回收!
像Person p = new Person();
就屬於強引用。回收器斷然不會回收!而虛引用則太容易被回收,所以最常用的是軟引用 和 弱引用,在需求不強烈或記憶體實在是不夠的情況下,垃圾回收器才會回收引用的物件。我們主要回收的是Bitmap物件,對集合進行包裝,使用軟引用。
//用法舉例
Bitmap bitmap = new Bitmap();
SoftReference<Bitmap> sBitmap = new SoftReference<Bitmap>(bitmap);
Bitmap bitmap2 = sBitmap.get();
( 軟引用版):
/**
* 記憶體快取工具類
*/
public class MemoryCacheUtils {
HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<String, SoftReference<Bitmap>> ;
/**
* 從記憶體讀取圖片
*
* @param url
* @return
*/
public Bitmap getBitmapFromMemory(String url) {
SoftReference<Bitmap> 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<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
mMemoryCache.put(url, bitmap);
}
}
翻譯:
在過去,我們經常會使用一種非常流行的記憶體快取技術的實現,即軟引用或弱引用 (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<String, Bitmap>((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<String, Bitmap> mMemoryCache;
public MemoryCacheUtils() {
long maxMemory = Runtime.getRuntime().maxMemory();// 獲取分配給app的記憶體大小
System.out.println("maxMemory:" + maxMemory);
mMemoryCache = new LruCache<String, Bitmap>((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<K, V> {
private final LinkedHashMap<K, V> 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<K, V> 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<K, V> 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來計算變數的總大小。一旦超出大小,就開始刪除物件,從而保證記憶體量在規定範圍內!
呼~這篇文章總算寫完了,拖了好多天,希望對你們有幫助 :)