〔两行哥〕提纲挈领,带你梳理Glide主要源码逻辑(二)

上篇文章主要分析Glide的处理流程以及with()方法的内部逻辑(参阅:〔两行哥〕提纲挈领,带你梳理Glide主要源码逻辑(一)),本篇主要分析load()方法,同时为大家介绍Bitmap优化和LruCache算法相关理论,为最后一个重头戏into()方法做铺垫。
Glide.with()方法返回值类型为RequestManger,那么我们继续分析load()方法也主要集中在RequestManger类中。

load()方法主要是对Glide内部的Model进行封装与处理(什么是Model?请参阅上篇文章),最终形成图片加载请求。

一、load()源码逻辑

在RequestManger类中提供了多种load()方法的重载,包括load(String string)、load(Uri uri)、load(Integer resId)、load(File file)等,分别适用于加载网络图片地址,加载图片Uri,加载图片resId,加载图片文件等,我们以加载网络图片地址为例进行分析:

RequestManger.java
    ......省略
    public DrawableTypeRequest<String> load(String string) {
        return (DrawableTypeRequest<String>) fromString().load(string);
    }
    ......省略
    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("......");
        }
        return optionsApplier.apply(new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,glide, requestTracker, lifecycle, optionsApplier));
    }
    ......省略

先看load()方法的返回值类型:DrawableTypeRequest<ModelType>,可以理解为Glide中加载图片的请求对象。DrawableTypeRequest<ModelType>继承了DrawableRequestBuilder<ModelType>,而 DrawableRequestBuilder<ModelType>又继承了GenericRequestBuilder<ModelType, ImageVideoWrapper, GifBitmapWrapper, GlideDrawable>。根据类名判断,GenericRequestBuilder类使用了构建者模式,追踪一下源码:

GenericRequestBuilder.java
    protected final Class<ModelType> modelClass;
    protected final Context context;
    protected final Glide glide;
    protected final Class<TranscodeType> transcodeClass;
    protected final RequestTracker requestTracker;//请求追踪器
    protected final Lifecycle lifecycle;
    private ChildLoadProvider<ModelType, DataType, ResourceType, TranscodeType> loadProvider;
    private ModelType model;
    private Key signature = EmptySignature.obtain();
    // model may occasionally be null, so to enforce that load() was called, set a boolean rather than relying on model not to be null.
    private boolean isModelSet;
    private int placeholderId;//占位图ResId
    private int errorId;//加载失败图ResId
    private RequestListener<? super ModelType, TranscodeType> requestListener;//请求监听
    private Float thumbSizeMultiplier;
    private GenericRequestBuilder<?, ?, ?, TranscodeType> thumbnailRequestBuilder;
    private Float sizeMultiplier = 1f;//尺寸缩放比例
    private Drawable placeholderDrawable;//占位图Drawable
    private Drawable errorPlaceholder;//加载失败图Drawable
    private Priority priority = null;
    private boolean isCacheable = true;
    private GlideAnimationFactory<TranscodeType> animationFactory = NoAnimation.getFactory();
    private int overrideHeight = -1;//覆写高度
    private int overrideWidth = -1;//覆写宽度
    private DiskCacheStrategy diskCacheStrategy = DiskCacheStrategy.RESULT;//磁盘缓存策略
    private Transformation<ResourceType> transformation = UnitTransformation.get();
    private boolean isTransformationSet;
    private boolean isThumbnailBuilt;
    private Drawable fallbackDrawable;
    private int fallbackResource;

上述源码截取了GenericRequestBuilder类所有的成员变量。可以发现Glide通过构建者模式配置的所有参数都在这里(什么是构建者模式?请读者自行查阅学习),load()方法最终返回了加载图片的请求对象(DrawableTypeRequest<ModelType>实例)。
接着看load()方法体内的fromString()方法。fromString()方法内部调用了loadGeneric()方法。在loadGeneric()方法内部,我们看到了熟悉的名字:ModelLoader。在上一篇中,我们已经介绍过,将Model转化为Data的角色就是ModelLoader。这里一共创建了两个ModelLoader,一个是输入流ModelLoader,一个是文档描述符ModelLoader。方法最终返回了optionsApplier.apply(new DrawableTypeRequest<T>(...))。追踪看看optionsApplier.apply()做了哪些操作,还是在RequestManger类中:

