Android Glide图片缓存原理及手写实现

文章目录

  • Glide的四层缓存
  • 第一层 活动资源
  • 第二层 内存缓存
  • 第三、四层 磁盘缓存
  • Bitmap复用
  • 手写实现
    • 缓存使用
    • Resource
    • ActiveResource
    • 弱引用示例
    • LruMemoryCache
    • LruBitmapPool

Glide : https://github.com/bumptech/glide

参考:
Glide缓存与解码复用

Android LruCache源码解析

LinkedHashMap 核心源码分析

Glide生命周期管理

Glide注册ModelLoader

Glide中的装饰者InputStream.resources

LruCache注释源码

ReferenceQueue的使用

Java中的四种引用以及ReferenceQueue

android图片压缩质量参数Bitmap.Config

缓存位图

管理位图内存

Java中equals()和HashCode()的关系

hashcode详解

Glide的四层缓存

默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:

  1. 活动资源 (Active Resources)
  2. 内存缓存 (Memory Cache)
  3. 资源类型(Resource Disk Cache)
  4. 原始数据 (Data Disk Cache)
  • 活动资源:如果当前对应的图片资源正在使用,则这个图片会被Glide放入活动缓存。
  • 内存缓存:如果图片最近被加载过,并且当前没有使用这个图片,则会被放入内存中
  • 资源类型: 被解码后的图片写入磁盘文件中,解码的过程可能修改了图片的参数(如:inSampleSize、inPreferredConfig)
  • 原始数据: 图片原始数据在磁盘中的缓存(从网络、文件中直接获得的原始数据)

相较于常见的内存+磁盘缓存,Glide将其缓存分成了4层。其中Glide将磁盘缓存分为了 资源类型(Resource Disk Cache)和 原始数据 (Data Disk Cache)

Glide缓存查找时序图如下:
Android Glide图片缓存原理及手写实现_第1张图片
图:缓存查找


第一层 活动资源

当需要加载某张图片能够从内存缓存中获得的时候,在图片加载时主动将对应图片从内存缓存中移除,加入到活动资源中。

这样也可以避免因为达到内存缓存最大值或者系统内存压力导致的内存缓存清理,从而释放掉活动资源中的图片(recycle)。

活动资源中是一个”引用计数"的图片资源的弱引用集合

因为同一张图片可能在多个地方被同时使用,每一次使用都会将引用计数+1,而当引用计数为0时候,则表示这个图片没有被使用也就是没有强引用了。这样则会将图片从活动资源中移除,并加入内存缓存。

第二层 内存缓存

内存缓存默认使用LRU(缓存淘汰算法/最近最少使用算法),当资源从活动资源移除的时候,会加入此缓存。使用图片的时候会主动从此缓存移除,加入活动资源。

LRU在Android中提供了LruCache工具类。

构造LinkedHashMap的accessOrder设置为true。在使用的此map的时候,自动进行排序(每次get/put,会将使用的value放入链表header头部)。LruCache会在每次get/put的时候判断数据如果达到了maxSize,则会优先删除tail尾端的数据。

Android Glide图片缓存原理及手写实现_第2张图片
图:LRU

磁盘缓存同样使用LRU算法。

第三、四层 磁盘缓存

Resource 缓存的是经过解码后的图片,如果再使用就不需要再去进行解码配置(BitmapFactory.Options),加快获得图片速度。比如原图是一个100x100的ARGB_8888图片,在首次使用的时候需要的是50x50的RGB_565图片,那么Resource将50x50 RGB_565缓存下来,再次使用此图片的时候就可以从 Resource 获得。不需要去计算inSampleSize(缩放因子)。

Data 缓存的则是图像原始数据。

Bitmap复用

如果缓存都不存在,那么会从源地址获得图片(网络/文件)。而在解析图片的时候会需要可以获得BitmapPool(复用池),达到复用的效果。

Android Glide图片缓存原理及手写实现_第3张图片
图:复用前

Android Glide图片缓存原理及手写实现_第4张图片
图:复用后

