Universal-Image-Loader(2)

10.MemoryCache

MemoryCache是實現內存緩存的類,不管是內存緩存還是磁盤緩存,對于ImageLoader來說都是核心功能,因為關系著圖片的加載速度,因此要深入了解UIL中緩存的工作原理。

回憶一下,之前學過的ImageLoader的緩存實現,在之前的實現當中,利用的是LruCache來實現的,而LruCache又是通過accessOrder為true的LinkedHashMap來實現LRU算法的。

在UIL中,不光光有基于LRU算法的LRUMemoryCache,還有FIFOLimitedMemoryCache、LargestLimitedMemoryCache、LimitedAgeMemoryCache、LRULimitedMemoryCache、UsingFreqLimitedMemoryCache和WeakMemoryCache基于各種緩存規則的MemoryCache,它們實現的核心就是Map,一般LRU算法的都是基于accessOrder為true的LinkedHashMap,其他用的是HashMap,其中WeakMemoryCache的值用的是WeakReference。它們的保證同步方法是通過Collections.synchronizedList(Map...)獲取到同步隊列。UIL中默認配置的是LruMemoryCache.

MemoryCache還有兩個基礎抽象實現類BaseMemoryCache和LimitedMemoryCache,而LimitedMemoryCache又是繼承于BaseMemoryCache,所有類型的MemoryCache類都是實現MemoryCache接口或者繼承于LimitedMemoryCache和BaseMrmoryCache這三者的,大致可以分為是沒有緩存大小限制和有緩存大小限制的,兩者之間的區別就是,在添加新數據時如果緩存的大小超過大小限制閾值時是否刪除Map中的數據;而如何刪除數據的規則又將有緩存大小限制的MemoryCache分為幾個類。下面是所有類型MemoryCache的分類表格。

MemoryCache子類 實現接口 Or 父類 有無大小限制 刪除規則
LruMemoryCache MemoryCache LRU最近最少使用
LimitedAgeMemoryCache MemoryCache 存在超過限制時間的
FuzzyKeyMemoryCache MemoryCache put的時候有等價key的
LRULimitedMemoryCache LimitedMemoryCache LRU最近最少使用
FIFOLimitedMemoryCache LlimitedMemoryCache FIFO先入先出
LargestLimitedMemoryCache LimitedMemoryCache Largest最大的
UsingFreqLimitedMemoryCache LimitedMemoryCache 使用次數最少的
WeakMemoryCache BaseMemoryCache

下面是MemoryCache繼承結構圖,可以幫助我們理解整個MemoryCache的框架

MemoryCache繼承結構圖.PNG

下面就來詳細介紹幾個常用的MemoryCache以及它們的工作流程。

LruMemoryCache

首先是所有類型MemoryCache的接口MemoryCache.java。
它的作用主要是向外提供接口,外界主要通過該接口添加、獲取數據,不關心內部的具體實現。
接口很簡單,就幾個基本的增刪查方法。

public interface MemoryCache {

    boolean put(String key, Bitmap value);

    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}

然后介紹主要實現MemoryCache接口的無限制的MemoryCache類。

首先是UIL默認使用的LruMemoryCache。
可以看出來,實現的原理跟LruCache是十分相似的。都是利用了accessOrder為true的LinkedHashMap來實現LRU算法,在超過容量之后將刪除Map中最近最少使用的數據。其他的操作大部分都是通過LinkedHashMap的同名操作實現的。

這里提前分析一下,既然都有容量限制,都是LRU算法,那么LruMemoryCache和LRULimitedMemoryCache有什么區別?
答:原理上是一樣的,只不過刪除的順序不一樣:LruMemoryCache在每次的put之后才調用了trimToSize()保證數據不超過限制大小;LRULimitedMemoryCache是確保數據不超過限制大小之后才添加進LinkedHashMap當中。
后面再詳細分析LRULimitedMemoryCache的具體實現原理,來看看實現和LruMemoryCache是有什么區別。

public class LruMemoryCache implements MemoryCache {

    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 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 Bitmap for 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 key if it exists. */
    @Override
    public final Bitmap 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);
            }
            return 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 Bitmap in bytes.
     * 
     * 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);
    }
}

LRULimitedMemoryCache

直接實現MemoryCache接口的只需要了解LruMemoryCache就足夠了,本節主要介紹繼承與LimitedMemoryCache的LRULimitedMemoryCache和FIFOLimitedMemoryCache。

這里LRULimitedMemoryCache的繼承結構有3層(除去MemoryCache接口),每層結構都有自己的作用,因此沒有清楚了解繼承結構的話會導致思維混亂,無法理解LRULimitedMemoryCache的工作原理,所以先來看一下繼承結構。

LimitedMemoryCache繼承結構圖.PNG

