Android緩存策略

老婆保佑,代碼無(wú)BUG

前言

最近在刷面試題,遇到一個(gè)問(wèn)題,關(guān)于緩存的原理的,所以在這里幾個(gè)筆記,關(guān)于緩存很多大牛都說(shuō)過(guò)了,我只是做個(gè)筆記,下面的很多都是網(wǎng)上查看到的,并非原創(chuàng)

目錄

  • 一:Android 緩存策略

      1. 內(nèi)存緩存(LruCache)
    • 2.磁盤(pán)緩存(文件緩存)——DiskLruCache分析
    • 3 ASimpleCache
  • 二:使用

      1. LRU使用
      1. DiskLruCache
  • 三 源碼分析


一:Android 緩存策略

1. 內(nèi)存緩存(LruCache)

LRU,全稱Least Rencetly Used,即最近最少使用,是一種非常常用的置換算法,也即淘汰最長(zhǎng)時(shí)間未使用的對(duì)象。LRU在操作系統(tǒng)中的頁(yè)面置換算法中廣泛使用,我們的內(nèi)存或緩存空間是有限的,當(dāng)新加入一個(gè)對(duì)象時(shí),造成我們的緩存空間不足了,此時(shí)就需要根據(jù)某種算法對(duì)緩存中原有數(shù)據(jù)進(jìn)行淘汰貨刪除,而LRU選擇的是將最長(zhǎng)時(shí)間未使用的對(duì)象進(jìn)行淘汰。

2. 磁盤(pán)緩存(文件緩存)——DiskLruCache分析

JakeWharton/DiskLruCache

不同于LruCache,LruCache是將數(shù)據(jù)緩存到內(nèi)存中去,而DiskLruCache是外部緩存,例如可以將網(wǎng)絡(luò)下載的圖片永久的緩存到手機(jī)外部存儲(chǔ)中去,并可以將緩存數(shù)據(jù)取出來(lái)使用,DiskLruCache不是google官方所寫(xiě),但是得到了官方推薦

3. ASimpleCache

ASimpleCache如何使用


二:使用

1. LRU使用

(1) 實(shí)例化

看下源碼

public class LruCache<K, V> {}

鍵值對(duì)的形式

下面是往上很多標(biāo)準(zhǔn)的寫(xiě)法,用于保存圖片

    private void init_Lru() {
        //設(shè)置LruCache緩存的大小,一般為當(dāng)前進(jìn)程可用容量的1/8。
        int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
        int cacheSize = maxMemory / 8;
        // 重寫(xiě)sizeOf方法,計(jì)算出要緩存的每張圖片的大小。
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // 重寫(xiě)sizeOf方法,計(jì)算出要緩存的每張圖片的大小。
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
}

(2)保存

 public final V put(K key, V value) {}

(3)獲取

  public final V get(K key) {}

2. DiskLruCache

(1) 權(quán)限

因?yàn)橐僮魍獠看鎯?chǔ),所以必須要先加上權(quán)限:

<!-- 在SDCard中創(chuàng)建與刪除文件權(quán)限 -->  
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  
<!-- 往SDCard寫(xiě)入數(shù)據(jù)權(quán)限 -->  
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 

另外要從網(wǎng)絡(luò)下載圖片,還要加上權(quán)限:

 <uses-permission android:name="android.permission.INTERNET" />

(2)初始化DiskLruCache

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize){}

參數(shù)說(shuō)明

  • File directory 數(shù)據(jù)的緩存地址

  • int appVersion 當(dāng)前應(yīng)用程序的版本號(hào)

  • int valueCount 指定同一個(gè)key可以對(duì)應(yīng)多少個(gè)緩存文件,基本都是傳1
  • long maxSize 定最多可以緩存多少字節(jié)的數(shù)據(jù)

