1. 程式人生 > >Android 開源框架Universal-Image-Loader完全解析(二)--- 圖片快取策略詳解

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

本篇文章繼續為大家介紹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.universalimageloader.cache.memory.impl;

import android.graphics.Bitmap;
import com.nostra13.universalimageloader.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.universalimageloader.cache.disc.impl;

import com.nostra13.universalimageloader.cache.disc.LimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
import com.nostra13.universalimageloader.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.universalimageloader.cache.disc;

import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
import com.nostra13.universalimageloader.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(...)配置

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