Universal-Image-Loader完全解析(下)

Mz_Chris發表於2015-06-30

Universal-Image-Loader完全解析(下)

在這篇文章中,我會繼續跟大家分享有關於Universal-Image-Loader框架的相關知識,這次主要分享的是框架中圖片快取方面的知識。當然,如果對這個框架還完全不瞭解的話,可以先看看之前寫的有關於這個框架的入門篇Universal-Image-Loader完全解析(上)。好了,言歸正傳,一般來說,我們去載入比較多的圖片的時候,大多數情況下都會採用圖片的快取策略,而進行圖片的快取,我們又可以分為記憶體快取與檔案快取(硬碟快取),針對於記憶體快取,有過專案經驗的人都知道,大部分情況下是採用LruCache這個類,我們可以為這個LruCache設定規定的一個快取圖片的最大值,之後它就會自動去檢測圖片快取總數是否達到我們設定的條件的最大值,當超過這個最大值後,LruCache類就會管理刪除近期最少使用的圖片。而作為一個以圖片載入管理為主要功能的框架,它也自然提供了多種圖片的快取策略,接下來,我們就來談談框架中的這些快取策略。

記憶體快取策略

首先,關於記憶體快取,一定會涉及到的就是弱引用和強引用,現在我們來了解這兩個概念:

  • 弱引用:是通過weakReference類所實現的,它具有很強的不穩定性,一旦有垃圾回收器掃描到有著WeakReference的物件,就不管此物件如何,都會將其進行回收,以便來釋放記憶體,它一般用來防止記憶體洩漏,保證記憶體被VM回收
  • 強引用:是指建立一個新的物件時,我們將其賦值為一個引用變數,這時這個物件就是具有強引用性,強引用的的引用的變數指向時不會自動地被垃圾回收器回收。即使當記憶體不足的時候,手機寧願發生OOM現象也不會被回收掉。一般來說,我們平時建立建立物件採用new的方式都是強引用

當然,還有軟引用和虛引用的概念,既然談到了,就順便一起來了解了解吧

  • 軟引用:主要是通過SoftReference類所實現的,它具有較強的引用功能,只有當記憶體不足的時候,垃圾回收器會回收掉它所指向的物件,而記憶體足夠時,通常是不會被回收的,一般多用來作實現Cache機制
  • 虛引用:通過PhantomReference類所實現,它主要是用來跟蹤物件被垃圾回收器回收的活動。一個物件持有虛引用,就相當於沒有任何引用一樣。而虛引用與軟引用和弱引用的一個重要區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。而程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動

通過上面的講解,相信各位朋友已經瞭解到了"四大引用"的基本概念。接下來,讓我們來進入正題,來看看Universal-Image-Loader框架中存在著哪些記憶體快取策略

1. 僅使用強引用進行快取

  • LruMemoryCache,這個類也是這個開源框架預設的記憶體快取類,採用的是Lru演算法進行快取,快取中使用的是bitmap的強引用

2. 僅使用弱引用進行快取

  • WeakMemoryCache,這個類快取的bitmap的總大小是沒有進行限制的,但不足的是不穩定,很容易被系統回收,當垃圾回收器一旦掃描到這個類快取的物件bitmap,則會被回收掉

3. 同時使用強引用和弱引用相結合

  • UsingFreqLimitedMemoryCache,這個類傳入圖片的總量最大值,當快取的圖片總量超過這個最大值,則會先刪除使用頻率最小的那個bitmap
  • FIFOLimitedMemoryCache,這個類是採用先進先出的快取策略,設定圖片大小限定值後,當超過這個值時,會先刪除最先入快取的那個bitmap
  • LRULimitedMemoryCache,這個類採用的也是Lru演算法進行快取,但與LruMemeoryCache不同的是,這個快取使用的是bitmap的弱引用
  • LargestLimitedMemoryCache,這個類傳入圖片總大小作為最大值,當超過快取所設定的最大值時,會先刪除最大的那個bitmap物件
  • LimitedAgeMemoryCache,這個類傳入的兩個引數,第一個引數是MemoryCache類,第二個是以long型別的時間值,當我們加入快取的bitmap時間超過我們所設定的值,就會將其進行刪除

上面主要介紹的是Universal-Image-Loader框架中主要所提供的所有有關於圖片記憶體快取的相關類,當我們上面的類不滿足專案需求的時候,這時我們也可以自定義使用自己寫的記憶體快取類,當寫完自己的記憶體快取類後,加入到我們的專案中,具體是需要配置ImageLoaderConfiguration.memoryCache(...),比如我們自定義了一個快取類,名為myWeakMemoryCache,則採取如下配置:

ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Build(this)
    .memoryCache(new MyWeakMemoryCache())
    .build();
    