RequestManger.java
    private final OptionsApplier optionsApplier;//用户自定义的Glide配置套用者
    private DefaultOptions options;//用户自定义的Glide配置
    ......省略
    public interface DefaultOptions {
        /**
         * Allows the implementor to apply some options to the given request.
         *
         * @param requestBuilder The request builder being used to construct the load.
         * @param <T> The type of the model.
         */
        <T> void apply(GenericRequestBuilder<T, ?, ?, ?> requestBuilder);
    }
    ......省略
    class OptionsApplier {

        public <A, X extends GenericRequestBuilder<A, ?, ?, ?>> X apply(X builder) {
            if (options != null) {
                options.apply(builder);
            }
            return builder;
        }
    }
    ......省略

OptionsApplier(optionsApplier)为RequestManger的内部类,只有一个apply(X builder)方法。在apply(X builder)中,对options进行了非空判断,如果不为空,就调用options的apply()方法。如源码中的注释说明,options为RequestManger类的成员变量(用户自定义的Glide配置),如果用户没有传入options,则默认值为null。apply(X builder)方法最终将参数builder进行了返回,结合上文来看,builder即 DrawableTypeRequest<T>的实例。
综上,loadGeneric()方法最终创建了一个DrawableTypeRequest<T>对象并进行了返回,在创建DrawableTypeRequest<T>对象的构造方法中,传入了之前所述的两个ModelLoader以及requestTracker(请求追踪器)等。

注:RequestTracker是Glide中一个核心类,将在下一篇into()方法中着重介绍。

二、Bitmap优化

(一)Bitmap的OOM

Bitmap占用内存大小 = 同时加载的Bitmap数量 * 每个Bitmap图片的宽度px * 每个Bitmap图片的高度px * 每个像素占用的内存。而每个像素占有多大的内存呢?这取决于此像素的类别及是否采用了压缩技术。
如果是非黑即白的二值图像,不压缩的情况下一个像素只需要1个bit。
如果是256种(2的8次方)状态的灰度图像,不压缩的情况下一个像素需要8bit(1Byte,256种状态)。
如果用256种(2的8次方)状态标识屏幕上某种颜色的灰度,而屏幕采用三基色红绿蓝(RGB),不压缩的情况下一个像素需要占用24bit(3Byte),这个就是常说的24位真彩色。
还有各种其他的存储方式,例如15bit、16bit、32bit等。如果考虑到压缩,有损压缩或无损压缩,具体采用的压缩算法及压缩参数设置都会影响一个像素占用的存储空间。
例如,如果在页面显示一张1920 * 1080的图片,采用Android内置的ARGB_8888压缩,占用的内存大约为8MB左右。
而每个Android应用的VM堆内存上限是通过dalvik.vm.heapgrowthlimit设置(参阅:Android Dalvik Heap 浅析),如果同一个页面展示的图片长宽过大或数量过多,占用的内存超过了此上限值,就会导致OOM。

注:1GB = 1024MB;1MB = 1024KB;1KB = 1024Byte;1Byte = 8bit。
(二)Bitmap的优化策略

1.选择不同的图片压缩策略,比如使用Bitmap.Config.RGB_565代替Bitmap.Config.ARGB_8888,同时对图片进行压缩等;

    /**
     * @param bitmap  源Bitmap
     * @param maxSize 目标Bitmap最大值(KB)
     * @return
     */
    private Bitmap zipBitmap(Bitmap bitmap, int maxSize) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(CompressFormat.JPEG, 100, baos);
        int options = 100;
        while ((baos.toByteArray().length / 1024 > maxSize)) {
            baos.reset();
            bitmap.compress(CompressFormat.JPEG, options, baos);
            options -= 5;
        }
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        return BitmapFactory.decodeStream(bais);
    }

2.将图片按比例压缩尺寸后再展示;

    /**
     * @param bitmap    源Bitmap
     * @param maxWidth  目标Bitmap最大宽
     * @param maxHeight 目标Bitmap最大高
     * @return
     */
    private Bitmap zipBitMap(Bitmap bitmap, int maxWidth, int maxHeight) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(CompressFormat.JPEG, 100, baos);
        Options options = new Options();
        options.inJustDecodeBounds = true;
        float width = options.outWidth * 1.0F;
        float height = options.outHeight * 1.0F;
        int size = 1;
        if (width > maxWidth || height > maxHeight) {
            int widthRatio = Math.round(width / maxWidth);
            int heightRatio = Math.round(height / maxHeight);
            size = widthRatio > heightRatio ? widthRatio : heightRatio;
        }
        options.inSampleSize = size;
        options.inJustDecodeBounds = false;
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        return BitmapFactory.decodeStream(bais);
    }

