Glide框架解析---缓存机制介绍及活动缓存 内存缓存 复用池相关类(一)

Glide是一个非常成熟的图片加载框架了,一开始从事Android开发就一直使用这个框架,使用的很熟,但是原理和源代码上就没怎么研究了,最近抽出时间来,好好的学习了Glide的源码和原理,我把自己的学习成果分享给大家。因为Glide的原理相对比较复杂,一篇博客很难把Glide说清楚,所以之后我会按照顺序分享6-8篇博客,手写一个阉割版的Glide。

今天我先简单介绍一下Glide的缓存机制及写一下相关的类,搭建一下基础机构

一、缓存机制

下面的图片,简单的描述了所包含的缓存及关系

Glide中一共包含4个缓存,其中的查找关系,如图所示:从活动缓存内存缓存资源缓存原始缓存中依次查找,有则返回。下面我一次介绍一下这四个缓存的作用,以便后续对Glide的理解更加容易。

1.活动缓存(Active Resources)

如果图片正在被使用,则这个图片会被Glide加入到这个活动缓存中。什么是正在被使用呢?比如进入某Activity展示一张图片,展示的时候会把这个图片加入到活动缓存中,退出Activity时,会从活动缓存中移除,移除的图片资源不会立即销毁,而是加入到内存缓存

如果在加载图片的时候,发现活动缓存中没有,会在内存缓存进行查找。资源存在的话,Glide会把对应的资源从内存缓存中移除,加入到活动缓存中去。这样可以避免因为达到内存缓存最大值或者系统内存压力导致的内存缓存清理,从而释放掉活动资源中的图片(recycle)。这句话是什么意思?如果一张图片资源同时被活动缓存内存缓存所引用的话,当内存缓存的数量达到所设定的最大值后,会根据LRU算法(在内存缓存中会介绍)移除图片资源,进行内存释放,如果所释放的图片资源也存在于活动资源的话,因为此图片资源正在使用,被释放后就会引起空指针异常的错误

2.内存缓存(Memory Cache)

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

LRU在Android support-v4中提供了LruCache工具类。我们在实现内存缓存的时候,直接继承这个类即可。我们进入LruCache这个类,查看一下源码

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    private int size;
    private int maxSize;
...

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
...

在LruCache的构造方法中,需要指定maxSize,这个是内存缓存的大小。

我们可以看见LruCache中封装了LinkedHashMap,并且在创建LinkedHashMap对象的时候,在构造器的第三个参数设置了true,表示会根据最近最少使用的算法进行排序。LinkedHashMap中的源码我这里就不过多介绍了。

3.资源缓存

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

4.原始缓存

原始缓存就是图像原始数据,使用的也是LRU算法。资源缓存原始缓存都属于磁盘缓存的范畴

二、构建基础代码

1.Key

首先我们创建一个Key接口,这个接口的作用:每级缓存都是以键值对的形式进行资源存储的,而存储的Key就是这里的Key接口类实现类,Key相当于对资源路径的封装,之后我们还会在这个Key接口中添加其他的方法。

public interface Key {
    
}

2.Resource

我们再创建一个Resource类,这个类中封装了Bitmap。具体细节我们先看代码,在做解释

public class Resource {
    private Bitmap bitmap;
    private int acquired;
    private Key key;
    private ResourceListener resourceListener;

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

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

    public Bitmap getBitmap() {
        return bitmap;
    }

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

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

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

    public interface ResourceListener {
        void onResourceReleased(Key key, Resource resource);
    }
}

一开始看到Resource这个类的时候可能会有些迷惑,不知道这个类的作用是什么?我们逐渐看一下其中的属性:
bitmap:我们可以看到,Resource类封装了Bitmap,其实可以把Resource当做Bitmap,Bitmap是Resource这个类核心属性

acquired:这个属性主要是做引用计数,哪里应用了其中的Bitmap,我们就将acquired这个值+1(调用acquired()这个方法);不再使用的时候我们就将acquired-1(调用release()这个方法)

ResourceListener:这是一个回调接口,当调用release方法的时候,会判断acquired是否为0,如果为0,监听就会被触发,回调给上层实现的方法做处理。

我们从上述可以知道,Resource主要就是封装了资源,其中添加了其他的功能便于Bitmap的操作

3.ActiveResource

我们先简单捋一下这个活动缓存:

(1)被使用的图片资源会被加入到活动缓存,所以呢,存储在活动缓存中的资源生命周期相对较短,所以我们使用弱引用来存储活动资源,有效的避免了内存泄漏的问题

(2)创建一个回调来和存储的Resource产生关联

我们来看一下代码:

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;
    }
    /**
     * 加入活动缓存
     *
     * @param key
     * @param resource
     */
    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
     * @param key
     * @return
     */
    public Resource get(Key key) {
        ResourceWeakReference reference = activeResources.get(key);
        if (reference != null) {
            return reference.get();
        }
        return null;
    }
    /**
     * 引用队列,通知我们弱引用被回收了
     * 让我们得到通知的作用
     *
     * @return
     */
    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;
        }
    }
}

如果比较熟悉弱引用的同学,对上述的代码应该不会太陌生。开启一个线程,来监听资源的使用情况,从而在ActiveResource中移除资源

