面试之——Android-Universal-Image-Loader缓存(二)

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进去的,该数据就在里面是什么顺序了,所以在取的时候,最先取出来的就是最先放进去的那个顺序
咋们重点看下LruMemoryCacheput方法,是怎么放数据进去的:

@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,紧接着判断之前是否存在相同keybitmap,如果存在需要减掉之前bitmap的大小。在trimToSize方法里面,如果当前的size没超过maxSize,不需要做任何处理。否则的话,从linkedHashMap中移除第一个bitmap。也正是LruMemoryCache缓存的精华所在,因为我们的第一个bitmap是我们最先put进来的,也是我们不经常访问的bitmap。所以在put的时候,最先移除的也是我们不经常访问的bitmap

关于memory还有其他几种cache,这里就不一一分析了:

面试之——Android-Universal-Image-Loader缓存(二)_第1张图片
MemoryCache截图.png

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:

DiskCache截图.png

那咱们去看看 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多久了,是否需要去移除该数据

你可能感兴趣的:(面试之——Android-Universal-Image-Loader缓存(二))