接下來,讓我們來學習一下Universal-Image-Loader預設的框架快取類LruMemoryCache的相關原始碼

package com.nostra13.universalimageloader.cache.memory.impl;

import android.graphics.Bitmap;
import com.nostra13.universalimageloader.cache.memory.MemoryCache;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map.Entry;

public class LruMemoryCache implements MemoryCache {
    //以LinkedHashMap容器來儲存bitmap引用
    private final LinkedHashMap<String, Bitmap> map;
    
    //定義快取的限制值
    private final int maxSize;
    
    //計算快取的大小
    private int size;

    public LruMemoryCache(int maxSize) {
        if(maxSize <= 0) {
           throw new IllegalArgumentException("maxSize <= 0");
        } else {
         this.maxSize = maxSize;
             this.map = new LinkedHashMap(0, 0.75F, true);
        }
    }
    
    //若快取中存在此bitmap,則返回,若沒快取,則將此bitmap插入到快取中並返回null值
    public final Bitmap get(String key) {
        if(key == null) {
            throw new NullPointerException("key == null");
        } else {
            synchronized(this) {
                return (Bitmap)this.map.get(key);
            }
        }
    }

    //將bitmap放入到LinkedHashMap物件容器內
    public final boolean put(String key, Bitmap value) {
        if(key != null && value != null) {
            synchronized(this) {
                this.size += this.sizeOf(key, value);
                Bitmap previous = (Bitmap)this.map.put(key, value);
                if(previous != null) {
                    this.size -= this.sizeOf(key, previous);
                }
            }

            this.trimToSize(this.maxSize);
            return true;
        } else {
            throw new NullPointerException("key == null || value == null");
        }
    }

    private void trimToSize(int maxSize) {
        while(true) {
            synchronized(this) {
                if(this.size < 0 || this.map.isEmpty() && this.size != 0) {
                    throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
             }

                if(this.size > maxSize && !this.map.isEmpty()) {
                    //移除容器中最先訪問的bitmap
                    Entry toEvict = (Entry)this.map.entrySet().iterator().next();
                    if(toEvict != null) {
                        String key = (String)toEvict.getKey();
                        Bitmap value = (Bitmap)toEvict.getValue();
                        this.map.remove(key);
                        this.size -= this.sizeOf(key, value);
                        continue;
                    }
                }

                return;
            }
        }
    }

    public final Bitmap remove(String key) {
        if(key == null) {
            throw new NullPointerException("key == null");
        } else {
            synchronized(this) {
                Bitmap previous = (Bitmap)this.map.remove(key);
                if(previous != null) {
                    this.size -= this.sizeOf(key, previous);
                }

                return previous;
            }
        }
    }

    public Collection<String> keys() {
        synchronized(this) {
            return new HashSet(this.map.keySet());
        }
    }

    public void clear() {
        this.trimToSize(-1);
    }
    
    //快取大小的計算方式
    private int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    public final synchronized String toString() {
        return String.format("LruCache[maxSize=%d]", new Object[]{Integer.valueOf(this.maxSize)});
    }
}

從原始碼可以看出,此LruMemoryCache類快取圖片是由一個LinkedHashMap來進行維護的,要弄懂LruMemoryCache首先要分析一下LinkedHashMap這個類。一些剛入門的朋友可能還不瞭解這個類,他們可能會存在疑問,到底什麼是LinkedHashMap,它又有哪些特點?下面就順道簡單分析一下,已熟悉瞭解的朋友請直接忽略。LinkedHashMap是HashMap的一個子類,它保留著插入的順序,當我們需要輸出的順序和輸入時的順序相同,並且允許使用null鍵和null值時,就可考慮選用LinkedHashMap,該類預設情況下是按插入的順序進行排序,當我們在此類建構函式中第三個引數傳入true時,則會按訪問順序進行排序,最新訪問的元素則會放在隊尾。
在此LruMemoryCache類的建構函式中,我們看到往其設定了一個快取圖片的最大值maxSize,並且例項化LinkedHashMap,而在LinkedHashMap建構函式中,第三個引數傳入的是true,說明它內部是按照訪問順序進行排序的。
我們接著看LruMemroyCache的put(String key, Bitmap value)方法,其方法中的SizeOf()計算的是每張圖片所佔有快取大小,以byte為單位,而變數名Size是記錄當前快取的bitmap的總大小,如果我們當前已經將該key之前就已經快取了bitmap,那就應該將之前快取的bitmap的大小減掉,防止重複快取相同的bitmap,接下來我們來看一看trimToSize(int maxSize)方法,方法中將當前快取圖片的總大小跟我們在建構函式中傳入的限制值進行比較,若快取的bitmap總數大小小於設定值,不作任何操作,直接返回;若大於設定值,則會獲取LinkedHashMap容器中的第一個實體完素,並且在size中減掉該刪除的bitmap對應的byte數。
只要有一點Java基礎,再通過認真分析原始碼,我們很容易瞭解到LruMemoryCache類的圖片快取邏輯。其次,系統也提供了其他圖片的快取邏輯,這裡我也順便簡單的講一下LimitedAgeMemoryCache類的實現邏輯,該類快取圖片是採用我們自己設定的繼承於MemoryCache的快取類,由建構函式第一個引數傳入。其次,它使用是用HashMap來儲存快取圖片時的時間值,當快取的圖片存在時間超過我們的設定值,就會在我們傳入的快取類中直接移除。至於其它的快取策略比較相似,比如說FIFOLimitedMemoryCache類就是利用HashMap與LinkedList來實現先進先出的快取策略,有興趣的朋友們可以去看看。

