Glide 核心设计二: 缓存管理

6,234 阅读13分钟

原文链接:Glide核心设计二:缓存管理

引言

Glide作为一个优秀的图片加载框架,缓存管理是必不可少的一部分,这篇文章主要通过各个角度、从整体设计到代码实现,深入的分析Glide的缓存管理模块,力求在同类分析Glide缓存的分析文章中脱颖而出。关于Glide的生命周期绑定,可查看Glide系列文章Glide核心设计一:皮皮虾,我们走

前提

  1. 本文分析Glide缓存管理,将以使用Glide加载网络图片为例子,如加载本地图片、Gif资源等使用不是本文的重点。因不管是何种使用方式,缓存模块都是一样的,只抓住网络加载图片这条主线,逻辑会更清晰。
  2. 本文将先给出Glide缓存管理整体设计的结论,然后再分析源码。

整体设计

缓存类型

Glide的缓存类型分为两大类,一类是Resource缓存,一类是Bitmap缓存。

Resource缓存

为什么需要缓存图片Resource,很好理解,因为图片从网络加载,将图片缓存到本地,当需要再次使用时,直接从缓存中取出而无需再次请求网络。

三层缓存

Glide在缓存Resource使用三层缓存,包括:

  1. 一级缓存:缓存被回收的资源,使用LRU算法(Least Frequently Used,最近最少使用算法)。当需要再次使用到被回收的资源,直接从内存返回。
  2. 二级缓存:使用弱引用缓存正在使用的资源。当系统执行gc操作时,会回收没有强引用的资源。使用弱引用缓存资源,既可以缓存正在使用的强引用资源,也不阻碍系统需要回收无引用资源。
  3. 三级缓存:磁盘缓存。网络图片下载成功后将以文件的形式缓存到磁盘中。

Bitmap缓存

Bitmap所占内存大小

Bitmap所占的内存大小由三部分组成:图片的宽度分辨率、高度分辨率和Bitmap质量参数。公式是:Bitmap内存大小 = (宽pix长pix)质量参数所占的位数。单位是字节B。

Bitmap压缩质量参数

质量参数决定每一个像素点用多少位(bit)来显示:

  1. ALPHA_8就是Alpha由8位组成(1B)
  2. ARGB_4444就是由4个4位组成即16位(2B)
  3. ARGB_8888就是由4个8位组成即32位(4B)
  4. RGB_565就是R为5位,G为6位,B为5位共16位(2B)

Glide默认使用RGB_565,比系统默认使用的ARGB_8888节省一半的资源,但RGB_565无法显示透明度。
举个例子:在手机上显示100pix*200pix的图片,解压前15KB,是使用Glide加载(默认RGB_565)Bitmap所占用的内存是:(100x200)x2B = 40000B≈40Kb,比以文件的形成存储的增加不少,因为png、jpg等格式的图片经过压缩。正因为Bitmap比较消耗内存,例如使用Recyclerview等滑动控件显示大量图片时,将大量的创建和回收Bitmap,导致内存波动影响性能。

Bitmap缓存算法

在Glide中,使用BitmapPool来缓存Bitmap,使用的也是LRU算法。当需要使用Bitmap时,从Bitmap的池子中取出合适的Bitmap,若取不到合适的,则再新创建。当Bitmap使用完后,不直接调用Bitmap.recycler()回收,而是放入Bitmap的池子。

缓存的Key类型

Glide的缓存使用的形式缓存,Resource和Bitmap都是作为Value的部分,将value存储时,必须要有一个Key标识缓存的内容,根据该Key可查找、移除对应的缓存。

缓存的key对比

  1. 从对比中可看出,Resource三层缓存所使用的key的构造形式是一样的,包括图片id(图片的Url地址),宽高等参数来标识。对于其他参数,举一个例子理解:图片资源从网络加载后,经过解码(decode)、缓存到磁盘、从磁盘中取出、变换资源(加圆角等,transformation)、磁盘缓存变换后的图片资源、转码(transcode)显示。
  2. Bitmap的缓存Key的构造相对简单得多,由长、宽的分辨率以及图片压缩参数即可唯一标示一个回收的Bitmap。当需要使用的bitmap时,在BitmapPool中查找对应的长、宽和config都一样的Bitmap并返回,而无需重新创建。

Resource缓存流程

