Universal-Image-Loader解析(二)內部緩存原理

Universal-Image-Loader解析系列

Universal-Image-Loader解析(一)基本介紹與使用
Universal-Image-Loader解析(二)內部緩存原理
Universal-Image-Loader解析(三)源代碼解析

前篇文章則跟大家介紹了UIL的一些常用用法。

對于我們所知道的緩存,常用的是內存緩存MemoryCache和硬盤緩存DiscCache。一個讀取快容量小,一個讀取慢容量大。

對于各自使用哪種緩存,則可以在前面配置ImageLoaderConfiguration進行緩存設置,當然也可以自己自定義適合的緩存。

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

對于Universal-Image-Loader來說它的緩存結構也是分為內存緩存MemoryCache和硬盤緩存DiskCache

一.MemoryCache內存緩存

首先先看個結構圖,理解UIL里面內存緩存的結構

MemoryCache

由于空間有限就沒畫成標準的UML類圖形式。
對于基類MemoryCache它則是一個接口,里面定義了put,get圖片的方法

public interface MemoryCache {
    ...
    boolean put(String key, Bitmap value);
    
    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}

都是大家比較所熟悉的方法,而對于其他的類

我們一個個看

LruMemoryCache

這個類就是這個開源框架默認的內存緩存類,緩存的是bitmap的強引用。直接實現了MemoryCache方法

public class LruMemoryCache implements MemoryCache {

    private final LinkedHashMap<String, Bitmap> map;
    //最大容量
    private final int maxSize;
    /** 目前緩存的容量大小 */
    private int size;
    public LruMemoryCache(int maxSize) {
        ...
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }
    @Override
    public final Bitmap get(String key) {
        ...
        synchronized (this) {
            return map.get(key);
        }
    }
    @Override
    public final boolean put(String key, Bitmap value) {
        ...
        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

    /**
     * Lru算法,當容量超過最大緩存容量,則移除最久的條目
     */
    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);
            }
        }
    }

    @Override
    public final Bitmap remove(String key) {
        ...
        synchronized (this) {
            Bitmap previous = map.remove(key);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
            return previous;
        }
    }
    ...
    //返回圖片的字節大小
    private int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }
    ...
}

LruMemoryCache的源碼也比較簡單,內部有個成員變量LinkedHashMap<String, Bitmap> map這里直接進行保存的話則是強引用的形式。
主要看get,put方法。
對于get方法來說,比較簡單,直接根據指定的key返回對應的圖片。
而對于put方法來說,則需要考慮容量的問題。

@Override
    public final boolean put(String key, Bitmap value) {
        ...
        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

put方法首先調用了sizeof方法,該方法則是返回指定Bitmap的字節大小,之后size +=,總緩存量增加,之后調用trimToSize該方法則是進行緩存容量判斷的。

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);
            }
        }
    }

如果加入后的size 緩存容量 <= maxSize 最大緩存容量,則直接break,不用進行判定處理。
如果大于的話,則直接移除最久未使用的。

大家肯定有疑問,它到底怎么判斷最久未使用的?沒看到相關代碼呀?

相信知道LinkedHashMap的話可能就知道。
LinkedHashMap自身已經實現了順序存儲,默認情況下是按照元素的添加順序存儲,也可以啟用按照訪問順序存儲,即最近讀取的數據放在最前面,最早讀取的數據放在最后面,然后它還有一個判斷是否刪除最老數據的方法,默認是返回false,即不刪除數據。大家常見也就是按順序存儲,很少忘了它還可以根據最近未使用的方法。

//LinkedHashMap的一個構造函數,當參數accessOrder為true時,即會按照訪問順序排序,最近訪問的放在最前,最早訪問的放在后面
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}

//LinkedHashMap自帶的判斷是否刪除最老的元素方法,默認返回false,即不刪除老數據
//我們要做的就是重寫這個方法,當滿足一定條件時刪除老數據
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
}

回看我們前面LinkedHashMap的創建

  this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);

再舉個使用例子

例子

就比較明了了。

BaseMemoryCache

BaseMemoryCache同樣也是實現了MemoryCache方法,不過它還是一個抽象類。
它是一個內存緩存的基類,實現了內存緩存中常用的方法,只不過它里面提供了一個非強引用的Reference作為擴展,方便GC的回收,避免OOM.

public abstract class BaseMemoryCache implements MemoryCache {

    /** Stores not strong references to objects */
    private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());

    @Override
    public Bitmap get(String key) {
        Bitmap result = null;
        Reference<Bitmap> reference = softMap.get(key);
        if (reference != null) {
            result = reference.get();
        }
        return result;
    }

    @Override
    public boolean put(String key, Bitmap value) {
        softMap.put(key, createReference(value));
        return true;
    }

    @Override
    public Bitmap remove(String key) {
        Reference<Bitmap> bmpRef = softMap.remove(key);
        return bmpRef == null ? null : bmpRef.get();
    }

    /** Creates {@linkplain Reference not strong} reference of value */
    protected abstract Reference<Bitmap> createReference(Bitmap value);
}