檔案快取策略(硬碟快取)

我們知道,像一些新浪微博等需要載入很多圖片的應用,本來圖片的載入就已經很慢了,如果載入完再次開啟時還需要繼續下載上次載入的圖片,相信很多使用者會直接把它卸了,因為這樣浪費的流量是巨大且無意義的。對於一個載入圖片比較多的應用,一個好的硬碟快取必不可少。辛運的是,Universal-Image-loader框架里正好提供了幾個常見的硬碟快取策略,接下來,讓我們瞭解瞭解這幾個常見的硬碟快取類:

  • FileCountLimitedDiscCache,這個類可以自己設定快取的圖片的個數,當超過我們所設定的值時,就會刪除最先加入到硬碟的那些檔案
  • LimitedAgeDiscCache,這個類可以設定檔案存活的最長時間,當檔案中超過這個設定值時,就會刪除這個檔案
  • TotalSizeLimitedDiscCache,這個類主要可以設定快取圖片總大小的最大值,當我們快取的圖片總大小超過這個值,就會刪除最先加入到硬碟的檔案
  • UnLimitedDiscCache,這個快取類比較特殊,它沒有任何的限值,所以執行起來比任何硬碟快取類都要快,框架預設是採用這個類,除非自己手動刪除快取的圖片,不然圖片快取不會自動被刪除。

其中,FileCountLimitedDiscCache與TotalSizeLimitedDiscCache這兩個快取類已經在最新的框架原始碼中刪掉了,而加入了新的LruDiscCache類,如果大家想要了解,可以看看LruDiscCache相關原始碼


上面主要介紹的是Universal-image-loader框架的有關圖片硬碟快取的相關類。當然,當這些策略不滿足我們的專案需求時候,我們也可以自定義自己的硬碟快取類。
下面就讓我們來學習一下框架中預設的UnLimitedDiscCache類的原始碼實現邏輯

package com.nostra13.universalimageloader.cache.disc.impl;

import com.nostra13.universalimageloader.cache.disc.impl.BaseDiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import java.io.File;

public class UnlimitedDiskCache extends BaseDiskCache {
    public UnlimitedDiskCache(File cacheDir) {
        super(cacheDir);
    }

    public UnlimitedDiskCache(File cacheDir, File reserveCacheDir) {
        super(cacheDir, reserveCacheDir);
    }

    public UnlimitedDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
        super(cacheDir, reserveCacheDir, fileNameGenerator);
    }
}

由上面原始碼可以知道,UnlimitedDiskCache是繼承的是BaseDiskCache類,而自己這個類內部並沒有實現自己什麼獨特的方法,也沒有重寫什麼函式,只是在建構函式中傳入引數,然後直接傳給父類處理。接下來看一下其父類BaseDiskCache的原始碼