4.LruMemoryCache

我们先写一个接口类MemoryCache,之后LruMemoryCache会实现这个类(这个类目前是不完整的,之后我们会逐渐丰满)

public interface MemoryCache {
    Resource remove2(Key key);
}

为什么这里叫做remove2呢?因为LruMemoryCache会继承LruCache这个类,而这个类中也存在remove的方法,并且是被final所修饰,不能被覆写的,所以我们只能在我们的接口方法中做一个区分。(LruCache这个类是系统自带的,而Glide中的LruCache是自己写的,其实内容大同小异,这里我为了方便直接使用了系统自带的)

我们看一下LruMemoryCache这个类(在Glide中内存缓存的类叫做LruResourceCache,为了于见名知意,所以改为了LruMemoryCache)

public class LruMemoryCache extends LruCache<Key, Resource> implements MemoryCache {

    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) {
    }


    @Override
    public Resource remove2(Key key) {
    }
}

基本上是这样的,我们后续会逐渐丰满。这里有两个地方需要注意:
1.maxSize:我们必须在构造方法中指定这个值,这是LruCache这个类所要求的。我们需要注意的是:这里的maxSize并不是存储在缓存中的数量,我一开始的时候就理解错了,maxSize指的是图片资源所占用内存的总大小。

2.sizeof方法:这个方法的返回值是当前Bitmap所需要的大小,那为什么这里需要做版本的区分呢?getAllocationByteCount()和getByteCount()这两个方法有什么区别呢?

如果我们使用一个Bitmap资源,它的大小为100*100。之后呢Bitmap所占用的内存被复用,新Bitmap的宽高为50*50,如果我们调用getByteCount()方法,那么返回的大小为100*100,和需要的50*50相差过大,造成了资源的浪费;如果我们调用的是getAllocationByteCount(),那么返回的就是准确的50*50的大小。这就是这两个方法的区别,当然getAllocationByteCount方法是在4.4之后才加入的。

5.LruBitmapPool

BitmapPool是Glide中的Bitmap复用池,同样适用LRU来进行管理。
当一个Bitmap从内存缓存 被动 的被移除(内存紧张、达到maxSize)的时候并不会被recycle,而是加入这个BitmapPool中,只有从这个BitmapPool 被动被移除的时候,Bitmap的内存才会真正被recycle释放。当然主动从内存缓存中移除的时候,不会加入到这个BitmapPool中。所以呢,上述的几个类,在这里需要进行功能的补充

首先是MemoryCache,我们在这个接口中添加一个回调函数及方法,这个回调函数的作用是,当有资源被动的从内存缓存中移除时,会触发这个监听,从而将这个资源加入到BitmapPool中

    void setResourceRemoveListener(ResourceRemoveListener listener);

    interface ResourceRemoveListener{
        void onResourceRemoved(Resource resource);
    }

其次是LruMemoryCache这个类的修改(我仅贴出修改的部分):

public class LruMemoryCache extends LruCache<Key, Resource> implements MemoryCache {

    private ResourceRemoveListener listener;
    private boolean isRemoved;
...

    @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;
    }

    /**
     * 资源移除监听
     *
     * @param listener
     */
    @Override
    public void setResourceRemoveListener(ResourceRemoveListener listener) {
        this.listener = listener;
    }
}

要记住主动移除的Bitmap资源我们不用加入到BitmapPool中,因为可能是从内存缓存加入到了活动缓存,资源依然被使用着,remove2这个方法是主动调用移除的方法。一旦调用LruCache中的remove方法,必定会触发entryRemoved这个方法。所以这里我们使用isRemoved变量来做控制。

接下来我们来写BitmapPool这个抽象类:

public interface BitmapPool {

    void put(Bitmap bitmap);

    /**
     * 获得一个可复用的Bitmap
     *  三个参数计算出 内存大小
     * @param width
     * @param height
     * @param config
     * @return
     */
    Bitmap get(int width,int height,Bitmap.Config config);
}

我们再写一下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放入复用池
     *
     * @param 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和RGB565)
        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();
    }
}

1.put方法

这个方法中需要注意的是bitmap.isMutable这个方法,它的返回值一定是true,表示可以被复用,否则的话直接被回收。

2.get方法

代码的注释已经写明了,主要是根据宽高和Bitmap.Config来计算符合条件的Bitmap。通过TreeMap的celling方法我们找到等于或者大于size的值,这也是为什么我们选择TreeMap来存储Bitamp的大小。

3.isRemoved和entryRemoved

这个属性和方法和LruMemoryCache中作用是一样的。主动移除,我们不释放资源,因为可能是这个资源被再次使用,加入到了别的缓存中;被动移除,表示内存不错或者到达了maxSize值,这时候我们必须释放资源

 

今天我们就先讲到这里,代码其实很简单,但是没有整体的了解过Glide的架构,还是不太清楚要干嘛。这里我建议可以先按照博客的代码一步一步来,之后我们会逐步的丰满我们的代码,思路也会变得清晰了。代码

如果有问题或者有错误,欢迎大家留言讨论!

 

 

 

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值