Android開源框架Universal-Image-Loader緩存機制淺析

緩存

提高用戶體驗,同時也使得應用更加流暢,也就是緩存圖片至內存時,可以更加高效的工作。

配置

在應用中配置ImageLoaderConfiguration參數(shù)(注意:只配置一次就好了,如多次配置,則默認第一次的配置參數(shù))

默認設置(即框架已配置好了參數(shù))

ImageLoaderConfiguration configuration = ImageLoaderConfiguration.createDefault(this);

自定義設置

File cacheDir = StorageUtils.getCacheDirectory(context);  //緩存文件夾路徑
    ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
            .memoryCacheExtraOptions(480, 800) // default = device screen dimensions 內存緩存文件的最大長寬
           // .diskCacheExtraOptions(480, 800, null)  // 本地緩存的詳細信息(緩存的最大長寬),最好不要設置這個 
            .taskExecutor(...)
            .taskExecutorForCachedImages(...)
            .threadPoolSize(5) // default是3個線程池  線程池內加載的數(shù)量
            .threadPriority(Thread.NORM_PRIORITY - 2) // default 設置當前線程的優(yōu)先級
            .tasksProcessingOrder(QueueProcessingType.FIFO) // default
            .denyCacheImageMultipleSizesInMemory()
            .memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通過自己的內存緩存實現(xiàn)
            .memoryCacheSize(2 * 1024 * 1024)  // 內存緩存的最大值
            .memoryCacheSizePercentage(13) // default
            .diskCache(new UnlimitedDiscCache(cacheDir)) // default ,可以自定義緩存路徑  
            .diskCacheSize(50 * 1024 * 1024) // 50 Mb sd卡(本地)緩存的最大值
            .diskCacheFileCount(100)  // 可以緩存的文件數(shù)量 
            // default為使用HASHCODE對UIL進行加密命名, 還可以用MD5(new Md5FileNameGenerator())加密
            .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) 
            .imageDownloader(new BaseImageDownloader(context)) // default
            .imageDecoder(new BaseImageDecoder()) // default
            .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
            .writeDebugLogs() // 打印debug log
            ImageLoader.getInstance().init(config.build()); //初始化配置,開始構建

主體有三個,分別是UI,緩存模塊和數(shù)據(jù)源(網絡)。

UI: 請求數(shù)據(jù),使用唯一的Key值索引Memory Cache中的Bitmap。
緩存模塊:分兩種,一是內存緩存,通過緩存搜索,如果能找到Key值對應的Bitmap,則返回數(shù)據(jù)。二是硬盤存儲,使用唯一Key值對應的文件名,檢索SDCard上的文件。

默認的內存緩存實現(xiàn)是LruMemoryCache,磁盤緩存是UnlimitedDiscCache。

  • 先說說 MemoryCache 接口實現(xiàn),底下有很多實現(xiàn)類
  • 它負責定義通用規(guī)則,具體的實現(xiàn)工作由不同緩存算法的子類去實現(xiàn)即可。
  • 這是一個繼承和多態(tài)的體現(xiàn)。
  • 當前分析的就是 LruMemoryCache 類,這也是默認的緩存策略
  • 內存緩存策略設計與實現(xiàn)
  • 只使用的是強引用緩存
    LruMemoryCache(這個類就是這個開源框架默認的內存緩存類,緩存的是bitmap的強引用,下面我會從源碼上面分析這個類)

  • 使用強引用和弱引用相結合的緩存有
    UsingFreqLimitedMemoryCache(如果緩存的圖片總量超過限定值,先刪除使用頻率最小的bitmap)
    LRULimitedMemoryCache(這個也是使用的lru算法,和LruMemoryCache不同的是,他緩存的是bitmap的弱引用)
    FIFOLimitedMemoryCache(先進先出的緩存策略,當超過設定值,先刪除最先加入緩存的bitmap)
    LargestLimitedMemoryCache(當超過緩存限定值,先刪除最大的bitmap對象)
    LimitedAgeMemoryCache(當 bitmap加入緩存中的時間超過我們設定的值,將其刪除)

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

LruMemoryCach

LruMemoryCach這也是默認的緩存策略

1、最近最少使用算法。
2、當圖片緩存到磁盤之后,然后就需要緩存到內存中。
3、需要定義的緩存空間是多大呢?(默認大小為 app 可用空間的 1/8 大小。trimToSize 方法可以確保當前空間還有剩余。)
4、緩存的數(shù)據(jù)結構是什么呢?(LinkedHashMap<String,Map>)