复用效果如上。在未使用复用的情况下,每张图片都需要一块内存。而使用复用的时候,如果存在能被复用的图片会重复使用该图片的内存。

所以复用并不能减少程序正在使用的内存大小。Bitmap复用,解决的是减少频繁申请内存带来的性能(抖动、碎片)问题。

手写实现

这里实现了 第一层活动资源 和 第二层内存缓存,以及Bitmap复用池的实现

缓存使用

先看看我们手写实现后如何使用,使用如下:

public class CacheTest implements Resource.ResourceListener, MemoryCache.ResourceRemoveListener {

    LruMemoryCache lruMemoryCache;
    ActiveResource activeResource;
    BitmapPool bitmapPool;

    public Resource test(Key key) {
        bitmapPool = new LruBitmapPool(10);
        //内存缓存
        lruMemoryCache = new LruMemoryCache(10);
        lruMemoryCache.setResourceRemoveListener(this);
        //活动资源缓存
        activeResource = new ActiveResource(this);

        /**
         * 第一步 从活动资源中查找是否有正在使用的图片
         */
        Resource resource = activeResource.get(key);
        if (null != resource) {
            //当不使用的时候 release
            resource.acquire();
            return resource;
        }
        /**
         * 第二步 从内存缓存中查找
         */
        resource = lruMemoryCache.get(key);
        if (null != resource) {
            //1.为什么从内存缓存移除?
            // 因为lru可能移除此图片 我们也可能recycle掉此图片
            // 如果不移除,则下次使用此图片从活动资源中能找到,但是这个图片可能被recycle掉了
            lruMemoryCache.remove2(key);
            resource.acquire();
            activeResource.activate(key, resource);
            return resource;
        }
        return null;
    }

    /**
     * 这个资源没有正在使用了
     * 将其从活动资源移除
     * 重新加入到内存缓存中
     */
    @Override
    public void onResourceReleased(Key key, Resource resource) {
        activeResource.deactivate(key);
        lruMemoryCache.put(key, resource);
    }

    /**
     * 从内存缓存被动移除 回调
     * 放入 复用池
     */
    @Override
    public void onResourceRemoved(Resource resource) {
        bitmapPool.put(resource.getBitmap());
    }
}

从上面可以看出,使用主要分为以下几步:

  1. 第一步 从活动资源中查找是否有正在使用的图片,如果找到了 引用计数+1,返回图片资源;
  2. 第二步 如果第一步活动资源中没有找到,就从内存缓存中查找,如果找到了,就将其从内存缓存中移除(lruMemoryCache的remove2方法来执行资源在内存缓存中的移除),引用计数+1,并添加到活动资源中,并返回图片资源;
  3. 如果资源没有正在使用,即引用计数=0的时候,就会调用Resource.ResourceListener的onResourceReleased的回调方法释放资源,在回调方法中将资源从活动资源中移除,并存入内存缓存中;
  4. 当资源从内存缓存中被移除的时候(根据LruCache算法实现),会调用MemoryCache.ResourceRemoveListener的onResourceRemoved方法,将资源的Bitmap存放到Bitmap复用池中。

Resource

public class Resource {

    private Bitmap bitmap;
    //引用计数
    private int acquired;
    private ResourceListener listener;
    private Key key;