從圖中可以看出,從BaseMemoryCache到LRULimitedMemoryCache一共有3層,所以問題就出來了,為什么要這么多層,像LruMemoryCache那樣直接實現MemoryCache不可以嗎?
當然可以,但是這樣寫就不符合類的單一職責要求,而且LRULimitedMemoryCache和FIFOLimitedMemoryCache這兩個類只是單單刪除規則不一樣,如果LRULimitedMemoryCache直接實現MemoryCache的話,那么FIFOLimitedMemoryCache也要實現該類,而且會有大部分的代碼和LRU是相同的,因此將共同的部分抽象出來,使得每個類的職責單一,降低耦合。

在這3層繼承結構中,每一層的工作是:
BaseMemoryCache:有一個Map的內部類,該Map的作用是提供最底層的緩存功能,最終存儲數據和獲取數據實際都是BaseMemoryCache實現的,注意該Map的value并不是一個Bitmap類,而是一個Bitmap的Reference類,通常會傳入Bitmap的WeakReference,這樣的效果是Map中的value隨時都能夠GC回收;
LimitedMemoryCache:該類的作用是保證緩存大小不超過閾值。類內部有一個List容器類用于強引用存儲Bitmap,通過該List類來保證正確提供容量限制的功能,即首先在put方法中判斷當前存儲的數據是否超過閾值,如果是則調用抽象方法removeNext()按照一定規則刪除,再利用List來刪除的Bitmap,如果List刪除成功則說明緩存過該Bitmap,然后再改變緩存大小;
LRULimitedMemoryCache和FIFOLimitedMemoryCache等其他類的LimitedMemoryCache:用于提供刪除規則即實現LimitedMemoryCache的removeNext(),內部有用于實現各個規則的數據結構,比如LRU利用accessOrder為true的LinkedHashMap,FIFO利用的是一個LinkedList。

下面分別介紹這三層結構。
1.BaseMemoryCache
首先先介紹BaseMemoryCache,因為LimitedMemoryCache繼承于它,下面是它的實現源碼。

關注的重點有:

  • BaseMemoryCache最主要的就是靠Map成員Map<String, Reference<Bitmap>> softMap實現的,如數據的緩存和獲取就是通過該softMap的put和get實現的
  • Map中的value是Bitmap的Reference對象而不是Bitmap,而該Reference通常被傳入WeakReference,因此造成的結果是,softMap中的value會隨時被GC回收
  • put方法中利用抽象方法createReference()來創建Bitmap的引用對象
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();
    }

    @Override
    public Collection<String> keys() {
        synchronized (softMap) {
            return new HashSet<String>(softMap.keySet());
        }
    }

    @Override
    public void clear() {
        softMap.clear();
    }

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

2.LimitedMemoryCache

之前已經說過了LimitedMemoryCache的作用就是實現防止緩存超過閾值的大小,所以關注該類的關注點在如何實現限制緩存大小。

由源碼可以看出:

  • 允許最大緩存為16MB
  • 內部有一個List用于以強引用的方式存儲Bitmap
  • 沒有get方法,即get方法是直接調用的是BaseMemoryCache的get方法
  • 最主要的是put方法,這是實現限制緩存的關鍵,在put方法中,判斷添加后的緩存大小是否超過閾值,如果超過則調用抽象方法removeNext()獲取緩存中應該被刪除的數據,比如LRU的removeNext()返回的是最近最少被使用的數據,FIFO的返回的是最早插入的數據。最后還要調用super.put(key, value)來讓BaseMemoryCache中的softMap存儲數據,因為前面說過數據的存儲和獲取是由BaseMemoryCache提供的
  • 內部類List的作用是確保緩存大小的正確變化。因為要確保removeNext()刪除的數據是之前緩存的數據,如果List刪除removeNext()返回的數據成功了證明緩存被刪除了,此時緩存大小才會變化,才能使緩存反應真實的變化
  • 在put方法中緩存數據超過閾值時,removeNext()會刪除子類比如LRU、FIFO中的緩存數據,List也會刪除數據,但注意BaseMemoryCache中的softMap并不會刪除數據,不必擔心softMap中的緩存量過大,因為WeakReference的對象會隨時被GC回收
public abstract class LimitedMemoryCache extends BaseMemoryCache {

    private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16;
    private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;

    private final int sizeLimit;

    private final AtomicInteger cacheSize;

    /**
     * Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed
     * limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any
     * time)
     */
    private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());

    /** @param sizeLimit Maximum size for cache (in bytes) */
    public LimitedMemoryCache(int sizeLimit) {
        this.sizeLimit = sizeLimit;
        cacheSize = new AtomicInteger();
        if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
            L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB);
        }
    }

    /**
     * 實現緩存大小限制的關鍵
     */
    @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) {
            // 在添加數據后大于限制大小時則刪除removeNext()返回的Bitmap
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // 最后一定要將數據存到sofeMap中
        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);
    }

    @Override
    public void clear() {
        hardCache.clear();
        cacheSize.set(0);
        super.clear();
    }

    protected int getSizeLimit() {
        return sizeLimit;
    }

    protected abstract int getSize(Bitmap value);

    protected abstract Bitmap removeNext();
}