package com.nostra13.universalimageloader.cache.disc.impl;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import com.nostra13.universalimageloader.cache.disc.DiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
import com.nostra13.universalimageloader.utils.IoUtils;
import com.nostra13.universalimageloader.utils.IoUtils.CopyListener;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public abstract class BaseDiskCache implements DiskCache {
    //預設的快取大小
    public static final int DEFAULT_BUFFER_SIZE = 32768;
    //預設的檔案壓縮格式
    public static final CompressFormat DEFAULT_COMPRESS_FORMAT;
    //預設檔案壓縮質量
    public static final int DEFAULT_COMPRESS_QUALITY = 100;
    private static final String ERROR_ARG_NULL = " argument must be not null";
    private static final String TEMP_IMAGE_POSTFIX = ".tmp";
    protected final File cacheDir;
    protected final File reserveCacheDir;
    protected final FileNameGenerator fileNameGenerator;
    protected int bufferSize;
    protected CompressFormat compressFormat;
    protected int compressQuality;

    public BaseDiskCache(File cacheDir) {
        this(cacheDir, (File)null);
    }

    public BaseDiskCache(File cacheDir, File reserveCacheDir) {
        this(cacheDir, reserveCacheDir, DefaultConfigurationFactory.createFileNameGenerator());
    }
    
    /**
    *@cacheDir,指的是檔案的快取目錄
    *@reserveCacheDir,備用的檔案快取目錄,可設為null,當cacheDir不可用時才用
    *@fileNameGenerator,檔名生成器,為快取的檔案生成檔名
    */
    public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
        this.bufferSize = '耀';
        this.compressFormat = DEFAULT_COMPRESS_FORMAT;
        this.compressQuality = 100;
        if(cacheDir == null) {
            throw new IllegalArgumentException("cacheDir argument must be not null");
        } else if(fileNameGenerator == null) {
            throw new IllegalArgumentException("fileNameGenerator argument must be not null");
        } else {
            this.cacheDir = cacheDir;
            this.reserveCacheDir = reserveCacheDir;
            this.fileNameGenerator = fileNameGenerator;
        }
    }

    public File getDirectory() {
        return this.cacheDir;
    }
    
    //根據檔案的Uri地址得到指向快取目錄所對應的檔案
    public File get(String imageUri) {
        return this.getFile(imageUri);
    }
    
                            
    public boolean save(String imageUri, InputStream imageStream, CopyListener listener) throws IOException {
        //根據檔案的Uri地址獲取指向快取目錄中相對應的檔案
        File imageFile = this.getFile(imageUri);
        //主要用來寫入的臨時檔案
        File tmpFile = new File(imageFile.getAbsolutePath() + ".tmp");
        boolean loaded = false;

        try {
            BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), this.bufferSize);

            try {
                //將檔案寫入到臨時檔案當中
                loaded = IoUtils.copyStream(imageStream, os, listener, this.bufferSize);
            } finally {
                IoUtils.closeSilently(os);
            }
        } finally {
            if(loaded && !tmpFile.renameTo(imageFile)) {
                loaded = false;
            }

            if(!loaded) {
                tmpFile.delete();
            }

        }

        return loaded;
    }

    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        File imageFile = this.getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + ".tmp");
        BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), this.bufferSize);
        boolean savedSuccessfully = false;

        try {
            savedSuccessfully = bitmap.compress(this.compressFormat, this.compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
            if(savedSuccessfully && !tmpFile.renameTo(imageFile)) {
                savedSuccessfully = false;
            }

            if(!savedSuccessfully) {
                tmpFile.delete();
            }

        }

        bitmap.recycle();
        return savedSuccessfully;
    }

    public boolean remove(String imageUri) {
        return this.getFile(imageUri).delete();
    }

    public void close() {
    }

    public void clear() {
        File[] files = this.cacheDir.listFiles();
        if(files != null) {
            File[] arr$ = files;
            int len$ = files.length;

            for(int i$ = 0; i$ < len$; ++i$) {
                File f = arr$[i$];
                f.delete();
            }
        }

    }
    
    //根據檔案的Uri地址生成一個指向快取目錄的檔案
    protected File getFile(String imageUri) {
        String fileName = this.fileNameGenerator.generate(imageUri);
        File dir = this.cacheDir;
        if(!this.cacheDir.exists() && !this.cacheDir.mkdirs() && this.reserveCacheDir != null && (this.reserveCacheDir.exists() || this.reserveCacheDir.mkdirs())) {
            dir = this.reserveCacheDir;
        }

        return new File(dir, fileName);
    }

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

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

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

    static {
             DEFAULT_COMPRESS_FORMAT = CompressFormat.PNG;
    }
}

由程式碼可以分析得出,該類是一個抽象類,並實現了DiskCache介面,對於方法sava(String imageUri, Bitmap bitmap)同樣是建立imageUri相應的的臨時檔案,這個臨時檔案是bitmap按照我們指定的圖片格式與圖片質量進行寫入的,若寫入不成功,直接刪除臨時檔案。
我們在使用過程中可以不自行配置硬碟快取的策略,直接用DefaultConfigurationFactory方法也可以,如果我們在ImageLoaderConfiguration中配置了diskCacheSize和diskCacheFileCount,那就是使用了LruDiskCache,否則使用的預設的UnlimitedDiscCache

總結

今天的分享也已經到達了尾聲,有關於Universal-Image-loader框架到目前主要的部份已經分析完了,這個框架確實寫得不錯,自己讀完程式碼也學到了很多。如果大家想真正瞭解這個框架,我希望可以堅持看完這兩篇文章,我相信,讀完後你肯定對圖片快取流程有了進一步的瞭解,針對自己的專案對這個框架的應用也更加會得心應手。當然,如果你覺得文章寫得有哪些地方錯誤或者不明白的地方,歡迎留言交流,共同進步。
以後我會大概每個星期都會更新自己的部落格,主要寫的是技術上、工作上、生活中的點點滴滴,歡迎大家持續關注,謝謝!

相關文章