    public Resource(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    /**
     * 当acquired 为0的时候 回调 onResourceReleased
     */
    public interface ResourceListener {
        void onResourceReleased(Key key, Resource resource);
    }

    public void setResourceListener(Key key, ResourceListener listener) {
        this.key = key;
        this.listener = listener;
    }

    /**
     * 释放
     */
    public void recycle() {
        if (acquired > 0) {
            return;
        }
        if (!bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }

    /**
     * 引用计数-1
     */
    public void release() {
        if (--acquired == 0) {
            listener.onResourceReleased(key, this);
        }
    }

    /**
     * 引用计数+1
     */
    public void acquire() {
        if (bitmap.isRecycled()) {
            throw new IllegalStateException("Acquire a recycled resource");
        }
        ++acquired;
    }

}

Resource中包含了Key、Bitmap、acquired引用计数、以及ResourceListener接口。

当调用release方法时,如果acquired引用计数=0,就调用ResourceListener的onResourceReleased方法。如:上面的CacheTest实现了ResourceListener接口,在调用onResourceReleased方法时,可以将资源从活动资源中移除,并添加到内存缓存中。

再次贴上CacheTest中onResourceReleased方法,如下:

 	/**
     * 这个资源没有正在使用了,将其从活动资源移除, 重新加入到内存缓存中
     */
    @Override
    public void onResourceReleased(Key key, Resource resource) {
        activeResource.deactivate(key);
        lruMemoryCache.put(key, resource);
    }

ActiveResource

/**
 * 正在使用的图片资源
 */
public class ActiveResource {

    private ReferenceQueue<Resource> queue;
    private final Resource.ResourceListener resourceListener;
    private Map<Key, ResourceWeakReference> activeResources = new HashMap<>();
    private Thread cleanReferenceQueueThread;
    private boolean isShutdown;

    public ActiveResource(Resource.ResourceListener resourceListener) {
        this.resourceListener = resourceListener;
    }

    /**
     * 加入活动缓存
     */
    public void activate(Key key, Resource resource) {
        resource.setResourceListener(key, resourceListener);
        activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
    }

    /**
     * 移除活动缓存
     */
    public Resource deactivate(Key key) {
        ResourceWeakReference reference = activeResources.remove(key);
        if (reference != null) {
            return reference.get();
        }
        return null;
    }

    /**
     * 获得对应value
     */
    public Resource get(Key key) {
        ResourceWeakReference reference = activeResources.get(key);
        if (reference != null) {
            return reference.get();
        }
        return null;
    }

    /**
     * 引用队列,通知我们弱引用被回收了
     * 让我们得到通知的作用
     */
    private ReferenceQueue<Resource> getReferenceQueue() {
        if (null == queue) {
            queue = new ReferenceQueue<>();
            cleanReferenceQueueThread = new Thread() {
                @Override
                public void run() {
                    while (!isShutdown) {
                        try {
                            //被回收掉的引用
                            ResourceWeakReference ref = (ResourceWeakReference) queue.remove();
                            activeResources.remove(ref.key);
                        } catch (InterruptedException e) {
                        }
                    }
                }
            };
            cleanReferenceQueueThread.start();
        }
        return queue;
    }
    
    void shutdown() {
        isShutdown = true;
        if (cleanReferenceQueueThread != null) {
            cleanReferenceQueueThread.interrupt();
            try {
                //5s  必须结束掉线程
                cleanReferenceQueueThread.join(TimeUnit.SECONDS.toMillis(5));
                if (cleanReferenceQueueThread.isAlive()) {
                    throw new RuntimeException("Failed to join in time");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static final class ResourceWeakReference extends WeakReference<Resource> {

        final Key key;

        public ResourceWeakReference(Key key, Resource referent,
                                     ReferenceQueue<? super Resource> queue) {
            super(referent, queue);
            this.key = key;
        }
    }

}

使用引用队列,当弱引用被回收了,通知将其从Map中移除。

弱引用示例

  //强引用
        String a = new String("1");
        //弱引用
        final ReferenceQueue<String> queue = new ReferenceQueue<>();
        new Thread() {
            @Override
            public void run() {
                try {
                    Reference<? extends String> remove = queue.remove();
                    System.out.println("回收掉: " + remove);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
        WeakReference<String> weakReference = new WeakReference<>(a, queue);
        System.out.println("若引用1: " + weakReference.get());
        a = null;
        System.gc();
        System.out.println("若引用2: " + weakReference.get());

输出:

若引用1: 1
若引用2: null
回收掉: java.lang.ref.WeakReference@48be9674

LruMemoryCache

public class LruMemoryCache extends LruCache<Key, Resource> implements MemoryCache {
    private ResourceRemoveListener listener;
    private boolean isRemoved;
    public LruMemoryCache(int maxSize) {
        super(maxSize);
    }
    @Override
    protected int sizeOf(Key key, Resource value) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            //当在4.4以上手机复用的时候 需要通过此函数获得占用内存
            return value.getBitmap().getAllocationByteCount();
        }
        return value.getBitmap().getByteCount();
    }
    @Override
    protected void entryRemoved(boolean evicted, Key key, Resource oldValue, Resource newValue) {
        //给复用池使用
        if (null != listener && null != oldValue && !isRemoved) {
            listener.onResourceRemoved(oldValue);
        }
    }
    @Override
    public Resource remove2(Key key) {
        // 如果是主动移除的不会掉 listener.onResourceRemoved
        isRemoved = true;
        Resource remove = remove(key);
        isRemoved = false;
        return remove;
    }
    /**
     * 资源移除监听
     */
    @Override
    public void setResourceRemoveListener(ResourceRemoveListener listener) {
        this.listener = listener;
    }
}
public interface MemoryCache {
    interface ResourceRemoveListener {
        void onResourceRemoved(Resource resource);
    }
    Resource put(Key key, Resource resource);
    void setResourceRemoveListener(ResourceRemoveListener listener);
    Resource remove2(Key key);
}

LruMemoryCache继承了LruCache,并实现了MemoryCache接口。

LruMemoryCache构造函数中调用了父类LruCache的带参构造函数,传入了缓存中的最大条目数。

LruMemoryCache覆写了LruCache的sizeOf和entryRemoved方法,sizeOf方法获得占用内存,entryRemoved方法在资源被内存缓存移除时调用,此时我们在MemoryCache.ResourceRemoveListener的回调方法onResourceRemoved中,将资源的Bitmap添加到复用池BitmapPool中。

LruBitmapPool

public class LruBitmapPool extends LruCache<Integer, Bitmap> implements BitmapPool {
    private boolean isRemoved;
    // 负责筛选
    NavigableMap<Integer, Integer> map = new TreeMap<>();
    private final static int MAX_OVER_SIZE_MULTIPLE = 2;

    public LruBitmapPool(int maxSize) {
        super(maxSize);
    }

    /**
     * 将Bitmap放入复用池
     */
    @Override
    public void put(Bitmap bitmap) {
        //isMutable 必须是true
        if (!bitmap.isMutable()) {
            bitmap.recycle();
            return;
        }
        int size = bitmap.getAllocationByteCount();
        if (size >= maxSize()) {
            bitmap.recycle();
            return;
        }
        put(size, bitmap);
        map.put(size, 0);
    }

    /**
     * 获得一个可复用的Bitmap
     */
    @Override
    public Bitmap get(int width, int height, Bitmap.Config config) {
        //新Bitmap需要的内存大小  (只关心 argb8888和RGB65)
        int size = width * height * (config == Bitmap.Config.ARGB_8888 ? 4 : 2);
        //获得等于 size或者大于size的key
        Integer key = map.ceilingKey(size);
        //从key集合从找到一个>=size并且 <= size*MAX_OVER_SIZE_MULTIPLE
        if (null != key && key <= size * MAX_OVER_SIZE_MULTIPLE) {
            isRemoved = true;
            Bitmap remove = remove(key);
            isRemoved = false;
            return remove;
        }
        return null;
    }

    @Override
    protected int sizeOf(Integer key, Bitmap value) {
        return value.getAllocationByteCount();
    }

    @Override
    protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
        map.remove(key);
        if (!isRemoved)
            oldValue.recycle();
    }

}

Bitmap复用池同样使用了LruCache

public interface BitmapPool {
    void put(Bitmap bitmap);
    /**
     * 获得一个可复用的Bitmap
     *  三个参数计算出 内存大小
     */
    Bitmap get(int width,int height,Bitmap.Config config);
}

你可能感兴趣的:(#,Android,Glide架构设计)