MemoryCache
前篇文章已经对Android-Universal-Image-Loader
源码部分分析了一遍,还没有看的小伙伴赶快猛戳,该篇内容主要围绕Android-Universal-Image-Loader
缓存部分进行分析。
直接去ImageLoader
类的displayImage
方法去看看,有这么一句:
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
很显然configuration.memoryCache
是一个内存对象,那咱们去瞧瞧是怎么生成的:
private void initEmptyFieldsWithDefaultValues() {
//省略代码
if (memoryCache == null) {
memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
}
//省略代码
}
该方法是ImageLoaderConfiguration
类的内部类Builder
生成的,这里生成的就是一个默认的内存对象了,里面还有个方法可以传入我们想要的memoryCache
对象:
public Builder memoryCache(MemoryCache memoryCache) {
if (memoryCacheSize != 0) {
L.w(WARNING_OVERLAP_MEMORY_CACHE);
}
this.memoryCache = memoryCache;
return this;
}
那咱们去看看默认生成的MemoryCache
是个什么对象:
public static MemoryCache createMemoryCache(Context context, int memoryCacheSize) {
if (memoryCacheSize == 0) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();
if (hasHoneycomb() && isLargeHeap(context)) {
memoryClass = getLargeMemoryClass(am);
}
memoryCacheSize = 1024 * 1024 * memoryClass / 8;
}
return new LruMemoryCache(memoryCacheSize);
}
可以看到这里默认是一个LruMemoryCache
对象了,那咋们去瞧瞧:
private final LinkedHashMap 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(0, 0.75f, true);
}
可以看到咋们这里内存存储其实就是一个LinkedHashMap,可能咋们平时用的是HashMap。首先得明白LinkedHashMap它是什么样的存储结构,它是一个具有线性的迭代存储结构,意思是什么时候put进去的,该数据就在里面是什么顺序了,所以在取的时候,最先取出来的就是最先放进去的那个顺序。
咋们重点看下LruMemoryCache的put方法,是怎么放数据进去的:
@Override
public final boolean put(String key, Bitmap value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
synchronized (this) {
//用最新的bitmap的size累加
size += sizeOf(key, value);
//如果当前的bitmap存在了,需要更新当前的总size
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
}
trimToSize(maxSize);
return true;
}
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!");
}
//如果当前的size没超过maxSize,直接跳过
if (size <= maxSize || map.isEmpty()) {
break;
}
当前缓存的bitmap总数大于maxSize,删除LinkedHashMap中的第一个元素,size中减去该bitmap对应的byte数
Map.Entry toEvict = map.entrySet().iterator().next();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
//移除掉我们最先put进去的bitmap
map.remove(key);
size -= sizeOf(key, value);
}
}
}
很明显,在put的时候,先是累加当前size
,紧接着判断之前是否存在相同key
的bitmap
,如果存在需要减掉之前bitmap
的大小。在trimToSize
方法里面,如果当前的size
没超过maxSize
,不需要做任何处理。否则的话,从linkedHashMap
中移除第一个bitmap。也正是LruMemoryCache
缓存的精华所在,因为我们的第一个bitmap是我们最先put进来的,也是我们不经常访问的bitmap。所以在put的时候,最先移除的也是我们不经常访问的bitmap。
关于memory
还有其他几种cache,这里就不一一分析了:
DiskCache
这里就直接跳到ImageLoaderConfiguration
内部类Builder
中找默认的DiskCache
是如何生成的吧:
private void initEmptyFieldsWithDefaultValues() {
//省略代码
if (diskCache == null) {
if (diskCacheFileNameGenerator == null) {
diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
}
diskCache = DefaultConfigurationFactory
.createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
}
//省略代码
}
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);
try {
return new LruDiskCache(individualCacheDir, reserveCacheDir, diskCacheFileNameGenerator, diskCacheSize,
diskCacheFileCount);
} catch (IOException e) {
L.e(e);
// continue and create unlimited cache
}
}
File cacheDir = StorageUtils.getCacheDirectory(context);
return new UnlimitedDiskCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);
}
很显然默认创建的是一个UnlimitedDiskCache
,咋们也去瞧瞧:
public class UnlimitedDiskCache extends BaseDiskCache {
/** @param cacheDir Directory for file caching */
public UnlimitedDiskCache(File cacheDir) {
super(cacheDir);
}
/**
* @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 UnlimitedDiskCache(File cacheDir, File reserveCacheDir) {
super(cacheDir, reserveCacheDir);
}
/**
* @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 UnlimitedDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
super(cacheDir, reserveCacheDir, fileNameGenerator);
}
}
很显然,这里什么都没做,看来得去父类找找了,在父类里面着重看下两个重载的save
方法:
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOEx
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;
}
@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;
}
其实两个方法是类似的,只不过处理OutputStream
方式不一样而已,这里提一下第一个save
方法的IoUtils.copyStream
方法:
public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)
throws IOException {
int current = 0;
int total = is.available();
if (total <= 0) {
total = DEFAULT_IMAGE_TOTAL_SIZE;
}
final byte[] bytes = new byte[bufferSize];
int count;
if (shouldStopLoading(listener, current, total)) return false;
while ((count = is.read(bytes, 0, bufferSize)) != -1) {
os.write(bytes, 0, count);
current += count;
if (shouldStopLoading(listener, current, total)) return false;
}
os.flush();
return true;
}
private static boolean shouldStopLoading(CopyListener listener, int current, int total) {
if (listener != null) {
boolean shouldContinue = listener.onBytesCopied(current, total);
if (!shouldContinue) {
//如果当前的size没超过总共的size话,直接视为不存储了
if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
return true; // if loaded more than 75% then continue loading anyway
}
}
}
return false;
}
上面的shouldStopLoading
方法里面判断了如果当前copy的大小不够总大小的75%,则认为是失败copy过程。这里就有点难得理解了,其实我看到这的时候,总感觉没看明白一样。
这里要重点提提另外一个DiskCache
:
那咱们去看看
LimitedAgeDiskCache
:
private final Map loadingDates = Collections.synchronizedMap(new HashMap());
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
boolean saved = super.save(imageUri, imageStream, listener);
rememberUsage(imageUri);
return saved;
}
@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
boolean saved = super.save(imageUri, bitmap);
rememberUsage(imageUri);
return saved;
}
private void rememberUsage(String imageUri) {
File file = getFile(imageUri);
long currentTime = System.currentTimeMillis();
file.setLastModified(currentTime);
loadingDates.put(file, currentTime);
}
很显然,这里在save
的时候,都去存储了文件的当前时间毫秒数,放到了map
中,这里的map
存储file
的当前毫秒数有什么用呢?咱们去get
方法中瞧瞧:
@Override
public File get(String imageUri) {
File file = super.get(imageUri);
if (file != null && file.exists()) {
boolean cached;
Long loadingDate = loadingDates.get(file);
if (loadingDate == null) {
cached = false;
loadingDate = file.lastModified();
} else {
cached = true;
}
//如果当前文件存活时间超过了maxFileAge,需要删除文件了
if (System.currentTimeMillis() - loadingDate > maxFileAge) {
file.delete();
loadingDates.remove(file);
} else if (!cached) {
loadingDates.put(file, loadingDate);
}
}
return file;
}
很显然,这里通过存储当前file的存储时间,来判断需不需要删除该file,所以说如果当前图片存储的时间超过了maxFileAge,下次取的时候,直接删除掉了该图片。
总结:
-
LruMemoryCache
存储特性是,不经常使用的数据,是最先被移除掉的,最新的数据,都是排在后面,里面是LinkHashMap
的存储结构 -
LimitedAgeDiskCache
通过存储当前数据的save时间,然后判断save多久了,是否需要去移除该数据