3.LRULimitedMemoryCache

由上面可知,數據的存取獲取以及緩存大小的限制在前面兩層結構已經實現了,此時LRULimitedMemoryCache以及FIFOLimitedMemoryCache等類的工作就簡單很多了,只需要制定相應的刪除規則removeNext()就行了,而刪除指定的數據使得類本身也需要用一個數據結構存儲數據,畢竟你沒有數據怎么確定要刪除的數據是哪個啊是吧,下面是源碼。

關注的重點:

  • 用一個accessOrder為true的LinkedHashMap作為存儲數據的數據結構
  • put方法是先調用super.put(key, value)再儲存數據到本身。這很好理解嘛,前面說過的,數據的存儲和獲取是通過BaseMemoryCache實現的,因此只有高層成功存儲了數據自身才存儲數據
  • get方法與put方法相反,先是調用本身的get然后再返回父類的get,這里返回的是父類的get為什么還要多此一舉調用本身的get干什么?這里調用get主要是為了觸發accessOrder為true的LinkedHashMap的LRU算法,即把常用的數據移到隊列的尾部,然后隊頭剩下的就是不常用的
  • removeNext()直接刪除了LinkedHashMap隊列頭部的數據,這些數據是最近最少使用的數據,跟accessOrder為true的LinkedHashMap特性有關
  • 抽象方法getSize()用于計算傳入的Bitmap的大小,這跟緩存的存儲和刪除改變的緩存大小密切相關,該方法由子類實現
public class LRULimitedMemoryCache extends LimitedMemoryCache {

    private static final int INITIAL_CAPACITY = 10;
    private static final float LOAD_FACTOR = 1.1f;

