大家好,我叫石頭.
關(guān)于 SoftReference 在緩存中的使用問題,Android 在官方文檔 SoftReference敌呈,明確指出
Avoid Soft References for Caching
Google從Android 2.3+開始宣布說袍辞,他們要從此版本開始鞋仍,讓GC更加頻繁地去回收具有軟引用對象的內(nèi)存,好吧搅吁。威创。。動不動就被GC回收了谎懦,那我們的對象豈不就會經(jīng)常丟失肚豺?對的,這樣的話界拦,SoftReference雖然不會造成OOM吸申,但是我們的數(shù)據(jù)就會丟失,就會變的十分不可靠了
Most applications should use an
android.util.LruCache
instead ofsoft references
.LruCache
has an effective eviction policy and lets the user tune how much memory is allotted.
為什么Android明確要求開發(fā)者們放棄SoftReference
呢,官方給出的原因:
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.
在實(shí)踐中,軟引用(soft references)在緩存中是低效的,因?yàn)?code>runtime并沒有足夠的信息來判別應(yīng)該清除或者保留哪個 SoftReference(持有的對象)享甸,更無法判定當(dāng) App 要求更多內(nèi)存的時候截碴,是應(yīng)該清除 SoftReference,還是增大 App 的Heap蛉威。
當(dāng)我們聽到這句話的時候是不是感覺很合理呀,但是按照我們的理解這個根本說不過去啊日丹。
因?yàn)樵谡5?JVM
中,只要不會觸發(fā) OOM
(達(dá)到系統(tǒng)內(nèi)存上限或者到達(dá) JVM
設(shè)定的內(nèi)存上限)蚯嫌,JVM
就應(yīng)該毫不留情的增大 Heap
來維持應(yīng)用的正常運(yùn)行哲虾。 而沒有必要考慮是先清理 SoftReference
,還是增大 Heap
這種無聊的問題择示。
Android Runtime
與 JVM
不一樣的是:用戶 App
通常沒有權(quán)限來設(shè)定自己的最大可用內(nèi)存妒牙,這個是由系統(tǒng)控制的, 單個 App
使用的最大內(nèi)存容量是固定的:
Runtime.getRuntime().maxMemory()
其他就是跟 JVM
差不多了对妄,Android 在啟動每一個 App
的時候湘今,也并不是一開始就給每個 App
分配固定的上限內(nèi)存,也是按需動態(tài)分配剪菱,所以摩瞎,這應(yīng)該不是技術(shù)問題。
官方也為我們給出了原因:
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.
讓我們回顧下軟引用
- 創(chuàng)建軟引用HashMap作為緩存
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
- 向緩存中添加新Bitmap
public void addBitmapToCache(String path) {
// 強(qiáng)引用的Bitmap對象
Bitmap bitmap = BitmapFactory.decodeFile(path);
// 軟引用的Bitmap對象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
// 添加該對象到Map中使其緩存
imageCache.put(path, softBitmap);
}
注意:由于bitmap為局部變量孝常, 當(dāng)方法結(jié)束時旗们,bitmap被銷毀,其指向的內(nèi)存空間依然只有imageCache中的軟引用。
- 從緩存中讀取Bitmap
public Bitmap getBitmapByPath(String path) {
// 從緩存中取軟引用的Bitmap對象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
// 判斷是否存在軟引用
if (softBitmap == null) {
return null;
}
// 取出Bitmap對象构灸,如果由于內(nèi)存不足Bitmap被回收上渴,將取得空
Bitmap bitmap = softBitmap.get();
if(bitmap==null){
return null;
}
return bitmap;
}
軟引用釋放資源是被動的, 當(dāng)內(nèi)存不足時, GC會對其主動回收稠氮。
下面開始我們的主菜~~~
LruCache
LruCache
是對限定數(shù)量的緩存對象持有強(qiáng)引用的緩存曹阔,每一次緩存對象被訪問,都會被移動到隊(duì)列的頭部隔披。LruCache
類包含在android-support-v4
包中,使用方法和其他緩存一樣:加載圖片前判斷緩存中是否已經(jīng)存在赃份, 如果不存在就重新從圖片源加載。
我們應(yīng)該注意到了LruCache
中的前3個單詞LRU
,是不是有點(diǎn)眼熟呢,所謂LRU
奢米,即為 Least recently used
抓韩,近期最少使用策略,其實(shí)很熟悉啦鬓长,操作系統(tǒng)還是學(xué)過的谒拴,嘿嘿~~~。
與使用SoftReference
不同涉波,LruCache
內(nèi)部通過一個LinkedHashMap
保存資源的強(qiáng)引用彪薛。其控制內(nèi)存的方式是主動的,需要在內(nèi)部記錄當(dāng)前緩存大小怠蹂, 并與初始化時設(shè)置的max
值比較,如果超過少态, 就將排序最靠前(即最近最少使用)的資源從LinkedHashMap
中移除城侧。這樣, 就沒有任何引用指向資源的內(nèi)存空間了彼妻。該內(nèi)存空間無人認(rèn)領(lǐng)嫌佑, 會在GC時得到釋放。
關(guān)于LinkedHashMap
侨歉, 其是HashMap
的子類屋摇, 支持兩種排序方式, 第一種是根據(jù)插入順序排序幽邓, 第二種就是根據(jù)訪問進(jìn)行排序炮温。采用哪種排序方式由其構(gòu)造函數(shù)傳入?yún)?shù)決定。在LruCache
中牵舵, 初始化LinkedHashMap
的代碼如下:
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
其中最后一個參數(shù)柒啤, 就是是否根據(jù)訪問進(jìn)行排序。
LruCache的具體實(shí)現(xiàn):
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 獲取到可用內(nèi)存的最大值畸颅,使用內(nèi)存超出這個值會引起 OutOfMemory異常担巩。
// LruCache通過構(gòu)造函數(shù)傳入緩存值,以KB為單位没炒。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用內(nèi)存值的1/8作為緩存的大小涛癌。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 必須重寫此方法來衡量每張圖片的大小,默認(rèn)返回圖片數(shù)量。
return bitmap.getByteCount() / 1024;
}
};
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
/**2種情況:
*1.當(dāng)有條目被擠出時拳话,evicted 為true, key與oldValue
為被擠出的條目的值
*2.有條目值發(fā)生改變時先匪,evicted 為false ,使用put()替換值
* key 為替換條目的key oldValue為條目之前的值
newValue 為條目的新值
* 使用remove()時假颇, key與oldValue
為被移除的條目
*/
@Override
protected void entryRemoved(boolean evicted, String key,
Bitmap oldValue, Bitmap newValue) {
System.out.println("evicted:" + evicted + "key:" + key
+ "oldValue:" + oldValue + "newValue:" + newValue);
}
public Bitmap removeBitmapFromMemCache(String key) {
return mMemoryCache.remove(key);
}
關(guān)于recycle()調(diào)用
其實(shí)最早在使用LruCache或者軟引用的時候胚鸯, 我產(chǎn)生了這樣的疑問:GC可以釋放沒有強(qiáng)引用指向的內(nèi)存,但Bitmap的圖片資源(像素數(shù)據(jù))笨鸡, 不是保存在native層姜钳, 需要顯示調(diào)用recycle方法進(jìn)行內(nèi)存釋放嗎。而在一些人關(guān)于LruCache的博客中形耗, 看到博主回復(fù)類似問題哥桥,說該操作由LruCache幫助完成了。然而我看遍了LruCache 的源碼激涤, 也沒有看到哪里有釋放底層資源的操作拟糕,這反而更加深了我的疑惑。 后來在網(wǎng)上看到了這樣的說明倦踢, 即在Android 3.0(Level 11)及其以后送滞, Bitmap的像素數(shù)據(jù)與Bitmap的對象一起保存在Java堆中, 如此辱挥, 系統(tǒng)GC時犁嗅, 也可以一起將像素資源回收了。 要注意的是晤碘, 在使用LruCache時褂微, 千萬不要畫蛇添足, 在LruCache的entryRemoved回調(diào)中實(shí)現(xiàn)對釋放資源的手動recycle园爷。 因?yàn)殡m然該Bitmap從LinkedHashMap中被移除了宠蚂, 但我們無法得知外部是否還有對當(dāng)前Bitmap的引用。如果還有ImageView正顯示著該圖片童社, 那必然會導(dǎo)致崩潰求厕。
發(fā)現(xiàn)一堆int
類型的變量,還有一個最重要的LinkedHashMap<K,V>
這個隊(duì)列扰楼,通俗的講LinkedHashMap<K,V>
就是一個雙向鏈表存儲結(jié)構(gòu)甘改。
各個變量的意思為:
size
- LruCache
中已經(jīng)存儲的大小
maxSize
- 我們定義的LruCache
緩存最大的空間
putCount
- put
的次數(shù)(為LruCache
添加緩存對象的次數(shù))
createCount
- create
的次數(shù)
evictionCount
- 回收的次數(shù)
hitCount
- 命中的次數(shù)
missCount
- 丟失的次數(shù)
結(jié)合SoftReference和LruCache的二級緩存結(jié)構(gòu)
整個思路是:使用了系統(tǒng)提供的LruCache
類做一級緩存, 大小為運(yùn)行內(nèi)存的1/8,當(dāng)LruCache
容量要滿的時候,會自動將系統(tǒng)移除的圖片放到二級緩存中,但為了避免OOM
的問題,這里將SoftReference
軟引用加入來,當(dāng)系統(tǒng)快要OOM
的時候會自動清除里面的圖片內(nèi)存,當(dāng)然內(nèi)存充足時就會繼續(xù)保存這些二級緩存的圖片.強(qiáng)調(diào)一點(diǎn),不要用SoftReference
去做一級緩存,現(xiàn)在的java中垃圾回收加強(qiáng)了對SoftReference
軟引用的回收機(jī)制,它只適合臨時的保存一些數(shù)據(jù)緩存,并不適合長期的(相對臨時而言,并不是真正的長期).
package com.shi.quan.lurcache;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.widget.ImageView;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by liaoshiquan on 2017/2/7.
*/
public class ImageLoadManager {
public enum IMAGE_LOAD_TYPE
{
FILE_PATH,FILE_URL,FILE_RESOURCE_ID
}
private String TAG = "ImageLoadManager...";
private Context context;
private Set<ImageLoadTask> taskCollection;
/** 最大內(nèi)存 **/
final static int maxCacheSize = (int)(Runtime.getRuntime().maxMemory() / 8);
/** 建立線程安全,支持高并發(fā)的容器 **/
private static ConcurrentHashMap<String, SoftReference<Bitmap>> currentHashmap
= new ConcurrentHashMap<String, SoftReference<Bitmap>>();
public ImageLoadManager(Context context)
{
super();
this.context = context;
taskCollection = new HashSet<ImageLoadTask>();
}
private static LruCache<String, Bitmap> BitmapMemoryCache = new LruCache<String, Bitmap>(maxCacheSize)
{
@Override
protected int sizeOf(String key, Bitmap value)
{
if(value != null)
{
return value.getByteCount();
//return value.getRowBytes() * value.getHeight(); //舊版本的方法
}
else
{
return 0;
}
}
//這個方法當(dāng)LruCache的內(nèi)存容量滿的時候會調(diào)用,將oldValue的元素移除出來騰出空間給新的元素加入
@Override
protected void entryRemoved(boolean evicted, String key,Bitmap oldValue, Bitmap newValue)
{
if(oldValue != null)
{
// 當(dāng)硬引用緩存容量已滿時,會使用LRU算法將最近沒有被使用的圖片轉(zhuǎn)入軟引用緩存
currentHashmap.put(key, new SoftReference<Bitmap>(oldValue));
}
}
};
/**
* 針對提供圖片資源ID來顯示圖片的方法
* @param loadType 圖片加載類型
* @param imageResourceID 圖片資源id
* @param imageView 顯示圖片的ImageView
*/
public void setImageView(IMAGE_LOAD_TYPE loadType, int imageResourceID, ImageView imageView)
{
if(loadType == IMAGE_LOAD_TYPE.FILE_RESOURCE_ID)
{
// if(ifResourceIdExist(imageResourceID))
// {
// imageView.setImageResource(imageResourceID);
//
// }else{ //映射無法獲取該圖片,則顯示默認(rèn)圖片
// imageView.setImageResource(R.drawable.pic_default);
// }
try
{
imageView.setImageResource(imageResourceID);
return;
} catch (Exception e) {
Log.e(TAG, "Can find the imageID of "+imageResourceID);
e.printStackTrace();
}
//默認(rèn)圖片
imageView.setImageResource(R.mipmap.ic_launcher);
}
}
/**
* 針對提供圖片文件鏈接或下載鏈接來顯示圖片的方法
* @param loadType 圖片加載類型
* @param imageFilePath 圖片文件的本地文件地址或網(wǎng)絡(luò)URL的下載鏈接
* @param imageView 顯示圖片的ImageView
*/
public void setImageView(IMAGE_LOAD_TYPE loadType, String imageFilePath, ImageView imageView)
{
if(imageFilePath == null || imageFilePath.trim().equals(""))
{
imageView.setImageResource(R.mipmap.ic_launcher);
}else{
Bitmap bitmap = getBitmapFromMemoryCache(imageFilePath);
if(bitmap != null)
{
imageView.setImageBitmap(bitmap);
}
else
{
imageView.setImageResource(R.mipmap.ic_launcher);
ImageLoadTask task = new ImageLoadTask(loadType, imageView);
taskCollection.add(task);
task.execute(imageFilePath);
}
}
}
/**
* 從LruCache中獲取一張圖片灭抑,如果不存在就返回null
* @param key 鍵值可以是圖片文件的filePath,可以是圖片URL地址
* @return Bitmap對象,或者null
*/
public Bitmap getBitmapFromMemoryCache(String key)
{
try
{
if(BitmapMemoryCache.get(key) == null)
{
if(currentHashmap.get(key) != null)
{
return currentHashmap.get(key).get();
}
}
return BitmapMemoryCache.get(key);
} catch (Exception e) {
e.printStackTrace();
}
return BitmapMemoryCache.get(key);
}
/**
* 將圖片放入緩存
* @param key
* @param bitmap
*/
private void addBitmapToCache(String key, Bitmap bitmap)
{
BitmapMemoryCache.put(key, bitmap);
}
/**
* 圖片異步加載
* @author Mr.Et
*
*/
private class ImageLoadTask extends AsyncTask<String, Void, Bitmap>
{
private String imagePath;
private ImageView imageView;
private IMAGE_LOAD_TYPE loadType;
public ImageLoadTask(IMAGE_LOAD_TYPE loadType , ImageView imageView)
{
this.loadType = loadType;
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(String...params)
{
imagePath = params[0];
try
{
if(loadType == IMAGE_LOAD_TYPE.FILE_PATH)
{
if(new File(imagePath).exists())
{ //從本地FILE讀取圖片
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeFile(imagePath, opts);
//將獲取的新圖片放入緩存
addBitmapToCache(imagePath, bitmap);
return bitmap;
}
return null;
}
else if(loadType == IMAGE_LOAD_TYPE.FILE_URL)
{ //從網(wǎng)絡(luò)下載圖片
byte[] datas = getBytesOfBitMap(imagePath);
if(datas != null)
{
// BitmapFactory.Options opts = new BitmapFactory.Options();
// opts.inSampleSize = 2;
// Bitmap bitmap = BitmapFactory.decodeByteArray(datas, 0, datas.length, opts);
Bitmap bitmap = BitmapFactory.decodeByteArray(datas, 0, datas.length);
addBitmapToCache(imagePath, bitmap);
return bitmap;
}
return null;
}
} catch (Exception e) {
e.printStackTrace();
// FileUtils.saveExceptionLog(e);
//可自定義其他操作
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap)
{
try
{
if(imageView != null)
{
if(bitmap != null)
{
imageView.setImageBitmap(bitmap);
}
else
{
Log.e(TAG, "The bitmap result is null...");
}
}
else
{
Log.e(TAG, "The imageView is null...");
//獲取圖片失敗時顯示默認(rèn)圖片
imageView.setImageResource(R.mipmap.ic_launcher);
}
} catch (Exception e) {
e.printStackTrace();
// FileUtils.saveExceptionLog(e);
}
}
}
/**
* InputStream轉(zhuǎn)byte[]
* @param inStream
* @return
* @throws Exception
*/
private byte[] readStream(InputStream inStream) throws Exception{
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while( (len=inStream.read(buffer)) != -1){
outStream.write(buffer, 0, len);
}
outStream.close();
inStream.close();
return outStream.toByteArray();
}
/**
* 獲取下載圖片并轉(zhuǎn)為byte[]
* @param imgUrl
* @return
*/
private byte[] getBytesOfBitMap(String imgUrl){
try {
URL url = new URL(imgUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10 * 1000); //10s
conn.setReadTimeout(20 * 1000);
conn.setRequestMethod("GET");
conn.connect();
InputStream in = conn.getInputStream();
return readStream(in);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 該資源ID是否有效
* @param resourceId 資源ID
* @return
*/
private boolean ifResourceIdExist(int resourceId)
{
try
{
Field field = R.drawable.class.getField(String.valueOf(resourceId));
Integer.parseInt(field.get(null).toString());
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 取消所有任務(wù)
*/
public void cancelAllTask()
{
if(taskCollection != null){
for(ImageLoadTask task : taskCollection)
{
task.cancel(false);
}
}
}
}
這里為了舉例使用了AsyncTask
,但是在實(shí)際項(xiàng)目中,我們不是很推薦使用AsyncTask
,因?yàn)樗泻芏酀撛诘膯栴},這里我們推薦"泡在網(wǎng)上的日子"的一篇關(guān)于AsyncTask
跟AsyncTaskLoader
的替代品使用RxJava.Observable取代AsyncTask和AsyncTaskLoader來替代代碼中的AsyncTask網(wǎng)絡(luò)部分.
每日箴言
只有登上山頂十艾,才能看到那邊的風(fēng)光。