Resource包括三层缓存,通过流程图看它们之间的关系:

Resource加载流程

因为内存缓存优于磁盘缓存,所以当需要使用资源时,先从内存缓存中查找(一级缓存和二级缓存都是内存缓存,其功能不一样,一级缓存用于在内存中缓存不是正在使用的资源,二级缓存是保存正在使用的资源),再从磁盘缓存中查找。若都找不到,则从网络加载。

滑动控件多图的性能优化

不论是Resource还是Bitmap缓存,若显示的仅是部分照片,并且不存在频繁使用的场景,则使用Glide没有太大的优势。设计缓存的目的就是为了在重复显示时,更快、更省的显示图片资源。Glide有针对ListView、Recyclerview等控件加载多图时进行优化。此处讨论最常见的场景:Recyclerview显示多图,简略图如下。

Glide在Recyclerview的使用

如上图所示,当图5划入界面时,会复用图一的Item,设置新的图片之前,会先清空原有图片的资源,清空时会把Resource资源放入一级缓存待将来复用,同时会将回收的Bitmap放入BitmapPool中;当图5向下隐藏,图一出现时,图5的资源会放到一级缓存中,图一的资源则从一级缓存中取出,无须重新网络请求,同时所需要的Bitmap也无须重新创建,直接复用。

LRU算法

BitmapPool的LRU算法流程图如下:

BitmapPool LRU流程

类图

在进行代码分析前,先给出跟Glide缓存管理相关的类图(省略类的大部分变量和方法)。

Glide缓存管理类图

Glide缓存管理类图大图地址

代码实现

根据以上的Glide缓存管理的结论及类图,可自主跟源码,跳过以下内容。

Glide.with(Context).load(String).into(ImageView)

Glide.with(Context)

返回RequestManager,主要实现和Fragment、Activity生命周期的绑定,详情请看Glide核心设计一:皮皮虾,我们走

.load(String)

RequestManager的load(String)方法返回DrawableTypeRequest,根据图片地址返回一个用于创建图片请求的Request的Builder,代码如下:

    public DrawableTypeRequest<String> load(String string) {
            return (DrawableTypeRequest<String>) fromString().load(string); //调用fromString()和load()方法
        }

fromString()方法调用loadGeneric()方法,代码如下:

 public DrawableTypeRequest<String> fromString() {
        return loadGeneric(String.class); 
    }

  private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
         ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
         ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
                 Glide.buildFileDescriptorModelLoader(modelClass, context);
         if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
             throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
                     + " which there is a registered ModelLoader, if you are using a custom model, you must first call"
                     + " Glide#register with a ModelLoaderFactory for your custom model class");
         }

         return optionsApplier.apply(  //传递的参数中创建了一个DrawableTypeRequest并返回该对象
                 new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                         glide, requestTracker, lifecycle, optionsApplier));  
     }

DrawableTypeRequest的load()方法如下:

 @Override
    public DrawableRequestBuilder<ModelType> load(ModelType model) {
        super.load(model);
        return this;
    }

DrawableTypeRequest父类是DrawableRequestBuilder,父类的父类是GenericRequestBuilder,调用super.load()方法如下:

 public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> load(ModelType model) {
        this.model = model;
        isModelSet = true;
        return this;
    }

以上代码可知,缓存管理的主要实现代码并不在.load(Sting)代码,接下来继续分析.into(ImageView)代码。

.into(ImageView)

GenericRequestBuilder的into(ImageView)代码如下:

public Target<TranscodeType> into(ImageView view) {
        Util.assertMainThread();
        if (view == null) { 
            throw new IllegalArgumentException("You must pass in a non null View");
        }

        if (!isTransformationSet && view.getScaleType() != null) {
            switch (view.getScaleType()) {  //根据图片的scaleType做相应处理
                case CENTER_CROP:
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }
        //调用buildImageViewTarget()方法创建了一个Target类型的对象
        return into(glide.buildImageViewTarget(view, transcodeClass));  
    }

以上代码主要有两个功能:

  1. 根据ScaleType进行图片的变换
  2. 将ImageView转换成一个Target

继续查看into(Target)的代码:

 public <Y extends Target<TranscodeType>> Y into(Y target) {
        Util.assertMainThread();
        if (target == null) {
            throw new IllegalArgumentException("You must pass in a non null Target");
        }
        if (!isModelSet) {
            throw new IllegalArgumentException("You must first set a model (try #load())");
        }

        Request previous = target.getRequest();  //获取请求体Request

        if (previous != null) { //若ImageView是复用过的,则previous不为空
            previous.clear(); //调用clear()方法清空ImageView上的图片资源,此方法会将回收的Resource放入内存缓存中,并不在内存中清空该资源。
            requestTracker.removeRequest(previous); //移除老的请求
            previous.recycle(); //回收Request使用
        }

        Request request = buildRequest(target); //获取新的Request
        target.setRequest(request); //将新的request设置到target中
        lifecycle.addListener(target); //添加生命周期的监听
        requestTracker.runRequest(request); //启动Request

        return target;
    }

以上代码,主要将图片加载的Request绑定到Target中,若原有Target具有旧的Request,得先处理旧的Request,再绑定上新的Request。target.setRequest()和target.getRequest()最终会调用ViewTarget的setRequest()方法和getRequest()方法,代码如下:

public void setRequest(Request request) {
        setTag(request);
    }
    private void setTag(Object tag) {
                 if (tagId == null) {
                     isTagUsedAtLeastOnce = true;
                     view.setTag(tag);//调用view的setTag方法,将Request和view做绑定
                 } else {
                     view.setTag(tagId, tag);//调用view的setTag方法,将Request和view做绑定
                 }
    }
    public Request getRequest() {
                    Object tag = getTag(); //获取view 的tag
                    Request request = null;
                    if (tag != null) {
                        if (tag instanceof Request) {  //若该tag是Request的一个实例
                            request = (Request) tag; 
                        } else {  //用户不能给view设置tag,因为该view的tag要用于保存Glide的Request对象,否则抛出异常
                            throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
                        }
                    }
                    return request;
            }

以上代码可知,Request通过setTag的方式和View进行绑定,当View是复用时,则Request不为空,通过Request可对原来的资源进行缓存与回收。此处通过View的setTag()方法绑定Request,可谓妙用。

以上代码创建了一个Request,requestTracker.runRequest(request);启动了Request,调用Request的begin()方法,该Request实例是GenericRequest,begin()代码如下:

@Override
    public void begin() {
        startTime = LogTime.getLogTime();
        if (model == null) {
            onException(null);
            return;
        }

        status = Status.WAITING_FOR_SIZE; //设置等待图片size的宽高状态
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) { //必须要确定图片的宽高,确定了则调用onSizeReady
            onSizeReady(overrideWidth, overrideHeight);
        } else { //设置回调,监听界面的绘制,当检测到宽高有效时,回调onSizeReady方法
            target.getSize(this);
        }

        if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
            target.onLoadStarted(getPlaceholderDrawable());
        }
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
    }

加载图片前,必须要确定图片的宽高,因为需要根据确定的宽高来获取资源。onSizeReady代码如下:

@Override
    public void onSizeReady(int width, int height) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
        if (status != Status.WAITING_FOR_SIZE) {//宽高没准备好,返回
            return;
        }
        status = Status.RUNNING;  //状态改为加载运行中

        width = Math.round(sizeMultiplier * width);
        height = Math.round(sizeMultiplier * height);

        ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
        final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);

        if (dataFetcher == null) {
            onException(new Exception("Failed to load model: \'" + model + "\'"));
            return;
        }
        ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
        }
        loadedFromMemoryCache = true;
        //真正的加载任务交给engine
        loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
                priority, isMemoryCacheable, diskCacheStrategy, this);
        loadedFromMemoryCache = resource != null;
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
    }

以上代码可知,在确定宽高后,将图片加载的任务交给类型为Engine的对象engine,并调用其load方法,代码如下:

 public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId(); //该id为图片的网络地址
        //缓存key的组成部分,使用工厂模式
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());
        //使用一级缓存,从回收的内存缓存中查找EngineResource
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) { //命中则直接返回
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }
        //从二级缓存中查找
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {//命中则直接返回
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }

        EngineJob current = jobs.get(key);
        if (current != null) {//该任务已经在执行,只需要添加回调接口,在任务执行完后调用接口告知即可
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }
        //一级缓存和二级缓存都不命中的情况下,启动新的任务
        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);//创建EngineJob
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority); //创建DecodeJob
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority); 
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable); //启动EngineRunnable runnable,使用线程池FifoPriorityThreadPoolExecutor管理

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