    /** Cache providing Least-Recently-Used logic */
    private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));

    /** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
    public LRULimitedMemoryCache(int maxSize) {
        super(maxSize);
    }

    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            lruCache.put(key, value);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap get(String key) {
        lruCache.get(key); // call "get" for LRU logic
        return super.get(key);
    }

    @Override
    public Bitmap remove(String key) {
        lruCache.remove(key);
        return super.remove(key);
    }

    @Override
    public void clear() {
        lruCache.clear();
        super.clear();
    }

    @Override
    protected int getSize(Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

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

4.FIFOLimitedMemoryCache

看完上面之后,FIFOLimiedMemoryCache更容易理解了,只是removeNext()返回的數據跟LRU不一樣而已,具體看下面的源碼

  • 內部使用的是LinkedList作為存儲數據的數據結構
  • put,get等方法除了調用super的put和get方法,本身直接調用LinkedList的put,get方法用于添加和獲取數據
  • removeNext()直接就是刪除隊列的頭部數據,也就是FIFO原則
public class FIFOLimitedMemoryCache extends LimitedMemoryCache {

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());

    public FIFOLimitedMemoryCache(int sizeLimit) {
        super(sizeLimit);
    }

    @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
    public void clear() {
        queue.clear();
        super.clear();
    }

    @Override
    protected int getSize(Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

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

內存緩存小結

緩存是ImageLoader中的一個核心功能,因此需要深刻的理解,重新看一下MemoryCache的框架圖,此時便會有了更深的體會。

MemoryCache繼承機構圖.PNG
  • MemoryCache:整個緩存內存框架的最底層的接口,它是外界主要使用的接口
  • LruMemoryCache:直接實現了MemoryCache接口,里面用了accessOrder為true的LinkedHashMap進行數據的存取的容器,在每次put方法時會檢查緩存大小是否超出閾值,是的話則根據LRU算法刪除數據
  • BaseMemoryCache:實現了MemoryCache接口,內部使用了HashMap<String, Reference<Bitmap>>作為數據存儲讀取的容器。需要注意的點是,傳入Map的Reference一般都是WeakReference,即該Bitmap可能隨時被GC回收
  • LimitedMemoryCache:繼承了BaseMemoryCache,該類主要是記錄已經緩存的大小cacheSize,還利用List緩存添加過的Bitmap對象,實現了限制緩存大小的功能,即在put方法中發現添加的數據總數超過閾值16MB時,會調用抽象方法removeNext()取出要刪除的對象,然后利用內部類List檢測該對象是否被緩存過,是則刪除List中的數據并改變cacheSize的大小
  • LRULimitedMemoryCache:繼承了LimitedMemoryCache,有前面可知,數據的存取以及容量的限制父類已經實現,該類只是為了提供在緩存超過緩存大小時應該刪除哪個數據的規則即removeNext()方法。內部使用了accessOrder為true的LinkedHashMap作為數據的存儲(注意這些存儲的數據并不是拿來供外界使用的,而是為了確定下一個被刪除的數據),然后利用LinkedHashMap的LRU特性在removeNext中返回最近最少使用的數據
  • FIFOLimitedMemoryCache:跟LRU一樣,只是內部用的是LinkedList實現數據的存儲,當然這些數據不是供外界使用的,然后再removeNext中返回LinkedList隊頭的數據,也就是最早插入的數據,這就是FIFO算法
  • 剩下的MemoryCache跟FIFO,LRU一樣,只是在removeNext中根據不同的規則提供了不一樣的被刪除的數據

11.DiskCache

ImageLoader中另一個緩存是磁盤緩存,首先還是先回憶一下之前學過的ImageLoader的DiskLruCache的工作原理是什么。

DiskLruCache工作原理:內部使用了accessOrder為true的LinkedHashMap作為數據的索引,因為DiskLruCache是以文件的形式存儲數據的,因此LinkedHaspMap里面并不持有Bitmap對象,實際上持有的是一個Entry內部類對象,該對象指向的是一個緩存文件,DiskLruCache對緩存數據的添加和獲取其實就是對該緩存文件的寫入和讀取。DiskLruCache對緩存文件的寫入和讀取分別是通過內部類對象Editor和Snotshot的OutputStream和InputStream來實現數據的寫入和讀取。

接下來我們從源碼中了解DiskCache整個的框架,跟MemoryCache一樣,先從框架圖入手掌握整個繼承結構。
注意圖中還有一個DiskLruCache不屬于繼承結構,其實DiskLruCache就是之前學過的ImageLoader里面的DiskLruCache,但是UIL中也添加了該類,原因是LruDiskCache在內部使用了DiskLruCache,簡單來說就是在DiskLruCache外封裝了一層。在ImageLoaderConfiguration中默認使用的LruDiskCache

DiskCache繼承結構圖.PNG

LruDiskCache

LruDiskCache是直接實現DiskCache的類,首先來看一下DiskCache接口的方法。
可以看到需要實現的方法并不多,主要關注get和save方法。get方法返回的是文件類,即緩存文件,要清楚地意識到凡是磁盤緩存都是用文件來緩存數據的;save方法用于將數據存儲進與imageUri相對應的文件,其中參數有一個InputStream,該InputStream是圖片的輸入流,通過該輸入流將數據寫入文件當中。

public interface DiskCache {
    /**
     * Returns root directory of disk cache
     */
    File getDirectory();

    /**
     * Returns file of cached image
     */
    File get(String imageUri);

    /**
     * Saves image stream in disk cache.
     * Incoming image stream shouldn't be closed in this method.
     *
     */
    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

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

    boolean remove(String imageUri);

    /** Closes disk cache, releases resources. */
    void close();

    void clear();
}


下面介紹LruDiskCache,相對于DiskLruCache簡單很多,原因是內部使用DiskLruCache類作為成員,文件數據的寫入和讀取其實都是通過DiskLruCache實現的。下面是使用LruDiskCache需要關注的地方。

  • 初始化:直接通過構造方法便可創建,無需DiskLruCache那樣需要通過open,因為在構造方法內部調用了open方法
  • get:因為使用DiskLruCache讀取數據是先獲取Snapshot然后,通過Snapshot的InputStream讀取文件數據的,在LruDiskCache中封裝了整個過程,直接在save方法內部獲取Snapshot并返回文件
  • save:同get方法一樣,LurDiskCache封裝了通過DiskLruCache的Editor的OutputStream寫入數據的過程,直接在參數里面傳入文件的輸入流便可實現寫入數據的功能,并且在commit方法當中可以確保緩存大小不超過閾值
  • remove:通過DiskLruCache的remove即可

總的來說,其實LruDiskCache就是對DiskLruCache做了一層封裝,實際的數據的操作方法還是通過DiskLruCache來實現的。

public class LruDiskCache implements DiskCache {
    /** {@value */
    public static final int DEFAULT_BUFFER_SIZE = 32 * 1024; // 32 Kb
    /** {@value */
    public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;
    /** {@value */
    public static final int DEFAULT_COMPRESS_QUALITY = 100;

    private static final String ERROR_ARG_NULL = " argument must be not null";
    private static final String ERROR_ARG_NEGATIVE = " argument must be positive number";

    protected DiskLruCache cache;
    private File reserveCacheDir;

    //用于文件的命名,有Hash和MD5兩種文件名
    protected final FileNameGenerator fileNameGenerator;

    protected int bufferSize = DEFAULT_BUFFER_SIZE;