代碼也比較簡單,內存持有一個Map<String, Reference<Bitmap>> softMap來保存非強引用對象,具體的引用類型則看它實現的抽象方法createReference。

WeakMemoryCache

我們看它的一個子類WeakMemoryCache則是繼承與BaseMemory,實現createReference

public class WeakMemoryCache extends BaseMemoryCache {
    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

很明顯是來保存弱引用對象的。

LimitedMemoryCache

我們看它的另外一個子類LimitedMemoryCache,但它并沒有實現BaseMemoryCache里的createReference方法,它也是一個抽象類,在BaseMemoryCache基礎上封裝了個抽象方法
protected abstract Bitmap removeNext();用來處理當緩存容量不足時的情況。

public abstract class LimitedMemoryCache extends BaseMemoryCache {
    ...
    //當前保存的Bitmap,用來統計緩存數
    private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
    ...
    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccessfully = false;
        // Try to add value to hard cache
        int valueSize = getSize(value);
        int sizeLimit = getSizeLimit();
        int curCacheSize = cacheSize.get();
        if (valueSize < sizeLimit) {
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // Add value to soft cache
        super.put(key, value);
        return putSuccessfully;
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            if (hardCache.remove(value)) {
                cacheSize.addAndGet(-getSize(value));
            }
        }
        return super.remove(key);
    }
   ...
    protected abstract int getSize(Bitmap value);

    protected abstract Bitmap removeNext();
}

可以看到在 LimitedMemoryCache里面又有一個List<Bitmap>保存的是強引用,而在BaseMemoryCache里面也有個Map<String, Reference<Bitmap>> softMap來保存Bitmap,為什么要這樣。

這主要是因為在BaseMemoryCache里面并沒有做緩存限制處理,它只是封裝實現了基本的Bitmap的put,get。而當面對緩存容量有限的情況下,則需要交給子類去處理。

我們看下這里的put方法,關鍵在

while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }

當超過容量時,調用抽象方法removeNext由子類自行實現,之后hardCache移除,但此時并沒有調用softMap的移除。

也就是對于List<Bitmap>來說,當它的緩存容量超過的時候,它會移除第一個對象來緩解容量,但是保存在Map<String, Reference<Bitmap>> softMap里面的Bitmap并沒有被移除。
如果這樣下去softMap豈不是會無限大?

這是因為在Map<String, Reference<Bitmap>> softMap里面保存的Bitmap是弱引用的存在,而在List<Bitmap>里面保存的是強引用,當內存不足的時候,GC則會先清除softMap里面的對象。

FIFOLimitedMemoryCache

我們看下LimitedMemoryCache的一個子類FIFOLimitedMemoryCache,看到FIFO也就是先進先出了。

public class FIFOLimitedMemoryCache extends LimitedMemoryCache {

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
    ...
    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            queue.add(value);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            queue.remove(value);
        }
        return super.remove(key);
    }
    ...
    @Override
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

可以看到同樣的這里也有個List<Bitmap> queue來保存記錄,而在removeNext那里,返回的正是隊列的第一個元素,符合FIFO。

LRULimitedMemoryCache

再來看一個另外一個子類LRULimitedMemoryCache也就是最近未使用刪除。

public class LRULimitedMemoryCache extends LimitedMemoryCache {

    /** Cache providing Least-Recently-Used logic */
    private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
   ...
    @Override
    protected Bitmap removeNext() {
        Bitmap mostLongUsedValue = null;
        synchronized (lruCache) {
            Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();
            if (it.hasNext()) {
                Entry<String, Bitmap> entry = it.next();
                mostLongUsedValue = entry.getValue();
                it.remove();
            }
        }
        return mostLongUsedValue;
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

可以看到,這里的LRU處理則是使用LinkedHashMap,在它的構造方法中第三個參數為true表示使用LRU,之后再removeNext返回那個Bitmap。

同理其他子類也如下,就不一一列舉。

MemoryCache小結

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

  • LruMemoryCache(這個類就是這個開源框架默認的內存緩存類,緩存的是bitmap的強引用)

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

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

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

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

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

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

3.只使用弱引用緩存

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

二.DiskCache硬盤緩存

同樣先來看個結構

DiskCache

DiskCache的設計其實和MemoryCache一樣,對于基類DiskCache,它同樣是一個接口

public interface DiskCache {
    //返回硬盤緩存的根目錄
    File getDirectory();

    File get(String imageUri);

    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

    boolean save(String imageUri, Bitmap bitmap) throws IOException;

    boolean remove(String imageUri);

    void close();

    void clear();
}

同樣一個個看

LruDiskCache

LruDiskCache則是直接實現了DiskCache接口,采用LRU算法來進行緩存處理。
再理解LruDiskCache前,先理解另一個類DiskLruCache

final class DiskLruCache implements Closeable {
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TEMP = "journal.tmp";
    static final String JOURNAL_FILE_BACKUP = "journal.bkp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    ...
    private final LinkedHashMap<String, Entry> lruEntries =
            new LinkedHashMap<String, Entry>(0, 0.75f, true);
    ...
    final ThreadPoolExecutor executorService =
            new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    ...
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
            throws IOException {
        ...
    }
    ...
    public synchronized Snapshot get(String key) throws IOException {
        ...
    }
    ...
    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        ...
    }
    /** A snapshot of the values for an entry. */
    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private File[] files;
        private final InputStream[] ins;
        private final long[] lengths;
        ...
    }
    ...
    public final class Editor {
        private final Entry entry;
        private final boolean[] written;
        private boolean hasErrors;
        private boolean committed;
        ...
    }
    ...
    private final class Entry {
        private final String key;

