Android图片缓存(软引用,LRU)

每天一篇系列:
强化知识体系,查漏补缺。
欢迎指正,共同学习!

软引用比较常见的使用是在图片缓存:

1.创建软引用HashMap作为缓存

private Map> imageCache = new HashMap>();

2.向缓存中添加新Bitmap

public void addBitmapToCache(String path) {
        // 强引用的Bitmap对象,这里bitmap是局部变量,该方法执行完毕后bitmap就会释放
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 软引用的Bitmap对象
        SoftReference softBitmap = new SoftReference(bitmap);
        // 添加该对象到Map中使其缓存
        imageCache.put(path, softBitmap);
}

3.从缓存中读取Bitmap

public Bitmap getBitmapByPath(String path) {
        // 从缓存中取软引用的Bitmap对象
        SoftReference softBitmap = imageCache.get(path);
        // 判断是否存在软引用
        if (softBitmap == null) {
            return null;
        }
        // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
        Bitmap bitmap = softBitmap.get();
        if(bitmap==null){
            return null;
        }
       return bitmap;
}

在图片缓存的处理中除了软引用的优化外,主要涉及的是LRU算法策略,这里需要涉及到图片的三级缓存问题:

三级缓存的设计:
首先url作为缓存的关键字和bitmap对象关联起来,先从内存LruCache中查询是否存在缓存,如果没有再从DiskLruCache查询是否存在缓存,如果仍然没有缓存,则开启异步任务AsyncTask去网络下载。

LRU(Least Recently Used)算法的核心是当缓存满了的时候,回收近期使用最少的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。

1.LruCache的介绍

public class LruCache {
    private final LinkedHashMap map;
...
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap(0, 0.75f, true);
    }
...
}

从源码上可以看到LruCache是个泛型类。
主要算法原理是把最近使用的对象用强引用存储在LinkedHashMap中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。
LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。这个缓存对象的列表就是LinkedHashMap,LinkedHashMap最近访问的最后输出的特性正好满足的LRU缓存算法的思想。
https://www.jianshu.com/p/b49a111147ee


可以看看LruCache的put()方法:

    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        //保证线程安全
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }

其中trimToSize会在内存满时回收近期使用较少的缓存对象。

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

                if (size <= maxSize) {
                    break;
                }
                //链表的头节点,即近期最少使用的对象
                Map.Entry toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

剩下的就是如何get缓存了:

    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        /*
         * Attempt to create a value. This may take a long time, and the map
         * may be different when create() returns. If a conflicting value was
         * added to the map while create() was working, we leave that value in
         * the map and release the created value.
         */

        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

get函数里面给开发者预留了一个可以继承的creat实现,当缓存不存在时,可以根据开发者自实现的缓存补充到LinkedHashMap中,并且对key值冲突的情况作了安全处理。
从LruCache的源码实现中希望可以深入到LinkedHashMap等集合的使用场合,synchronized同步的意义、泛型类的使用,父类调用sizeof是否使用了某种设计模式等更广层面的思考。

2.DiskLruCache介绍

public final class DiskLruCache implements Closeable {
...
    private final LinkedHashMap lruEntries
            = new LinkedHashMap(0, 0.75f, true);
...
    private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
...
}

不同于LruCache,LruCache是将数据缓存到内存中去,而DiskLruCache是外部缓存,例如可以将网络下载的图片永久的缓存到手机外部存储中去。同样DiskLruCache内部也利用了LinkedHashMap特性,还利用了ExecutorService来优化线程池。
DiskLruCache被声明未final,表明这个类不能被继承。
(当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法)
(使用final修饰方法的原因是把方法锁定,以防任何继承类修改它的含义)
(对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象)。
其中LinkedHashMap的使用与LRU方式没有差别。通过一个文件记录缓存读取记录,从而记录缓存事件。

    public synchronized Snapshot get(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            return null;
        }

        if (!entry.readable) {
            return null;
        }

        /*
         * Open all streams eagerly to guarantee that we see a single published
         * snapshot. If we opened streams lazily then the streams could come
         * from different edits.
         */
        InputStream[] ins = new InputStream[valueCount];
        try {
            for (int i = 0; i < valueCount; i++) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));
            }
        } catch (FileNotFoundException e) {
            // a file must have been deleted manually!
            return null;
        }

        redundantOpCount++;
        journalWriter.append(READ + ' ' + key + '\n');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }

        return new Snapshot(key, entry.sequenceNumber, ins);
    }

你可能感兴趣的:(Android图片缓存(软引用,LRU))