    protected Bitmap.CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
    protected int compressQuality = DEFAULT_COMPRESS_QUALITY;

    public LruDiskCache(File cacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize) throws IOException {
        this(cacheDir, null, fileNameGenerator, cacheMaxSize, 0);
    }

    /**
     * @param cacheDir          Directory for file caching
     * @param reserveCacheDir   null-ok; Reserve directory for file caching. It's used when the primary directory isn't available.
     * @param fileNameGenerator {@linkplain com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator
     *                          Name generator} for cached files. Generated names must match the regex
     *                          <strong>[a-z0-9_-]{1,64}</strong>
     * @param cacheMaxSize      Max cache size in bytes. <b>0</b> means cache size is unlimited.
     * @param cacheMaxFileCount Max file count in cache. <b>0</b> means file count is unlimited.
     * @throws IOException if cache can't be initialized (e.g. "No space left on device")
     */
    public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
            int cacheMaxFileCount) throws IOException {
        if (cacheDir == null) {
            throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);
        }
        if (cacheMaxSize < 0) {
            throw new IllegalArgumentException("cacheMaxSize" + ERROR_ARG_NEGATIVE);
        }
        if (cacheMaxFileCount < 0) {
            throw new IllegalArgumentException("cacheMaxFileCount" + ERROR_ARG_NEGATIVE);
        }
        if (fileNameGenerator == null) {
            throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL);
        }

        if (cacheMaxSize == 0) {
            cacheMaxSize = Long.MAX_VALUE;
        }
        if (cacheMaxFileCount == 0) {
            cacheMaxFileCount = Integer.MAX_VALUE;
        }

        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
        initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
    }

    private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
            throws IOException {
        try {
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        } catch (IOException e) {
            L.e(e);
            if (reserveCacheDir != null) {
                initCache(reserveCacheDir, null, cacheMaxSize, cacheMaxFileCount);
            }
            if (cache == null) {
                throw e; //new RuntimeException("Can't initialize disk cache", e);
            }
        }
    }

    @Override
    public File getDirectory() {
        return cache.getDirectory();
    }

    @Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } catch (IOException e) {
            L.e(e);
            return null;
        } finally {
            if (snapshot != null) {
                snapshot.close();
            }
        }
    }

    @Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean copied = false;
        try {
            copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
        } finally {
            IoUtils.closeSilently(os);
            if (copied) {
                editor.commit();
            } else {
                editor.abort();
            }
        }
        return copied;
    }

    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
        }
        if (savedSuccessfully) {
            editor.commit();
        } else {
            editor.abort();
        }
        return savedSuccessfully;
    }

    @Override
    public boolean remove(String imageUri) {
        try {
            return cache.remove(getKey(imageUri));
        } catch (IOException e) {
            L.e(e);
            return false;
        }
    }

    @Override
    public void close() {
        try {
            cache.close();
        } catch (IOException e) {
            L.e(e);
        }
        cache = null;
    }

    @Override
    public void clear() {
        try {
            cache.delete();
        } catch (IOException e) {
            L.e(e);
        }
        try {
            initCache(cache.getDirectory(), reserveCacheDir, cache.getMaxSize(), cache.getMaxFileCount());
        } catch (IOException e) {
            L.e(e);
        }
    }

    private String getKey(String imageUri) {
        return fileNameGenerator.generate(imageUri);
    }

    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    public void setCompressFormat(Bitmap.CompressFormat compressFormat) {
        this.compressFormat = compressFormat;
    }

    public void setCompressQuality(int compressQuality) {
        this.compressQuality = compressQuality;
    }
}

BaseDiskCache

UIL中還有另外兩個具體磁盤緩存類LimitedAgeDiskCache和UnlimitedDiskCache,它們的不同點只是一個還刪除緩存文件的過時文件,一個不限制緩存大小。這里只介紹他們的共同父類:BaseDiskCache。

在BaseDiskCache中并沒有使用特殊的數據結構來存儲數據,直接就是通過對文件類的操作來達成使用文件緩存的目的。注意該類并沒有限制緩存大小,下面就簡單看一下BaseDiskCache的save和get方法。

public abstract class BaseDiskCache implements DiskCache {

    ...

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

    @Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        boolean loaded = false;
        try {
            OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
            try {
                loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
            } finally {
                IoUtils.closeSilently(os);
            }
        } finally {
            if (loaded && !tmpFile.renameTo(imageFile)) {
                loaded = false;
            }
            if (!loaded) {
                tmpFile.delete();
            }
        }
        return loaded;
    }


}

磁盤緩存小結