        private final long[] lengths;

        private boolean readable;

        private Editor currentEditor;

        private long sequenceNumber;
        ...
    }

這個DiskLruCache比較長也比較復雜,它是LruDiskCache的一個文件工具類。這里的緩存數據存儲在文件系統上的一個目錄。
同時也注意到這里的一個成員變量
private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
可以知道這是用來處理LRU的。

同時這里的value則是Entry,Entry則是封裝了當前文件的編輯情況Ediotr以及key。
而這里Editor封裝了文件的寫入情況OutputStreamSnapshot封裝了文件的讀取情況InputStream。

回頭看回LruDiskCache

public class LruDiskCache implements DiskCache {
    protected DiskLruCache cache;
    private File reserveCacheDir;

    protected final FileNameGenerator fileNameGenerator;
    ...
    public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
            int cacheMaxFileCount) throws IOException {
        ...
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
        initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
    }

    private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
        ...
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        ...
    }
    @Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } 
        ...
    }
    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        ...
        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        }
        ...
        return savedSuccessfully;
    }

首先LruDiskCache內部成員變量帶有DiskLruCache還有文件的保存目錄等,在它的構造方法中調用DiskLruCache.open方法創建了DiskLruCache對象,而在它的open方法里,則根據文件的目錄情況創建了對應的文件系統。

再看它的save方法,先調用getKey方法將uri轉換為對應的key,而在cache,edit中

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        ...
        Entry entry = lruEntries.get(key);
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
                || entry.sequenceNumber != expectedSequenceNumber)) {
            return null; // Snapshot is stale.
        }
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {
            return null; // Another edit is in progress.
        }

        Editor editor = new Editor(entry);
        entry.currentEditor = editor;
        ...
        return editor;
    }

則是根據指定的key先判斷緩存文件中有沒有相應的key,如果沒有則創建一個Entry對象持有它,之后保存在lruEntries之后,創建一個當前Entry的編輯對象Editor,以便之后寫入到文件中。

s之后調用了

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);

editor.newOutputStream則是根據當前目錄和key創建出一個文件,之后打開這個文件的一個輸出流情況,獲取到之后就進行Bitmap的寫入。

同理,看下LruDiskCache的get方法

@Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } 
        ...
    }

調用了cache,get

public synchronized Snapshot get(String key) throws IOException {
        。。。
        Entry entry = lruEntries.get(key);
        ...
        File[] files = new File[valueCount];
        InputStream[] ins = new InputStream[valueCount];
        try {
            File file;
            for (int i = 0; i < valueCount; i++) {
                file = entry.getCleanFile(i);
                files[i] = file;
                ins[i] = new FileInputStream(file);
            }
        } 
        ...
        return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
    }

在get方法中,先根據key拿到對應的Entry,再拿到對應的文件打開輸入流,之后傳入到Snapshot,
而在snapshot.getFile

/** Returns file with the value for {@code index}. */
        public File getFile(int index) {
            return files[index];
        }

返回的則是對應的文件。

BaseDiskCache

BaseDiskCache同樣也是直接實現了DiskCache方法,實現的方法也比較簡單

public abstract class BaseDiskCache implements DiskCache {
    ...
    protected final File cacheDir;
    protected final File reserveCacheDir;

    protected final FileNameGenerator fileNameGenerator;
    
    public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
        ...
        this.cacheDir = cacheDir;
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
    }
    
    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
            if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
                savedSuccessfully = false;
            }
            if (!savedSuccessfully) {
                tmpFile.delete();
            }
        }
        bitmap.recycle();
        return savedSuccessfully;
    }
    
    @Override
    public File get(String imageUri) {
        return getFile(imageUri);
    }
    
    protected File getFile(String imageUri) {
        String fileName = fileNameGenerator.generate(imageUri);
        File dir = cacheDir;
        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
                dir = reserveCacheDir;
            }
        }
        return new File(dir, fileName);
    }

比較簡單,根據對應的文件去打開獲取。它的兩個子類LimitedAgeDiskCacheUnlimitedDiskCache也都不一一擴展開了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容