Glide 核心设计二: 缓存管理

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

引言

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

前提

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

总体设计

缓存类型

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

Resource缓存

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

三层缓存

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

  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可查找、移除对应的缓存。
app

缓存的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()释放内存。
相关文章
相关标签/搜索