相對于內存緩存,UIL中的磁盤緩存的框架簡單的多,大致分為兩個磁盤緩存策略,一個是使用DiskLruCache作為底層實現的LruDiskCache,另一個是直接對文件類File進行操作。

  • LruDiskCache:底層采用DiskLruCache實現,只不過是封裝了DiskLruCache較為復雜的數據操作方法
  • BaseDiskCache:不采取任何數據,直接在實現方法里面使用方法類實現

12.ImageLoader

之前的章節主要介紹了ImageLoader在加載圖片時主要用到的一些類,比如配置類ImageLoaderConfiguration、線程池管理者以及主要任務執行者ImageLoaderEngine、加載和顯示圖片任務LoadAndDisplayImageDisk、圖片加載顯示配置類DisplayImageOptions、圖片下載器ImageDownloader、圖片解碼器ImageDocoder、緩存內存類MemoryCache和DiskCache等等。接下來分析ImageLoader的使用和具體工作流程,由于主要需要的類的分析過了,因此分析起來簡單了許多。

ImageLoader使用

ImageLoader的很簡單,主要分為3步

  1. 配置加載圖片的參數類ImageLoaderConfiguration并創建Imageloader對象
  2. 配置顯示圖片用的參數類DisplayImageOptions
  3. 使用displayImage()顯示圖片

第一步,配置并創建ImageLoader對象
后面注釋的是Configuration的可選項和一些默認項,可以看出一些加載圖片的必要默認項已經可以讓ImageLoader正常工作了,比如taskExecutor、diskCache、memoryCache、downloader、decoder和defaultDisplayImageOptions等等。

//Set the ImageLoaderConfigutation
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(getApplicationContext())
        .threadPriority(Thread.NORM_PRIORITY - 2)
        .denyCacheImageMultipleSizesInMemory()
        .diskCacheFileNameGenerator(new Md5FileNameGenerator())
        .diskCacheSize(50 * 1024 * 1024)
        .tasksProcessingOrder(QueueProcessingType.LIFO)
        .writeDebugLogs()
        .build();

//Initial ImageLoader with ImageLoaderConfiguration
ImageLoader.getInstance().init(configuration);

/*
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
    .memoryCacheExtraOptions(480, 800) // default = device screen dimensions
    .diskCacheExtraOptions(480, 800, null)
    .taskExecutor(...)// default
    .taskExecutorForCachedImages(...)
    .threadPoolSize(3) // default
    .threadPriority(Thread.NORM_PRIORITY - 2) // default
    .tasksProcessingOrder(QueueProcessingType.FIFO) // default
    .denyCacheImageMultipleSizesInMemory()
    .memoryCache(new LruMemoryCache(2 * 1024 * 1024))
    .memoryCacheSize(2 * 1024 * 1024)
    .memoryCacheSizePercentage(13) // default
    .diskCache(new UnlimitedDiskCache(cacheDir)) // default
    .diskCacheSize(50 * 1024 * 1024)
    .diskCacheFileCount(100)
    .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default
    .imageDownloader(new BaseImageDownloader(context)) // default
    .imageDecoder(new BaseImageDecoder()) // default
    .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
    .writeDebugLogs()
    .build();
/**/


第二步,配置顯示圖片用的參數類DislayImageOptions
從類名就能判斷出該類的作用是讓ImageLoader按需求顯示圖片,跟Configuration一樣,Options也有很多可選的配置參數,并且一些顯示圖片的必要參數已經被初始化了,比如displayer用于控件顯示圖片和handler傳回主線程操作。
但值得注意的是,在Options中,cacheInMemory和cacheOnDisk默認是false,因此很有必要在程序中手動將它們將設置成true,如下面代碼所示。

DisplayImageOptions options = new DisplayImageOptions.Builder()
        .cacheInMemory(true)    //緩存的這兩步很有必要
        .cacheOnDisk(true)
        .considerExifParams(true)
        .displayer(new CircleBitmapDisplayer(Color.WHITE, 5))
        .build();
/*
DisplayImageOptions options = new DisplayImageOptions.Builder()
    .showImageOnLoading(R.drawable.ic_stub) // resource or drawable
    .showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
    .showImageOnFail(R.drawable.ic_error) // resource or drawable
    .resetViewBeforeLoading(false)  // default
    .delayBeforeLoading(1000)
    .cacheInMemory(false) // default
    .cacheOnDisk(false) // default
    .preProcessor(...)
    .postProcessor(...)
    .extraForDownloader(...)
    .considerExifParams(false) // default
    .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
    .bitmapConfig(Bitmap.Config.ARGB_8888) // default
    .decodingOptions(...)
    .displayer(new SimpleBitmapDisplayer()) // default
    .handler(new Handler()) // default
    .build();      
/**/    