LruMemoryCache:一種使用強引用來保存有數(shù)量限制的Bitmap的cache(在空間有限的情況,保留最近使用過的Bitmap)。每次Bitmap被訪問時,它就被移動到一個隊列的頭部。當Bitmap被添加到一個空間已滿的cache時,在隊列末尾的Bitmap會被擠出去并變成適合被GC回收的狀態(tài)。
注意:這個cache只使用強引用來保存Bitmap。

/** @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;
      //使用LinkedHashMap來緩存數(shù)據(jù)
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }

思考:為什么不用HashMap來緩存數(shù)據(jù)?好了,我們繼續(xù)看源碼

LruMemoryCache.get(...);

/**
     * 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) {
     //代碼中除了異常判斷,就是利用synchronized進行同步控制。
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        synchronized (this) {
            return map.get(key);
        }
    }
public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }

LruMemoryCache保留在空間有限的情況下保留最近使用過的Bitmap。
LinkedHashMap中的get()方法不僅返回所匹配的值,并且在返回前還會將所匹配的key對應的entry調整在列表中的順序(LinkedHashMap使用雙鏈表來保存數(shù)據(jù),不懂可以去看看Map集合類了解),讓它處于列表的最后。當然,這種情況必須是在LinkedHashMap中accessOrder==true的情況下才生效的,反之就是get()方法不會改變被匹配的key對應的entry在列表中的位置。

@Override
 public V get(Object key) {
          /*
           * This method is overridden to eliminate the need for a polymorphic
           * invocation in superclass at the expense of code duplication.
           */
          if (key == null) {
              HashMapEntry<K, V> e = entryForNullKey;
              if (e == null)
                  return null;
             if (accessOrder)//調整entry在列表中的位置,其實就是雙向鏈表的調整。它判斷accessOrder
                 makeTail((LinkedEntry<K, V>) e);
             return e.value;
         }
 
         // Replace with Collections.secondaryHash when the VM is fast enough (http://b/8290590).
         int hash = secondaryHash(key);
         HashMapEntry<K, V>[] tab = table;
         for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                 e != null; e = e.next) {
             K eKey = e.key;
             if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                 if (accessOrder)
                     makeTail((LinkedEntry<K, V>) e);
                 return e.value;
             }
         }
         return null;
     }

結論

LruMemoryCache緩存的存儲數(shù)據(jù)結構是LinkedHashMap<String,Map>,在LinkedHashMap.get()方法執(zhí)行后,LinkedHashMap中entry的順序會得到調整。

如果圖片過多,就會保留最近使用的,其他都要被回收,怎么才不被剔出呢?

需要定義的緩存空間是多大呢?
1、默認大小為 app 可用空間的 1/8 大小。
2、trimToSize 方法可以確保當前空間還有剩余。

LruMemoryCache中的trimToSize(...)這個函數(shù)就是用來限定LruMemoryCache的大小不要超過用戶限定的大小,cache的大小由用戶在LruMemoryCache剛開始初始化的時候限定。那么就看看LruMemoryCache中的put方法。