獲取緩存地址的方法

    /***
     *
     * @param context
     * @param uniqueName 緩存地址的名字
     * @return 緩存地址
     */
    public File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        //當(dāng)SD卡存在或者SD卡不可被移除
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            // 路徑/sdcard/Android/data/<application package>/cache/uniqueName
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            // 路徑/data/data/<application package>/cache/uniqueName
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

獲取App版本

    /***
     * 
     * @param context
     * @return App 版本
     */
    public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

初始化

        File test = getDiskCacheDir(this, "Bitmap");
        int appVersion = getAppVersion(this);
        long maxSize = 10 * 1024 * 1024;
        try {
            mDiskLruCache = DiskLruCache.open(test, appVersion, 1, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

(3) 存數(shù)據(jù)

String key = hashKeyForDisk(url);  
DiskLruCache.Editor editor = mDiskLruCache.edit(key); 
OuputStream os = editor.newOutputStream(0); 


//進(jìn)行提交才能使寫(xiě)入生效
 editor.commit();
//表示放棄此次寫(xiě)入
 editor.abort();

生成MD5


    public String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

(4)取數(shù)據(jù)

 DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
if (snapShot != null) {  
    InputStream is = snapShot.getInputStream(0);  
}

(5)刪除數(shù)據(jù)

public synchronized boolean remove(String key) throws IOException

(6)其他API

  • size()
    這個(gè)方法會(huì)返回當(dāng)前緩存路徑下所有緩存數(shù)據(jù)的總字節(jié)數(shù),以byte為單位,如果應(yīng)用程序中需要在界面上顯示當(dāng)前緩存數(shù)據(jù)的總大小,就可以通過(guò)調(diào)用這個(gè)方法計(jì)算出來(lái)

  • flush()
    這個(gè)方法用于將內(nèi)存中的操作記錄同步到日志文件(也就是journal文件)當(dāng)中。這個(gè)方法非常重要,因?yàn)镈iskLruCache能夠正常工作的前提就是要依賴于journal文件中的內(nèi)容。前面在講解寫(xiě)入緩存操作的時(shí)候我有調(diào)用過(guò)一次這個(gè)方法,但其實(shí)并不是每次寫(xiě)入緩存都要調(diào)用一次flush()方法的,頻繁地調(diào)用并不會(huì)帶來(lái)任何好處,只會(huì)額外增加同步j(luò)ournal文件的時(shí)間。比較標(biāo)準(zhǔn)的做法就是在Activity的onPause()方法中去調(diào)用一次flush()方法就可以了

  • close()
    這個(gè)方法用于將DiskLruCache關(guān)閉掉,是和open()方法對(duì)應(yīng)的一個(gè)方法。關(guān)閉掉了之后就不能再調(diào)用DiskLruCache中任何操作緩存數(shù)據(jù)的方法,通常只應(yīng)該在Activity的onDestroy()方法中去調(diào)用close()方法。

  • delete()
    這個(gè)方法用于將所有的緩存數(shù)據(jù)全部刪除,比如說(shuō)網(wǎng)易新聞中的那個(gè)手動(dòng)清理緩存功能,其實(shí)只需要調(diào)用一下DiskLruCache的delete()方法就可以實(shí)現(xiàn)了。


三. 源碼分析

1. LRU 源碼分析

屬性

public class LruCache<K, V> {

private int size;// 當(dāng)前大小
private int maxSize;// 最大容量

private int putCount;// put次數(shù)
private int createCount;// 創(chuàng)建次數(shù)
private int evictionCount;// 回收次數(shù)
private int hitCount;// 命中次數(shù)
private int missCount;// 未命中次數(shù)
}

構(gòu)造方法

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
  • 設(shè)置了最大容量
  • 可以看到有一個(gè)LinkedHashMap,這個(gè)是核心,用于儲(chǔ)存緩存的對(duì)象,
  • 在LinkedHashMap中的第三個(gè)參數(shù)指定為true,表示這個(gè)LinkedHashMap將是基于數(shù)據(jù)的訪問(wèn)順序進(jìn)行排序。為false,則為插入順序(這里就是核心)

sizeOf && safeSizeOf

 private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

    /**
     * Returns the size of the entry for {@code key} and {@code value} in
     * user-defined units.  The default implementation returns 1 so that size
     * is the number of entries and max size is the maximum number of entries.
     *
     * <p>An entry's size must not change while it is in the cache.
     */
    protected int sizeOf(K key, V value) {
        return 1;
    }
  • 由于各種數(shù)據(jù)類(lèi)型大小測(cè)量的標(biāo)準(zhǔn)不統(tǒng)一,具體測(cè)量的方法應(yīng)該由使用者來(lái)實(shí)現(xiàn)

保存數(shù)據(jù)

下面代碼來(lái)自 http://blog.csdn.net/shakespeare001/article/details/51695358

/**
   * 給對(duì)應(yīng)key緩存value,并且將該value移動(dòng)到鏈表的尾部。
   */
public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

      V previous;
      synchronized (this) {
        // 記錄 put 的次數(shù)
        putCount++;
        // 通過(guò)鍵值對(duì),計(jì)算出要保存對(duì)象value的大小,并更新當(dāng)前緩存大小
        size += safeSizeOf(key, value);
        /*
         * 如果 之前存在key,用新的value覆蓋原來(lái)的數(shù)據(jù), 并返回 之前key 的value
         * 記錄在 previous
         */
        previous = map.put(key, value);
        // 如果之前存在key,并且之前的value不為null
        if (previous != null) {
            // 計(jì)算出 之前value的大小,因?yàn)榍懊鎠ize已經(jīng)加上了新的value數(shù)據(jù)的大小,此時(shí),需要再次更新size,減去原來(lái)value的大小
            size -= safeSizeOf(key, previous);
        }
      }

    // 如果之前存在key,并且之前的value不為null
    if (previous != null) {
        /*
         * previous值被剔除了,此次添加的 value 已經(jīng)作為key的 新值
         * 告訴 自定義 的 entryRemoved 方法
         */
        entryRemoved(false, key, previous, value);
    }
    //裁剪緩存容量(在當(dāng)前緩存數(shù)據(jù)大小超過(guò)了總?cè)萘縨axSize時(shí),才會(huì)真正去執(zhí)行LRU)
    trimToSize(maxSize);
      return previous;
}
  • key和value判空,說(shuō)明LruCache中不允許key和value為null;
  • 通過(guò)safeSizeOf()獲取要加入對(duì)象數(shù)據(jù)的大小,并更新當(dāng)前緩存數(shù)據(jù)的大??;
  • 將新的對(duì)象數(shù)據(jù)放入到緩存中,即調(diào)用LinkedHashMap的put方法,如果原來(lái)存在該key時(shí),直接替換掉原來(lái)的value值,并返回之前的value值,得到之前value的大小,更新當(dāng)前緩存數(shù)據(jù)的size大??;如果原來(lái)不存在該key,則直接加入緩存即可;
  • 清理緩存空間,當(dāng)我們加入一個(gè)數(shù)據(jù)時(shí)(put),為了保證當(dāng)前數(shù)據(jù)的緩存所占大小沒(méi)有超過(guò)我們指定的總大小,通過(guò)調(diào)用trimToSize()來(lái)對(duì)緩存空間進(jìn)行管理控制。

