上篇文章主要分析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 load(String string) {
return (DrawableTypeRequest) fromString().load(string);
}
......省略
public DrawableTypeRequest fromString() {
return loadGeneric(String.class);
}
......省略
private DrawableTypeRequest loadGeneric(Class modelClass) {
ModelLoader streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
ModelLoader fileDescriptorModelLoader = Glide.buildFileDescriptorModelLoader(modelClass, context);
if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
throw new IllegalArgumentException("......");
}
return optionsApplier.apply(new DrawableTypeRequest(modelClass, streamModelLoader, fileDescriptorModelLoader, context,glide, requestTracker, lifecycle, optionsApplier));
}
......省略
先看load()方法的返回值类型:DrawableTypeRequest
GenericRequestBuilder.java
protected final Class modelClass;
protected final Context context;
protected final Glide glide;
protected final Class transcodeClass;
protected final RequestTracker requestTracker;//请求追踪器
protected final Lifecycle lifecycle;
private ChildLoadProvider 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 animationFactory = NoAnimation.getFactory();
private int overrideHeight = -1;//覆写高度
private int overrideWidth = -1;//覆写宽度
private DiskCacheStrategy diskCacheStrategy = DiskCacheStrategy.RESULT;//磁盘缓存策略
private Transformation transformation = UnitTransformation.get();
private boolean isTransformationSet;
private boolean isThumbnailBuilt;
private Drawable fallbackDrawable;
private int fallbackResource;
上述源码截取了GenericRequestBuilder类所有的成员变量。可以发现Glide通过构建者模式配置的所有参数都在这里(什么是构建者模式?请读者自行查阅学习),load()方法最终返回了加载图片的请求对象(DrawableTypeRequest
接着看load()方法体内的fromString()方法。fromString()方法内部调用了loadGeneric()方法。在loadGeneric()方法内部,我们看到了熟悉的名字:ModelLoader。在上一篇中,我们已经介绍过,将Model转化为Data的角色就是ModelLoader。这里一共创建了两个ModelLoader,一个是输入流ModelLoader,一个是文档描述符ModelLoader。方法最终返回了optionsApplier.apply(new DrawableTypeRequest
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 The type of the model.
*/
void apply(GenericRequestBuilder requestBuilder);
}
......省略
class OptionsApplier {
public > 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
综上,loadGeneric()方法最终创建了一个DrawableTypeRequest
注: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缓存算法,如下图所示。
划重点:
1.新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候(即达到总缓存数量上限),将链表尾部的数据丢弃。
Glide的内存缓存策略基于LruCache算法,android.util包下就有关于Lru缓存算法的实现类LruCache。
让我们看看LruCache的源码实现,首先来看看LruCache的成员变量及构造方法。
LruCache.java
private final LinkedHashMap 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(0, 0.75f, true);
}
留意LruCache构造方法,首先对maxSize进行了赋值。其次,在创建LinkedHashMap
第一个参数为initialCapacity,即初始容量,定义了LinkedHashMap的初始大小。
第二个参数为loadFactor,即加载因子,默认值0.75F,意为如果LinkedHashMap中的元素数量达到了总容量的75%,就会扩容为原来的两倍。例如,定义一个HashMap,初始容量默认为16,加载因子0.75F,那么此HashMap的初始实际容量为12,当HashMap内元素数量达到12时,会自动扩容至2倍,即32。这块各位读者可以参阅HashMap源码,日后我也会写一些HashMap源码分析。
第三个参数为accessOrder,定义了LinkedHashMap
接下来看一下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.
*
* The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
*
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中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.
*
* 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 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;
}
最后看一下删除数据的逻辑,比较简单,留给读者自行阅读。