Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 之 三級緩存(內存!!!、本地、網絡)及內存LruCache擴展 及源碼分析--- 學習和代碼講解

Android 之 三級緩存(內存!!!、本地、網絡)及內存LruCache擴展 及源碼分析--- 學習和代碼講解

編輯:關於Android編程

一. 三級緩存簡介

這裡寫圖片描述

如上圖所示,目前App中UI界面經常會涉及到圖片,特別是像“今日關注”新聞這類app中,圖片運用的幾率十分頻繁。當手機上需要顯示大量圖片類似listView、gridView控件並且用戶會上下滑動,即將浏覽過的圖片又加載一遍,若是不停的進行網絡請求,很快就會OOM,這時三級緩存顯得尤為重要,適時地利用資源,進行圖片緩存,下面就用一個新聞組圖demo進行圖片緩存演示。


1.三級緩存的順序<喎?/kf/ware/vc/" target="_blank" class="keylink">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

這裡的邏輯就是 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



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就是每張圖片對應的urlvalue就是每個圖片 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來計算變量的總大小。一旦超出大小,就開始刪除對象,從而保證內存量在規定范圍內!


呼~這篇文章總算寫完了,拖了好多天,希望對你們有幫助 :)

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved