Android 開源框架Universal-Image-Loader完全解析(二)--- 圖片緩存策略詳解

轉載請註明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/26810303),請尊重他人的辛勤勞動成果,謝謝!

本篇文章繼續爲大家介紹Universal-Image-Loader這個開源的圖片加載框架,介紹的是圖片緩存策略方面的,如果大家對這個開源框架的使用還不瞭解,大家可以看看我之前寫的一篇文章Android 開源框架Universal-Image-Loader完全解析(一)--- 基本介紹及使用,我們一般去加載大量的圖片的時候,都會做緩存策略,緩存又分爲內存緩存和硬盤緩存,我之前也寫了幾篇異步加載大量圖片的文章,使用的內存緩存是LruCache這個類,LRU是Least Recently Used 近期最少使用算法,我們可以給LruCache設定一個緩存圖片的最大值,它會自動幫我們管理好緩存的圖片總大小是否超過我們設定的值, 超過就刪除近期最少使用的圖片,而作爲一個強大的圖片加載框架,Universal-Image-Loader自然也提供了多種圖片的緩存策略,下面就來詳細的介紹下


內存緩存


首先我們來了解下什麼是強引用和什麼是弱引用?

強引用是指創建一個對象並把這個對象賦給一個引用變量, 強引用有引用變量指向時永遠不會被垃圾回收。即使內存不足的時候寧願報OOM也不被垃圾回收器回收,我們new的對象都是強引用

弱引用通過weakReference類來實現,它具有很強的不確定性,如果垃圾回收器掃描到有着WeakReference的對象,就會將其回收釋放內存


現在我們來看Universal-Image-Loader有哪些內存緩存策略

1. 只使用的是強引用緩存 


  • LruMemoryCache(這個類就是這個開源框架默認的內存緩存類,緩存的是bitmap的強引用,下面我會從源碼上面分析這個類)


2.使用強引用和弱引用相結合的緩存有


  • UsingFreqLimitedMemoryCache(如果緩存的圖片總量超過限定值,先刪除使用頻率最小的bitmap)

  • LRULimitedMemoryCache(這個也是使用的lru算法,和LruMemoryCache不同的是,他緩存的是bitmap的弱引用)

  • FIFOLimitedMemoryCache(先進先出的緩存策略,當超過設定值,先刪除最先加入緩存的bitmap)

  • LargestLimitedMemoryCache(當超過緩存限定值,先刪除最大的bitmap對象)

  • LimitedAgeMemoryCache(當 bitmap加入緩存中的時間超過我們設定的值,將其刪除)


3.只使用弱引用緩存


  • WeakMemoryCache(這個類緩存bitmap的總大小沒有限制,唯一不足的地方就是不穩定,緩存的圖片容易被回收掉)


上面介紹了Universal-Image-Loader所提供的所有的內存緩存的類,當然我們也可以使用我們自己寫的內存緩存類,我們還要看看要怎麼將這些內存緩存加入到我們的項目中,我們只需要配置ImageLoaderConfiguration.memoryCache(...),如下

ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)  
        .memoryCache(new WeakMemoryCache())  
        .build();



下面我們來分析LruMemoryCache這個類的源代碼

package com.nostra13.universalp_w_picpathloader.cache.memory.impl;  
  
import android.graphics.Bitmap;  
import com.nostra13.universalp_w_picpathloader.cache.memory.MemoryCacheAware;  
  
import java.util.Collection;  
import java.util.HashSet;  
import java.util.LinkedHashMap;  
import java.util.Map;  
  
/** 
 * A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to 
 * the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may 
 * become eligible for garbage collection.<br /> 
 * <br /> 
 * <b>NOTE:</b> This cache uses only strong references for stored Bitmaps. 
 * 
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 
 * @since 1.8.1 
 */  
public class LruMemoryCache implements MemoryCacheAware<String, Bitmap> {  
  
    private final LinkedHashMap<String, Bitmap> map;  
  
    private final int maxSize;  
    /** Size of this cache in bytes */  
    private int size;  
  
    /** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */  
    public LruMemoryCache(int maxSize) {  
        if (maxSize <= 0) {  
            throw new IllegalArgumentException("maxSize <= 0");  
        }  
        this.maxSize = maxSize;  
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);  
    }  
  
    /** 
     * Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head 
     * of the queue. This returns null if a Bitmap is not cached. 
     */  
    @Override  
    public final Bitmap get(String key) {  
        if (key == null) {  
            throw new NullPointerException("key == null");  
        }  
  
        synchronized (this) {  
            return map.get(key);  
        }  
    }  
  
    /** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */  
    @Override  
    public final boolean put(String key, Bitmap value) {  
        if (key == null || value == null) {  
            throw new NullPointerException("key == null || value == null");  
        }  
  
        synchronized (this) {  
            size += sizeOf(key, value);  
            Bitmap previous = map.put(key, value);  
            if (previous != null) {  
                size -= sizeOf(key, previous);  
            }  
        }  
  
        trimToSize(maxSize);  
        return true;  
    }  
  
    /** 
     * Remove the eldest entries until the total of remaining entries is at or below the requested size. 
     * 
     * @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements. 
     */  
    private void trimToSize(int maxSize) {  
        while (true) {  
            String key;  
            Bitmap 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<String, Bitmap> toEvict = map.entrySet().iterator().next();  
                if (toEvict == null) {  
                    break;  
                }  
                key = toEvict.getKey();  
                value = toEvict.getValue();  
                map.remove(key);  
                size -= sizeOf(key, value);  
            }  
        }  
    }  
  
    /** Removes the entry for {@code key} if it exists. */  
    @Override  
    public final void remove(String key) {  
        if (key == null) {  
            throw new NullPointerException("key == null");  
        }  
  
        synchronized (this) {  
            Bitmap previous = map.remove(key);  
            if (previous != null) {  
                size -= sizeOf(key, previous);  
            }  
        }  
    }  
  
    @Override  
    public Collection<String> keys() {  
        synchronized (this) {  
            return new HashSet<String>(map.keySet());  
        }  
    }  
  
    @Override  
    public void clear() {  
        trimToSize(-1); // -1 will evict 0-sized elements  
    }  
  
    /** 
     * Returns the size {@code Bitmap} in bytes. 
     * <p/> 
     * An entry's size must not change while it is in the cache. 
     */  
    private int sizeOf(String key, Bitmap value) {  
        return value.getRowBytes() * value.getHeight();  
    }  
  
    @Override  
    public synchronized final String toString() {  
        return String.format("LruCache[maxSize=%d]", maxSize);  
    }  
}


我們可以看到這個類中維護的是一個LinkedHashMap,在LruMemoryCache構造函數中我們可以看到,我們爲其設置了一個緩存圖片的最大值maxSize,並實例化LinkedHashMap, 而從LinkedHashMap構造函數的第三個參數爲ture,表示它是按照訪問順序進行排序的,
我們來看將bitmap加入到LruMemoryCache的方法put(String key, Bitmap value),  第61行,sizeOf()是計算每張圖片所佔的byte數,size是記錄當前緩存bitmap的總大小,如果該key之前就緩存了bitmap,我們需要將之前的bitmap減掉去,接下來看trimToSize()方法,我們直接看86行,如果當前緩存的bitmap總數小於設定值maxSize,不做任何處理,如果當前緩存的bitmap總數大於maxSize,刪除LinkedHashMap中的第一個元素,size中減去該bitmap對應的byte數


我們可以看到該緩存類比較簡單,邏輯也比較清晰,如果大家想知道其他內存緩存的邏輯,可以去分析分析其源碼,在這裏我簡單說下FIFOLimitedMemoryCache的實現邏輯,該類使用的HashMap來緩存bitmap的弱引用,然後使用LinkedList來保存成功加入到FIFOLimitedMemoryCache的bitmap的強引用,如果加入的FIFOLimitedMemoryCache的bitmap總數超過限定值,直接刪除LinkedList的第一個元素,所以就實現了先進先出的緩存策略,其他的緩存都類似,有興趣的可以去看看。


硬盤緩存


接下來就給大家分析分析硬盤緩存的策略,這個框架也提供了幾種常見的緩存策略,當然如果你覺得都不符合你的要求,你也可以自己去擴展


  • FileCountLimitedDiscCache(可以設定緩存圖片的個數,當超過設定值,刪除掉最先加入到硬盤的文件)

  • LimitedAgeDiscCache(設定文件存活的最長時間,當超過這個值,就刪除該文件)

  • TotalSizeLimitedDiscCache(設定緩存bitmap的最大值,當超過這個值,刪除最先加入到硬盤的文件)

  • UnlimitedDiscCache(這個緩存類沒有任何的限制)


下面我們就來分析分析TotalSizeLimitedDiscCache的源碼實現

/******************************************************************************* 
 * Copyright 2011-2013 Sergey Tarasevich 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 *******************************************************************************/  
package com.nostra13.universalp_w_picpathloader.cache.disc.impl;  
  
import com.nostra13.universalp_w_picpathloader.cache.disc.LimitedDiscCache;  
import com.nostra13.universalp_w_picpathloader.cache.disc.naming.FileNameGenerator;  
import com.nostra13.universalp_w_picpathloader.core.DefaultConfigurationFactory;  
import com.nostra13.universalp_w_picpathloader.utils.L;  
  
import java.io.File;  
  
/** 
 * Disc cache limited by total cache size. If cache size exceeds specified limit then file with the most oldest last 
 * usage date will be deleted. 
 * 
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 
 * @see LimitedDiscCache 
 * @since 1.0.0 
 */  
public class TotalSizeLimitedDiscCache extends LimitedDiscCache {  
  
    private static final int MIN_NORMAL_CACHE_SIZE_IN_MB = 2;  
    private static final int MIN_NORMAL_CACHE_SIZE = MIN_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;  
  
    /** 
     * @param cacheDir     Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
     *                     needed for right cache limit work. 
     * @param maxCacheSize Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the 
     *                     most oldest last usage date will be deleted. 
     */  
    public TotalSizeLimitedDiscCache(File cacheDir, int maxCacheSize) {  
        this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), maxCacheSize);  
    }  
  
    /** 
     * @param cacheDir          Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
     *                          needed for right cache limit work. 
     * @param fileNameGenerator Name generator for cached files 
     * @param maxCacheSize      Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the 
     *                          most oldest last usage date will be deleted. 
     */  
    public TotalSizeLimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int maxCacheSize) {  
        super(cacheDir, fileNameGenerator, maxCacheSize);  
        if (maxCacheSize < MIN_NORMAL_CACHE_SIZE) {  
            L.w("You set too small disc cache size (less than %1$d Mb)", MIN_NORMAL_CACHE_SIZE_IN_MB);  
        }  
    }  
  
    @Override  
    protected int getSize(File file) {  
        return (int) file.length();  
    }  
}

 

這個類是繼承LimitedDiscCache,除了兩個構造函數之外,還重寫了getSize()方法,返回文件的大小,接下來我們就來看看LimitedDiscCache

/******************************************************************************* 
 * Copyright 2011-2013 Sergey Tarasevich 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 *******************************************************************************/  
package com.nostra13.universalp_w_picpathloader.cache.disc;  
  
import com.nostra13.universalp_w_picpathloader.cache.disc.naming.FileNameGenerator;  
import com.nostra13.universalp_w_picpathloader.core.DefaultConfigurationFactory;  
  
import java.io.File;  
import java.util.Collections;  
import java.util.HashMap;  
import java.util.Map;  
import java.util.Map.Entry;  
import java.util.Set;  
import java.util.concurrent.atomic.AtomicInteger;  
  
/** 
 * Abstract disc cache limited by some parameter. If cache exceeds specified limit then file with the most oldest last 
 * usage date will be deleted. 
 * 
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com) 
 * @see BaseDiscCache 
 * @see FileNameGenerator 
 * @since 1.0.0 
 */  
public abstract class LimitedDiscCache extends BaseDiscCache {  
  
    private static final int INVALID_SIZE = -1;  
  
    //記錄緩存文件的大小  
    private final AtomicInteger cacheSize;  
    //緩存文件的最大值  
    private final int sizeLimit;  
    private final Map<File, Long> lastUsageDates = Collections.synchronizedMap(new HashMap<File, Long>());  
  
    /** 
     * @param cacheDir  Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
     *                  needed for right cache limit work. 
     * @param sizeLimit Cache limit value. If cache exceeds this limit then file with the most oldest last usage date 
     *                  will be deleted. 
     */  
    public LimitedDiscCache(File cacheDir, int sizeLimit) {  
        this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), sizeLimit);  
    }  
  
    /** 
     * @param cacheDir          Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's 
     *                          needed for right cache limit work. 
     * @param fileNameGenerator Name generator for cached files 
     * @param sizeLimit         Cache limit value. If cache exceeds this limit then file with the most oldest last usage date 
     *                          will be deleted. 
     */  
    public LimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int sizeLimit) {  
        super(cacheDir, fileNameGenerator);  
        this.sizeLimit = sizeLimit;  
        cacheSize = new AtomicInteger();  
        calculateCacheSizeAndFillUsageMap();  
    }  
  
    /** 
     * 另開線程計算cacheDir裏面文件的大小,並將文件和最後修改的毫秒數加入到Map中 
     */  
    private void calculateCacheSizeAndFillUsageMap() {  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                int size = 0;  
                File[] cachedFiles = cacheDir.listFiles();  
                if (cachedFiles != null) { // rarely but it can happen, don't know why  
                    for (File cachedFile : cachedFiles) {  
                        //getSize()是一個抽象方法,子類自行實現getSize()的邏輯  
                        size += getSize(cachedFile);  
                        //將文件的最後修改時間加入到map中  
                        lastUsageDates.put(cachedFile, cachedFile.lastModified());  
                    }  
                    cacheSize.set(size);  
                }  
            }  
        }).start();  
    }  
  
    /** 
     * 將文件添加到Map中,並計算緩存文件的大小是否超過了我們設置的最大緩存數 
     * 超過了就刪除最先加入的那個文件 
     */  
    @Override  
    public void put(String key, File file) {  
        //要加入文件的大小  
        int valueSize = getSize(file);  
          
        //獲取當前緩存文件大小總數  
        int curCacheSize = cacheSize.get();  
        //判斷是否超過設定的最大緩存值  
        while (curCacheSize + valueSize > sizeLimit) {  
            int freedSize = removeNext();  
            if (freedSize == INVALID_SIZE) break; // cache is empty (have nothing to delete)  
            curCacheSize = cacheSize.addAndGet(-freedSize);  
        }  
        cacheSize.addAndGet(valueSize);  
  
        Long currentTime = System.currentTimeMillis();  
        file.setLastModified(currentTime);  
        lastUsageDates.put(file, currentTime);  
    }  
  
    /** 
     * 根據key生成文件 
     */  
    @Override  
    public File get(String key) {  
        File file = super.get(key);  
  
        Long currentTime = System.currentTimeMillis();  
        file.setLastModified(currentTime);  
        lastUsageDates.put(file, currentTime);  
  
        return file;  
    }  
  
    /** 
     * 硬盤緩存的清理 
     */  
    @Override  
    public void clear() {  
        lastUsageDates.clear();  
        cacheSize.set(0);  
        super.clear();  
    }  
  
      
    /** 
     * 獲取最早加入的緩存文件,並將其刪除 
     */  
    private int removeNext() {  
        if (lastUsageDates.isEmpty()) {  
            return INVALID_SIZE;  
        }  
        Long oldestUsage = null;  
        File mostLongUsedFile = null;  
          
        Set<Entry<File, Long>> entries = lastUsageDates.entrySet();  
        synchronized (lastUsageDates) {  
            for (Entry<File, Long> entry : entries) {  
                if (mostLongUsedFile == null) {  
                    mostLongUsedFile = entry.getKey();  
                    oldestUsage = entry.getValue();  
                } else {  
                    Long lastValueUsage = entry.getValue();  
                    if (lastValueUsage < oldestUsage) {  
                        oldestUsage = lastValueUsage;  
                        mostLongUsedFile = entry.getKey();  
                    }  
                }  
            }  
        }  
  
        int fileSize = 0;  
        if (mostLongUsedFile != null) {  
            if (mostLongUsedFile.exists()) {  
                fileSize = getSize(mostLongUsedFile);  
                if (mostLongUsedFile.delete()) {  
                    lastUsageDates.remove(mostLongUsedFile);  
                }  
            } else {  
                lastUsageDates.remove(mostLongUsedFile);  
            }  
        }  
        return fileSize;  
    }  
  
    /** 
     * 抽象方法,獲取文件大小 
     * @param file 
     * @return 
     */  
    protected abstract int getSize(File file);  
}