分析至此,我们终于看到实现一级缓存和二级缓存的相关代码,可以猜测三级缓存的实现跟EngineRunnable有关。engineJob.start(runnable)会启动EngineRunnable的start()方法。代码如下:

 @Override
    public void run() {
        if (isCancelled) {
            return;
        }

        Exception exception = null;
        Resource<?> resource = null;
        try {
            resource = decode();  //调用decode()方法
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Exception decoding", e);
            }
            exception = e;
        }

        if (isCancelled) { //请求被取消
            if (resource != null) {
                resource.recycle();
            }
            return;
        }

        if (resource == null) { //加载失败
            onLoadFailed(exception);
        } else { //加载成功
            onLoadComplete(resource);
        }
    }

查看decode()方法如下:

private Resource<?> decode() throws Exception {
        if (isDecodingFromCache()) {
            return decodeFromCache();  //从磁盘缓存中获取
        } else {
            return decodeFromSource(); //从网络中获取资源
        }
    }

至此,我们看到磁盘缓存和网络请求获取图片资源的代码。查看onLoadFailed()的代码逻辑可知,默认先从磁盘获取,失败则从网络获取。

BitmapPool缓存逻辑

以上就是Resource三层缓存的代码,接下来看BitmapPool的缓存实现代码。
在decodeFromSource()的代码中,会返回一个类型为BitmapResource的对象。在RecyclerView的例子中,当ImageView被复用时,会在Tag中取出Request,调用request.clear()代码。该方法最终会调用BitmapResource的recycler()方法,代码如下:

 public void recycle() {
        if (!bitmapPool.put(bitmap)) {
            bitmap.recycle();
        }
    }

该代码调用bitmapPool.put(bitmap),bitmapPool的实例是LruBitmapPool代码如下:

  public synchronized boolean put(Bitmap bitmap) {
         if (bitmap == null) {
             throw new NullPointerException("Bitmap must not be null");
         }
         if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || !allowedConfigs.contains(bitmap.getConfig())) {
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "Reject bitmap from pool"
                         + ", bitmap: " + strategy.logBitmap(bitmap)
                         + ", is mutable: " + bitmap.isMutable()
                         + ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig()));
             }
             return false;
         }

         final int size = strategy.getSize(bitmap);
         strategy.put(bitmap);//该strategy的实例是Lru算法
         tracker.add(bitmap); //log跟踪

         puts++; //缓存的bitmap数量标记加一
         currentSize += size;//缓存bitmap的总大小

         if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
         }
         dump(); //仅用于Log

         evict();  //判断是否超出指定的内存大小,若超出则移除
         return true;
     }

可以看出,正常情况下调用put方法返回true,证明缓存该Bitmap成功,缓存成功则不调用bitmap.recycler()方法。当需要使用Bitmap时,先从Bitmap中查找是否有符合条件的Bitmap。在RecyclerView中使用Glide的例子中,将大量复用宽高及Bitmap.Config都相等的Bitmap,极大的优化系统内存性能,减少频繁的创建回收Bitmap。

小结

Glide的缓存管理至此就分析完了,主要抓住Resource和Bitmap的缓存来讲解。在代码的阅读中还发现了工厂、装饰者等设计模式。Glide的解耦给开发者提供很大的便利性,可根据自身需求设置缓存参数,例如默认Bitmap.Config、BitmapPool缓存大小等。最后,针对Glide的缓存设计,提出几点小建议:

  1. Glide虽然默认使用的Bitmap.Config是RGB_565,但在进行transform(例如圆角显示图片)时往往默认是ARGB_8888,因为RGB_565没有透明色,此时可重写圆角变换的代码,继续使用RGB_565,同时给canvas设置背景色。
  2. BitmapPool缓存的Bitmap大小跟Bitmap的分辨率也有关系,在加载图片的过程中,可调用.override(width, height)指定图片的宽高,再调整ImageView控件的大小适应布局。
  3. Resource的一级缓存和Bitmap都是内存缓存,虽然极大的提升了复用,但也会导致部分内存在系统执行GC时无法释放。若内存达到手机性能瓶颈,应在合适的时机调用Glide.get(this).clearMemory()释放内存。