3.使用try...catch...抓取OOM异常。

三、LruCache算法(LeastRecentlyUsed,最近最少使用算法,参阅:LRU缓存淘汰算法

Glide拥有三级缓存,即每获取到一张图片,都会在内存和本地文件进行缓存,如果下次又用到了同样的图片:
1.内存有无需要的图片?有的话就用,没有就去本地文件找。
2.本地文件有无需要的图片?有的话就用,没有就去网络找。
一共有三个环节:内存 --> 本地文件 --> 网络。
说完三级缓存,接下来再引入一个概念:Lru缓存算法,如下图所示。


Lru缓存算法示意图
划重点:
1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候(即达到总缓存数量上限),将链表尾部的数据丢弃。

Glide的内存缓存策略基于LruCache算法,android.util包下就有关于Lru缓存算法的实现类LruCache。
让我们看看LruCache的源码实现,首先来看看LruCache的成员变量及构造方法。

LruCache.java
    private final LinkedHashMap<K, V> map;//LruCache内部基于LinkedHashMap实现
    private int size;//当前已缓存的数量
    private int maxSize;//可缓存的最大数量(总缓存容量)
    private int putCount;//加入的缓存数量
    private int createCount;//创造的缓存数量(如果取缓存时缓存不存在,则会优先创造缓存,下文分析)
    private int evictionCount;//淘汰的数量(如果超出缓存容量,则最少使用的缓存会被淘汰)
    private int hitCount;//命中数量(如果取用缓存,缓存依旧存在,没有没淘汰,则算作命中)
    private int missCount;//未命中数量(如果取用缓存,缓存已经被淘汰,则算未命中)

    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进行了赋值。其次,在创建LinkedHashMap<K,V>对象的时候,传入了3个参数:0,0.75F,true。这里稍微解释一下:
第一个参数为initialCapacity,即初始容量,定义了LinkedHashMap的初始大小。
第二个参数为loadFactor,即加载因子,默认值0.75F,意为如果LinkedHashMap中的元素数量达到了总容量的75%,就会扩容为原来的两倍。例如,定义一个HashMap,初始容量默认为16,加载因子0.75F,那么此HashMap的初始实际容量为12,当HashMap内元素数量达到12时,会自动扩容至2倍,即32。这块各位读者可以参阅HashMap源码,日后我也会写一些HashMap源码分析。
第三个参数为accessOrder,定义了LinkedHashMap<K,V>的排序模式。当为true时,LinkedHashMap<K,V>为access-order(访问顺序模式,也就是LruCache算法的模式),当为false时,LinkedHashMap<K,V>为insertion-order(插入顺序模式)。建议读者参阅LinkedHashMap<K,V>源码。
接下来看一下LruCache类中的四个核心方法:获取数据、缓存数据、调整总缓存大小及删除数据。

(一)获取数据
LruCache.java
    //获取数据
    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) {
                //命中数量+1,并返回mapValue
                hitCount++;
                return mapValue;
            }
            missCount++;//未命中数量+1
        }

        /*
         * 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(K key)进行分析。首先从LinkedHashMap中get(key),如果取出数据不为null,说明数据命中,命中数计数+1,同时返回取出的数据。如果取出数据为null,未命中数计数+1,get(K key)方法继续向下执行,调用了方法体内的create(key)方法。create(key)方法执行了什么操作呢?查看源码。

LruCache.java
    /**
     * Called after a cache miss to compute a value for the corresponding key.
     * Returns the computed value or null if no value can be computed. The
     * default implementation returns null.
     *
     * <p>The method is called without synchronization: other threads may
     * access the cache while this method is executing.
     *
     * <p>If a value for {@code key} exists in the cache when this method
     * returns, the created value will be released with {@link #entryRemoved}
     * and discarded. This can occur when multiple threads request the same key
     * at the same time (causing multiple values to be created), or when one
     * thread calls {@link #put} while another is creating a value for the same
     * key.
     */
    protected V create(K key) {
        return null;
    }