在構造方法中,第69行有一個方法calculateCacheSizeAndFillUsageMap(),該方法是計算cacheDir的文件大小,並將文件和文件的最後修改時間加入到Map中


然後是將文件加入硬盤緩存的方法put(),在106行判斷當前文件的緩存總數加上即將要加入緩存的文件大小是否超過緩存設定值,如果超過了執行removeNext()方法,接下來就來看看這個方法的具體實現,150-167中找出最先加入硬盤的文件,169-180中將其從文件硬盤中刪除,並返回該文件的大小,刪除成功之後成員變量cacheSize需要減掉改文件大小。

FileCountLimitedDiscCache這個類實現邏輯跟TotalSizeLimitedDiscCache是一樣的,區別在於getSize()方法,前者返回1,表示爲文件數是1,後者返回文件的大小。

等我寫完了這篇文章,我才發現FileCountLimitedDiscCache和TotalSizeLimitedDiscCache在最新的源碼中已經刪除了,加入了LruDiscCache,由於我的是之前的源碼,所以我也不改了,大家如果想要了解LruDiscCache可以去看最新的源碼,我這裏就不介紹了,還好內存緩存的沒變化,下面分析的是最新的源碼中的部分,我們在使用中可以不自行配置硬盤緩存策略,直接用DefaultConfigurationFactory中的就行了

我們看DefaultConfigurationFactory這個類的createDiskCache()方法


/** 
 * Creates default implementation of {@link DiskCache} depends on incoming parameters 
 */  
public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,  
        long diskCacheSize, int diskCacheFileCount) {  
    File reserveCacheDir = createReserveDiskCacheDir(context);  
    if (diskCacheSize > 0 || diskCacheFileCount > 0) {  
        File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);  
        LruDiscCache diskCache = new LruDiscCache(individualCacheDir, diskCacheFileNameGenerator, diskCacheSize,  
                diskCacheFileCount);  
        diskCache.setReserveCacheDir(reserveCacheDir);  
        return diskCache;  
    } else {  
        File cacheDir = StorageUtils.getCacheDirectory(context);  
        return new UnlimitedDiscCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);  
    }  
}

  

如果我們在ImageLoaderConfiguration中配置了diskCacheSize和diskCacheFileCount,他就使用的是LruDiscCache,否則使用的是UnlimitedDiscCache,在最新的源碼中還有一個硬盤緩存類可以配置,那就是LimitedAgeDiscCache,可以在ImageLoaderConfiguration.diskCache(...)配置


今天就給大家分享到這裏,有不明白的地方在下面留言,我會盡量爲大家解答的,下一篇文章我將繼續更深入的分析這個框架,希望大家繼續關注!

轉載:

http://blog.csdn.net/xiaanming/article/details/26810303


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章