trimToSize

public void trimToSize(int maxSize) {
    /*
     * 循環(huán)進(jìn)行LRU,直到當(dāng)前所占容量大小沒(méi)有超過(guò)指定的總?cè)萘看笮?     */
    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!");
            }
            // 首先判斷當(dāng)前緩存數(shù)據(jù)大小是否超過(guò)了指定的緩存空間總大小。如果沒(méi)有超過(guò),即緩存中還可以存入數(shù)據(jù),直接跳出循環(huán),清理完畢
            if (size <= maxSize || map.isEmpty()) {
                break;
            }
            /**
             * 執(zhí)行到這,表示當(dāng)前緩存數(shù)據(jù)已超過(guò)了總?cè)萘?,需要?zhí)行LRU,即將最近最少使用的數(shù)據(jù)清除掉,直到數(shù)據(jù)所占緩存空間沒(méi)有超標(biāo);
             * 根據(jù)前面的原理分析,知道,在鏈表中,鏈表的頭結(jié)點(diǎn)是最近最少使用的數(shù)據(jù),因此,最先清除掉鏈表前面的結(jié)點(diǎn)
             */
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            // 移除掉后,更新當(dāng)前數(shù)據(jù)緩存的大小
            size -= safeSizeOf(key, value);
            // 更新移除的結(jié)點(diǎn)數(shù)量
            evictionCount++;
        }
        /*
         * 通知某個(gè)結(jié)點(diǎn)被移除,類(lèi)似于回調(diào)
         */
        entryRemoved(true, key, value, null);
    }
}
  • trimToSize()方法的作用就是為了保證當(dāng)前數(shù)據(jù)的緩存大小不能超過(guò)我們指定的緩存總大小,如果超過(guò)了,就會(huì)開(kāi)始移除最近最少使用的數(shù)據(jù),直到size符合要求。
  • trimToSize()方法在put()的時(shí)候一定會(huì)調(diào)用,在get()的時(shí)候有可能會(huì)調(diào)用。