为便于理解,我把原注释也摘录了出来。create(K key)方法在获取数据失败(未命中缓存)的时候调用,用户可以覆写该方法,在未命中缓存的时候返回特定的数据,默认情况下返回了null。
对接下来的一个同步代码块进行分析,可能读者对这块代码非常疑惑,如下段对map数据重新覆盖的逻辑。

LruCache.java
        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);
            }
        }

首先将创造的缓存数量计数+1。

注:调用HashMap.put(key,value)方法具有返回值。如果原本HashMap<String,String>中key = “key”,对应的value = “preValue”,调用put(“key”,“newValue”)方法后,则put()方法会返回“preValue”,现HashMap中的该键值对为:key = “key”,对应的value = “newValue”。即如果key对应的value原本就有值,若调用put()方法放入新value,则put()方法会返回原本的旧value。

回到源码中,调用map.put(key, createdValue)方法,返回该key值的原本数据mapValue。如果原本的mapValue不为null,则再次调用put(key,mapValue)将原本数据mapValue放回去。这里会比较疑惑,为什么mapValue可能不为null?之前调用get(key)方法不是已经说明该key对应的mapValue为null了吗?为什么还要用mapValue覆盖掉createdValue?

这里体现了源码作者的严谨性。前文已经说过,在这块代码之前已经调用了 create(key)方法,默认返回了null。而实际情况中,用户可能覆写该方法,在未命中缓存的情况下,返回自定义的数据。而用户覆写的逻辑可能是耗时操作,同时此处的代码并不是线程安全的,因此在调用上述同步代码块的时候,map.put(key, createdValue)方法可能会返回曾经已经放进去的mapValue。那么接下来的操作就是将原本放进去的mapValue再次覆盖createdValue,即再次调用map.put(key, mapValue),销毁掉createdValue。这里请读者仔细体悟。

size += safeSizeOf(key, createdValue)的作用是重新计算此时已经占用的缓存数量。接下来if(mapValue != null)的分支中执行了entryRemoved(false, key, createdValue, mapValue)方法,这是要实现啥?看看源码:

LruCache.java
    /**
     * Called for entries that have been evicted or removed. This method is
     * invoked when a value is evicted to make space, removed by a call to
     * {@link #remove}, or replaced by a call to {@link #put}. The default
     * implementation does nothing.
     *
     * <p>The method is called without synchronization: other threads may
     * access the cache while this method is executing.
     *
     * @param evicted true if the entry is being removed to make space, false
     *     if the removal was caused by a {@link #put} or {@link #remove}.
     * @param newValue the new value for {@code key}, if it exists. If non-null,
     *     this removal was caused by a {@link #put}. Otherwise it was caused by
     *     an eviction or a {@link #remove}.
     */
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

看原注释,了解到这是一个空实现,如果有需要的话,用户可以覆写这个方法,这个方法会在缓存数据被淘汰或移除时调用。回到之前的代码,if(mapValue != null)的else分支执行了trimToSize(maxSize)来对超过最大缓存数量外的缓存数据进行了淘汰,下文再对trimToSize(maxSize)方法进行分析。

(二)缓存数据
LruCache.java
    //缓存数据   
    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;
    }

首先对加入的缓存计数putCount+1,并执行size += safeSizeOf(key, value)对已缓存数据容量重新计算。然后调用map.put(key, value)获取原本旧缓存previous。如果previous不为null,需要再次执行size -= safeSizeOf(key, previous)对已缓存数据容量重新计算。
entryRemoved(false, key, previous, value)方法前文已经分析过,跳过。
最终又调用了trimToSize(maxSize)对超过最大缓存数量外的缓存数据进行了淘汰。

(三)调整缓存大小
LruCache.java
    //调整总缓存大小
    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<K, V> 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);
        }
    }

这块逻辑比较简单,核心点是调用 map.eldest()获取最老的缓存键值对。从map中remove该键值对,重新计算已缓存数量,并对淘汰缓存数量计数evictionCount+1。

(四)删除数据
LruCache.java
    //删除数据
    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

        return previous;
    }

最后看一下删除数据的逻辑,比较简单,留给读者自行阅读。

这篇文章到此结束,下篇文章将对Glide的into()方法进行分析,这将是Glide中最复杂的方法。再会。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容