第三步,使用displayImage()顯示圖片
調用displayImage()之前要獲取到ImageLoader的實例,因為ImageLoader采用單例模式,因此ImageLoader的實例是通過IamgeLoader的靜態方法getInstance()獲取的。
displayImage()有很多種重載方法,這里只展示一個,后面的注釋是所有displayImage()的版本。
由displayImage的參數中可以看出最主要的兩個參數就是imageUri和imageView,也就是要顯示的圖片的uri地址和顯示圖片的控件,這里也體現出了ImageLoader的最本質的工作,那就是將圖片從uri中加載到控件中。

注:不像前面Configuration和Optins配置一次就夠了,displayImage()方法在每次加載顯示圖片時都應該調用一次,因此通常該方法使用在ListView的Adapter的getView當中,因為getView中可以獲取到當前要顯示圖片的控件,并且列表滑動就會觸發getView方法,因此只需要在getView中將對應的ImageView傳送給displayImage就可以了


ImageLoader.getInstance().displayImage(imageUri, imageView, options, imageLoadingListener);

/*
//所有displayImage的方法,前面一部分是針對ImageAware的,后面一部分是針對ImageView的,也就是我們在開發中所使用到的,其實實現是利用了前面的方法

displayImage(String uri, ImageAware imageAware)
displayImage(String uri, ImageAware imageAware, ImageLoadingListener listener)
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options)
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageLoadingListener listener) 
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

displayImage(String uri, ImageView imageView)
displayImage(String uri, ImageView imageView, ImageSize targetImageSize)
displayImage(String uri, ImageView imageView, DisplayImageOptions options)
displayImage(String uri, ImageView imageView, ImageLoadingListener listener)
displayImage(String uri, ImageView imageView, DisplayImageOptions options,
        ImageLoadingListener listener)
displayImage(String uri, ImageView imageView, DisplayImageOptions options,
        ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
/**/

到此ImageLoader的使用方法就結束了,是不是很簡單,只需要3步,甚至如果使用的都是默認的配置,那只需要初始化ImageLoader并調用displayImage就可以了。下面具體分析ImageLoader的工作流程。

ImageLoader工作流程

從上面可以看出外界使用ImageLoader只需要調用displayImage()就可以實現圖片的加載和顯示,所以研究ImageLoader的工作流程其實就是分析ImageLoader的displayImage方法。由于displayImage()中調用的方法會貫穿整個UIL包,加上前面仔細分析了主要類的工作原理,因此下面只需分析主要的部分,便于理解整個UIL的工作流程。

public class ImageLoader
{
    ...

    //這些是工作時所需要的類成員
    private ImageLoaderConfiguration configuration;
    private ImageLoaderEngine engine;

    private ImageLoadingListener defaultListener = new SimpleImageLoadingListener();

    private volatile static ImageLoader instance;

    /**
     *  uri:圖片的uri
     *  imageAware:顯示圖片的控件
     *  options:顯示圖片的參數配置
     *  targetSize:要顯示的圖片的大小
     *  listener:用于加載圖片中的回調
     *  progressListener:也是用于圖片加載時的回調
     */
    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
            {
                ...
                //將uri轉換成key
                String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
                //從內存中獲取相應key的圖片
                Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
                //如果不為空說明從內存中獲取到了圖片
                if (bmp != null && !bmp.isRecycled()) {
                    //判斷是否需要加工處理,如果是則封裝圖片的信息和需求成Info類,然后通過一個任務類ProcessAndDisplayImageTask將圖片按照需求加載到控件中
                    if (options.shouldPostProcess()) {
                        ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                                options, listener, progressListener, engine.getLockForUri(uri));
                        ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                                defineHandler(options));

                        //如果此時運行在子線程中就直接運行,否則使用線程池執行
                        if (options.isSyncLoading()) {
                            displayTask.run();
                        } else {
                            engine.submit(displayTask);
                        }
                    } 
                    //如果不需要經過處理,則直接通過displayer的display將圖片顯示在控件中
                    else {
                        options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                        listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
                    }
                } 
                //為空說明需要內存緩存中不存在該圖片,需要從磁盤和網絡中加載該圖片
                else {
                    //設置加載前的默認圖片
                    if (options.shouldShowImageOnLoading()) {
                        imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
                    } else if (options.isResetViewBeforeLoading()) {
                        imageAware.setImageDrawable(null);
                    }

                    //封裝圖片加載信息類,然后通過LoadAndDisplayImageTask執行加載顯示圖片任務
                    ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                            options, listener, progressListener, engine.getLockForUri(uri));
                    LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                            defineHandler(options));
                    if (options.isSyncLoading()) {
                        displayTask.run();
                    } else {
                        engine.submit(displayTask);
                    }
                }

            }

    ...
}

//同步加載圖片并顯示的任務
final class LoadAndDisplayImageTask implements Runnable, IoUtils.CopyListener
{
    ...