public final V get(K key) {}獲取數(shù)據(jù)

/**
 * 根據(jù)key查詢緩存,如果該key對(duì)應(yīng)的value存在于緩存,直接返回value;
* 訪問(wèn)到這個(gè)結(jié)點(diǎn)時(shí),LinkHashMap會(huì)將它移動(dòng)到雙向循環(huán)鏈表的的尾部。
* 如果如果沒(méi)有緩存的值,則返回null。(如果開(kāi)發(fā)者重寫(xiě)了create()的話,返回創(chuàng)建的value)
*/
public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        // LinkHashMap 如果設(shè)置按照訪問(wèn)順序的話,這里每次get都會(huì)重整數(shù)據(jù)順序
        mapValue = map.get(key);
        // 計(jì)算 命中次數(shù)
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 計(jì)算 丟失次數(shù)
        missCount++;
    }

    /*
     * 官方解釋:
     * 嘗試創(chuàng)建一個(gè)值,這可能需要很長(zhǎng)時(shí)間,并且Map可能在create()返回的值時(shí)有所不同。如果在create()執(zhí)行的時(shí)
     * 候,用這個(gè)key執(zhí)行了put方法,那么此時(shí)就發(fā)生了沖突,我們?cè)贛ap中刪除這個(gè)創(chuàng)建的值,釋放被創(chuàng)建的值,保留put進(jìn)去的值。
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    /***************************
     * 不覆寫(xiě)create方法走不到下面 *
     ***************************/
    /*
     * 正常情況走不到這里
     * 走到這里的話 說(shuō)明 實(shí)現(xiàn)了自定義的 create(K key) 邏輯
     * 因?yàn)槟J(rèn)的 create(K key) 邏輯為null
     */
    synchronized (this) {
        // 記錄 create 的次數(shù)
        createCount++;
        // 將自定義create創(chuàng)建的值,放入LinkedHashMap中,如果key已經(jīng)存在,會(huì)返回 之前相同key 的值
        mapValue = map.put(key, createdValue);

        // 如果之前存在相同key的value,即有沖突。
        if (mapValue != null) {
            /*
             * 有沖突
             * 所以 撤銷(xiāo) 剛才的 操作
             * 將 之前相同key 的值 重新放回去
             */
            map.put(key, mapValue);
        } else {
            // 拿到鍵值對(duì),計(jì)算出在容量中的相對(duì)長(zhǎng)度,然后加上
            size += safeSizeOf(key, createdValue);
        }
    }

    // 如果上面 判斷出了 將要放入的值發(fā)生沖突
    if (mapValue != null) {
        /*
         * 剛才create的值被刪除了,原來(lái)的 之前相同key 的值被重新添加回去了
         * 告訴 自定義 的 entryRemoved 方法
         */
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 上面 進(jìn)行了 size += 操作 所以這里要重整長(zhǎng)度
        trimToSize(maxSize);
        return createdValue;
    }
}
  • 先嘗試從map緩存中獲取value,即mapVaule = map.get(key);如果mapVaule != null,說(shuō)明緩存中存在該對(duì)象,直接返回即可;
  • 如果mapVaule == null,說(shuō)明緩存中不存在該對(duì)象,大多數(shù)情況下會(huì)直接返回null;但是如果我們重寫(xiě)了create()方法,在緩存沒(méi)有該數(shù)據(jù)的時(shí)候自己去創(chuàng)建一個(gè),則會(huì)繼續(xù)往下走,中間可能會(huì)出現(xiàn)沖突,看注釋;
  • 注意:在我們通過(guò)LinkedHashMap進(jìn)行g(shù)et(key)或put(key,value)時(shí)都會(huì)對(duì)鏈表進(jìn)行調(diào)整,即將剛剛訪問(wèn)get或加入put的結(jié)點(diǎn)放入到鏈表尾部。

