softReference+LruCache優(yōu)化Android緩存

近眼看世界

大家好,我叫石頭.

關(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 of soft references. LruCachehas 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.


在實踐中,軟引用(soft references)在緩存中是低效的,因為runtime并沒有足夠的信息來判別應(yīng)該清除或者保留哪個 SoftReference(持有的對象),更無法判定當(dāng) App 要求更多內(nèi)存的時候,是應(yīng)該清除 SoftReference,還是增大 App 的Heap。

當(dāng)我們聽到這句話的時候是不是感覺很合理呀,但是按照我們的理解這個根本說不過去啊。

因為在正常的 JVM中,只要不會觸發(fā) OOM(達(dá)到系統(tǒng)內(nèi)存上限或者到達(dá) JVM 設(shè)定的內(nèi)存上限),JVM 就應(yīng)該毫不留情的增大 Heap 來維持應(yīng)用的正常運(yùn)行。 而沒有必要考慮是先清理 SoftReference,還是增大 Heap 這種無聊的問題。

Android RuntimeJVM 不一樣的是:用戶 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)引用的緩存,每一次緩存對象被訪問,都會被移動到隊列的頭部。LruCache類包含在android-support-v4包中,使用方法和其他緩存一樣:加載圖片前判斷緩存中是否已經(jīng)存在, 如果不存在就重新從圖片源加載。

我們應(yīng)該注意到了LruCache中的前3個單詞LRU,是不是有點眼熟呢,所謂LRU,即為 Least recently used,近期最少使用策略,其實很熟悉啦,操作系統(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的具體實現(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)用
其實最早在使用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)中實現(xiàn)對釋放資源的手動recycle。 因為雖然該Bitmap從LinkedHashMap中被移除了, 但我們無法得知外部是否還有對當(dāng)前Bitmap的引用。如果還有ImageView正顯示著該圖片, 那必然會導(dǎo)致崩潰。

LruCache源碼

發(fā)現(xiàn)一堆int類型的變量,還有一個最重要的LinkedHashMap<K,V> 這個隊列,通俗的講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)一點,不要用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,但是在實際項目中,我們不是很推薦使用AsyncTask,因為它有很多潛在的問題,這里我們推薦"泡在網(wǎng)上的日子"的一篇關(guān)于AsyncTaskAsyncTaskLoader的替代品使用RxJava.Observable取代AsyncTask和AsyncTaskLoader來替代代碼中的AsyncTask網(wǎng)絡(luò)部分.


每日箴言

只有登上山頂,才能看到那邊的風(fēng)光。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容