    @Override
    public void run()
    {
        ...
        //從內存中提取圖片
        bmp = configuration.memoryCache.get(memoryCacheKey);
        if (bmp == null || bmp.isRecycled()) {
            //如果內存中為null則從磁盤網絡中獲取圖片
            bmp = tryLoadBitmap();
            ...
            if (bmp != null && options.isCacheInMemory()) {
                //將圖片存進內存緩存中
                configuration.memoryCache.put(memoryCacheKey, bmp);
            }
            ...
        }
        ...
        //前面加載完圖片接著通過執行DisplayBitmapTask顯示圖片到控件中
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);

    }

    //同步從磁盤、網絡中加載圖片
    private Bitmap tryLoadBitmap() throws TaskCancelledException
    {
        Bitmap bitmap = null;
        ...
        //從磁盤緩存中獲取文件并解碼成圖片
        File imageFile = configuration.diskCache.get(uri);
        bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) 
        {
            //如果磁盤中獲取不到圖片則通過tryCacheImageOnDisk()從網絡中獲取并存至磁盤中
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) 
            {
                ...
                //再次從磁盤中獲取文件
                imageFile = configuration.diskCache.get(uri);
                if (imageFile != null) {
                    imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                }
            }   
            //再次將文件解碼成圖片
            bitmap = decodeImage(imageUriForDecoding);
        }
        ...
        return bitmap;
    }

    //從網絡中加載圖片(通過ImageDownloader),并按照需求壓縮(通過ImageDecoder),最后放入磁盤緩存中
    private boolean tryCacheImageOnDisk() throws TaskCancelledException 
    {
        boolean loaded;

        loaded = downloadImage();
        ...
        resizeAndSaveImage(width, height);
        ...

        return loaded;
    }

}

//異步顯示圖片的任務,里面主要是調用了圖片在加載過程中的各種回調,最后通過displayer.display將圖片顯示到控件中,注意該任務要運行在主線程當中
final class DisplayBitmapTask implements Runnable {

    @Override
    public void run() {
        if (imageAware.isCollected()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
            L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);

            //最主要的是這步,顯示圖片
            displayer.display(bitmap, imageAware, loadedFrom);
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

}

上面就是整個UIL的工作流程,大致上可以分為兩步,同步加載圖片和異步顯示圖片,主要關注點在同步加載圖片這個過程。

  • 同步加載圖片:內存緩存中獲取->磁盤緩存中獲取->網絡中獲取->存進磁盤緩存->磁盤緩存中獲取->存進內存緩存->返回圖片
  • 異步顯示圖片:利用Handler將任務執行在主線程當中,通過displayer將圖片顯示在控件當中

以下是加載顯示圖片的流程,幫助理解和記憶。

UIL_Load&Display Task Flow.PNG

總結UIL的關注點

UIL主要關注以下幾點:圖片從網絡加載的加載所使用的方式,內存緩存的方式,磁盤緩存的方式以及整個UIL的工作流程。

網絡加載:采用HttpURLConnection進行網絡連接來獲取數據
內存緩存:UIL中內存緩存有好幾種用于內存緩存的類,其中默認使用的是LruMemoryCache,它的實現原理跟Android自帶的LruCache差不多,都是利用accessOrder為true的LinkedHashMap實現的,其他的還有LRULimitedMemoryCache,FIFOLimitedMemoryCache等等,它們的公共特點就是緩存大小有限制,不同點是在緩存超過限制的時候刪除的規則不一樣
磁盤緩存:UIL中的磁盤緩存比內存緩存簡單了好多,主要分為兩種實現,一種是UIL中默認使用的LruDiskCache,它的底層實現是直接使用的DiskLruCache,只不過是做了一層封裝;另一種實現是直接對文件類File進行操作,其中有兩個具體實現,一個是有緩存大小限制的,另一個是沒有限制的
UIL工作流程:就是內存緩存->磁盤緩存->網絡3個流程,詳細在前一節有介紹

優缺點
優點:比較老的框架,穩定,加載速度適中
缺點:不支持GIF圖片加載,緩存機制沒有和http的緩存很好的結合,完全是自己的一套緩存機制

0.ImageLoader中使用的設計模式

單例模式

public static ImageLoader getInstance() {
    if (instance == null) {
        synchronized (ImageLoader.class) {
            if (instance == null) {
                instance = new ImageLoader();
            }
        }
    }
    return instance;
}

建造者模式

在ImageLoaderConfiguration和DisplayImageOptions都是使用了建造者模式,原因是它們有很多可選的屬性可以被設置,使用建造者模式可以使得代碼更清晰和健壯。

學到的知識

看源碼的方式:先了解大致的流程,或者自己猜想工作流程,然后帶著大局觀去學習源碼,這樣有利于保持思路的清晰,而不至于在函數各種跳轉的過程中迷失了,導致興趣全無

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容