entryRemoved()

/**
* 1.當(dāng)被回收或者刪掉時(shí)調(diào)用。該方法當(dāng)value被回收釋放存儲(chǔ)空間時(shí)被remove調(diào)用
* 或者替換條目值時(shí)put調(diào)用,默認(rèn)實(shí)現(xiàn)什么都沒(méi)做。
* 2.該方法沒(méi)用同步調(diào)用,如果其他線程訪問(wèn)緩存時(shí),該方法也會(huì)執(zhí)行。
* 3.evicted=true:如果該條目被刪除空間 (表示 進(jìn)行了trimToSize or remove)  evicted=false:put沖突后 或 get里成功create后
* 導(dǎo)致
* 4.newValue!=null,那么則被put()或get()調(diào)用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
  • 可以發(fā)現(xiàn)entryRemoved方法是一個(gè)空方法,說(shuō)明這個(gè)也是讓開(kāi)發(fā)者自己根據(jù)需求去重寫(xiě)的。
  • entryRemoved()主要作用就是在結(jié)點(diǎn)數(shù)據(jù)value需要被刪除或回收的時(shí)候,給開(kāi)發(fā)者的回調(diào)。開(kāi)發(fā)者就可以在這個(gè)方法里面實(shí)現(xiàn)一些自己的邏輯:
    • 可以進(jìn)行資源的回收;
    • 可以實(shí)現(xiàn)二級(jí)內(nèi)存緩存,可以進(jìn)一步提高性能,思路如下:

二級(jí)緩存

  • 重寫(xiě)LruCache的entryRemoved()函數(shù),
  • 把刪除掉的item,再次存入另外一個(gè)LinkedHashMap<String, SoftWeakReference<Bitmap>>
  • 這個(gè)數(shù)據(jù)結(jié)構(gòu)當(dāng)做二級(jí)緩存,每次獲得圖片的時(shí)候,先判斷LruCache中是否緩存,沒(méi)有的話,再判斷這個(gè)二級(jí)緩存中是否有,如果都沒(méi)有再?gòu)膕dcard上獲取。sdcard上也沒(méi)有的話,就從網(wǎng)絡(luò)服務(wù)器上拉取。

entryRemoved()在LruCache中有四個(gè)地方進(jìn)行了調(diào)用:put()、get()、trimToSize()、remove()中進(jìn)行了調(diào)用。


2. DiskLruCache

Android DiskLruCache 源碼解析 硬盤(pán)緩存的絕佳方案


原文地址

http://blog.csdn.net/shakespeare001/article/details/51695358

https://www.tuicool.com/articles/JB7RNj

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

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

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