/** 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);
            //map.put()的返回值如果不為空,說明存在跟key對應的entry,put操作只是更新原有key對應的entry
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

當Bitmap緩存的大小超過原來設定的maxSize時應該是在trimToSize(...)這個函數(shù)中做到的。遍歷map,將多余的項(代碼中對應toEvict)剔除掉,直到當前cache的大小等于或小于限定的大小。

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

磁盤緩存策略設計與實現(xiàn)

自己實現(xiàn)磁盤緩存,要考慮的太多,UIL提供了幾種常見的磁盤緩存策略,當然你覺得都不符合你的要求,你也可以自己去擴展的。

  • FileCountLimitedDiscCache(可以設定緩存圖片的個數(shù),當超過設定值,刪除掉最先加入到硬盤的文件)
  • LimitedAgeDiscCache(設定文件存活的最長時間,當超過這個值,就刪除該文件)
  • TotalSizeLimitedDiscCache(設定緩存bitmap的最大值,當超過這個值,刪除最先加入到硬盤的文件)
  • UnlimitedDiscCache(這個緩存類沒有任何的限制)

在UIL中有著比較完整的存儲策略,根據(jù)預先指定的空間大小,使用頻率(生命周期),文件個數(shù)的約束條件,都有著對應的實現(xiàn)策略。最基礎的接口DiscCacheAware和抽象類BaseDiscCache

UnlimitedDiscCache解析

UnlimitedDiscCache實現(xiàn)DiskCache接口,是ImageLoaderConfiguration中默認的磁盤緩存處理。用它的時候,磁盤緩存的大小是不受限的。
接下來看看實現(xiàn)UnlimitedDiscCache的源代碼,通過源代碼我們發(fā)現(xiàn)他其實就是繼承了BaseDiscCache,這個類內部沒有實現(xiàn)自己獨特的方法,也沒有重寫什么,那么我們就直接看BaseDiscCache這個類。BaseDiscCache繼承了DiscCache。在分析這個類之前,我們先想想自己實現(xiàn)一個磁盤緩存需要做多少麻煩的事情:

  1. 圖片的命名會不會重。你沒有辦法知道用戶下載的圖片原始的文件名是怎么樣的,因此很可能因為文件重名將有用的圖片給覆蓋掉了。
  2. 當應用卡頓或網絡延遲的時候,同一張圖片反復被下載。
  3. 處理圖片寫入磁盤可能遇到的延遲和同步問題。

直接上源碼

/**
     * @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
     */
    public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
        if (cacheDir == null) {
            throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);
        }
        if (fileNameGenerator == null) {
            throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL);
        }

        this.cacheDir = cacheDir;
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
    }

我們看一下BaseDiscCache的構造函數(shù):
cacheDir: 文件緩存目錄
reserveCacheDir: 備用的文件緩存目錄,可以為null。它只有當cacheDir不能用的時候才有用。
fileNameGenerator: 文件名生成器。為緩存的文件生成文件名。

當看到fileNameGenerator,有點疑慮,我們并沒有設置文件名???查閱代碼發(fā)現(xiàn)是默認生成DefaultConfigurationFactory.createFileNameGenerator())。
代碼如下:

/**
     * @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.
     */
    public BaseDiskCache(File cacheDir, File reserveCacheDir) {
        this(cacheDir, reserveCacheDir, DefaultConfigurationFactory.createFileNameGenerator());
    }
/** Creates {@linkplain HashCodeFileNameGenerator default implementation} of FileNameGenerator */
    public static FileNameGenerator createFileNameGenerator() {
        return new HashCodeFileNameGenerator();
    }
/**
 * Names image file as image URI {@linkplain String#hashCode() hashcode}
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.3.1
 */
public class HashCodeFileNameGenerator implements FileNameGenerator {
    @Override
    public String generate(String imageUri) {
        return String.valueOf(imageUri.hashCode());
    }
}

UIL中有3種文件命名策略,默認的文件命名策略DefaultConfigurationFactory.createFileNameGenerator()。它是一個HashCodeFileNameGenerator。用String.hashCode()進行文件名的生成,這樣也不會生成重復的文件名,就那么簡單。

接著看看是如何存儲數(shù)據(jù)的
直接看源碼

@Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        //主要用于生成一個指向緩存目錄中的文件,在這個函數(shù)里面調用了剛剛介紹過的fileNameGenerator來生成文件名。
        File imageFile = getFile(imageUri);
        //它是用來寫入bitmap的臨時文件,然后就把這個文件給刪除了
        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加載圖片的一般流程是先判斷內存中是否有對應的Bitmap,再判斷磁盤(disk)中是否有,如果沒有就從網絡中加載。最后根據(jù)原先在UIL中的配置判斷是否需要緩存Bitmap到內存或磁盤中。也就是說,當需要調用BaseDiscCache.save(...)之前,其實已經判斷過這個文件不在磁盤中。

/** Returns file object (not null) for incoming image URI. File object can reference to non-existing file. */
    protected File getFile(String imageUri) {
       //利用fileNameGenerator生成一個唯一的文件名
        String fileName = fileNameGenerator.generate(imageUri);
        File dir = cacheDir;
        //當cacheDir不可用的時候,就是用reserveCachedir作為緩存目錄了。
        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
                dir = reserveCacheDir;
            }
        }
        //最后返回一個指向文件的對象,但是要注意當File類型的對象指向的文件不存在時,file會為null,而不是報錯。
        return new File(dir, fileName);
    }

總結
內存緩存其實就是利用Map接口的對象在內存中進行緩存,可能有不同的存儲機制。磁盤緩存其實就是將文件寫入磁盤。這樣就達到緩存目的。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容