上篇文章主要分析Glide的處理流程以及with()方法的內(nèi)部邏輯(參閱:〔兩行哥〕提綱挈領吱型,帶你梳理Glide主要源碼邏輯(一))逸贾,本篇主要分析load()方法,同時為大家介紹Bitmap優(yōu)化和LruCache算法相關理論,為最后一個重頭戲into()方法做鋪墊铝侵。
Glide.with()方法返回值類型為RequestManger灼伤,那么我們繼續(xù)分析load()方法也主要集中在RequestManger類中。
load()方法主要是對Glide內(nèi)部的Model進行封裝與處理(什么是Model咪鲜?請參閱上篇文章)狐赡,最終形成圖片加載請求。
一疟丙、load()源碼邏輯
在RequestManger類中提供了多種load()方法的重載颖侄,包括load(String string)、load(Uri uri)享郊、load(Integer resId)览祖、load(File file)等,分別適用于加載網(wǎng)絡圖片地址拂蝎,加載圖片Uri穴墅,加載圖片resId,加載圖片文件等温自,我們以加載網(wǎng)絡圖片地址為例進行分析:
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>松捉。根據(jù)類名判斷,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;//請求監(jiān)聽
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類所有的成員變量隘世。可以發(fā)現(xiàn)Glide通過構建者模式配置的所有參數(shù)都在這里(什么是構建者模式鸠踪?請讀者自行查閱學習)丙者,load()方法最終返回了加載圖片的請求對象(DrawableTypeRequest<ModelType>實例)。
接著看load()方法體內(nèi)的fromString()方法营密。fromString()方法內(nèi)部調用了loadGeneric()方法械媒。在loadGeneric()方法內(nèi)部,我們看到了熟悉的名字:ModelLoader评汰。在上一篇中纷捞,我們已經(jīng)介紹過,將Model轉化為Data的角色就是ModelLoader被去。這里一共創(chuàng)建了兩個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的內(nèi)部類,只有一個apply(X builder)方法臀玄。在apply(X builder)中瓢阴,對options進行了非空判斷,如果不為空健无,就調用options的apply()方法荣恐。如源碼中的注釋說明,options為RequestManger類的成員變量(用戶自定義的Glide配置)累贤,如果用戶沒有傳入options叠穆,則默認值為null。apply(X builder)方法最終將參數(shù)builder進行了返回臼膏,結合上文來看硼被,builder即 DrawableTypeRequest<T>的實例。
綜上渗磅,loadGeneric()方法最終創(chuàng)建了一個DrawableTypeRequest<T>對象并進行了返回嚷硫,在創(chuàng)建DrawableTypeRequest<T>對象的構造方法中,傳入了之前所述的兩個ModelLoader以及requestTracker(請求追蹤器)等始鱼。
注:RequestTracker是Glide中一個核心類仔掸,將在下一篇into()方法中著重介紹。
二医清、Bitmap優(yōu)化
(一)Bitmap的OOM
Bitmap占用內(nèi)存大小 = 同時加載的Bitmap數(shù)量 * 每個Bitmap圖片的寬度px * 每個Bitmap圖片的高度px * 每個像素占用的內(nèi)存起暮。而每個像素占有多大的內(nèi)存呢?這取決于此像素的類別及是否采用了壓縮技術会烙。
如果是非黑即白的二值圖像负懦,不壓縮的情況下一個像素只需要1個bit。
如果是256種(2的8次方)狀態(tài)的灰度圖像柏腻,不壓縮的情況下一個像素需要8bit(1Byte纸厉,256種狀態(tài))。
如果用256種(2的8次方)狀態(tài)標識屏幕上某種顏色的灰度五嫂,而屏幕采用三基色紅綠藍(RGB)残腌,不壓縮的情況下一個像素需要占用24bit(3Byte),這個就是常說的24位真彩色贫导。
還有各種其他的存儲方式,例如15bit蟆盹、16bit孩灯、32bit等。如果考慮到壓縮逾滥,有損壓縮或無損壓縮峰档,具體采用的壓縮算法及壓縮參數(shù)設置都會影響一個像素占用的存儲空間败匹。
例如,如果在頁面顯示一張1920 * 1080的圖片讥巡,采用Android內(nèi)置的ARGB_8888壓縮掀亩,占用的內(nèi)存大約為8MB左右。
而每個Android應用的VM堆內(nèi)存上限是通過dalvik.vm.heapgrowthlimit設置(參閱:Android Dalvik Heap 淺析)欢顷,如果同一個頁面展示的圖片長寬過大或數(shù)量過多槽棍,占用的內(nèi)存超過了此上限值,就會導致OOM抬驴。
注:1GB = 1024MB炼七;1MB = 1024KB;1KB = 1024Byte布持;1Byte = 8bit豌拙。
(二)Bitmap的優(yōu)化策略
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擁有三級緩存推捐,即每獲取到一張圖片,都會在內(nèi)存和本地文件進行緩存侧啼,如果下次又用到了同樣的圖片:
1.內(nèi)存有無需要的圖片牛柒?有的話就用,沒有就去本地文件找痊乾。
2.本地文件有無需要的圖片皮壁?有的話就用,沒有就去網(wǎng)絡找哪审。
一共有三個環(huán)節(jié):內(nèi)存 --> 本地文件 --> 網(wǎng)絡蛾魄。
說完三級緩存,接下來再引入一個概念:Lru緩存算法湿滓,如下圖所示滴须。
劃重點:
1.新數(shù)據(jù)插入到鏈表頭部;
2.每當緩存命中(即緩存數(shù)據(jù)被訪問)叽奥,則將數(shù)據(jù)移到鏈表頭部扔水;
3.當鏈表滿的時候(即達到總緩存數(shù)量上限),將鏈表尾部的數(shù)據(jù)丟棄朝氓。
Glide的內(nèi)存緩存策略基于LruCache算法魔市,android.util包下就有關于Lru緩存算法的實現(xiàn)類LruCache主届。
讓我們看看LruCache的源碼實現(xiàn),首先來看看LruCache的成員變量及構造方法待德。
LruCache.java
private final LinkedHashMap<K, V> map;//LruCache內(nèi)部基于LinkedHashMap實現(xiàn)
private int size;//當前已緩存的數(shù)量
private int maxSize;//可緩存的最大數(shù)量(總緩存容量)
private int putCount;//加入的緩存數(shù)量
private int createCount;//創(chuàng)造的緩存數(shù)量(如果取緩存時緩存不存在君丁,則會優(yōu)先創(chuàng)造緩存,下文分析)
private int evictionCount;//淘汰的數(shù)量(如果超出緩存容量将宪,則最少使用的緩存會被淘汰)
private int hitCount;//命中數(shù)量(如果取用緩存绘闷,緩存依舊存在,沒有沒淘汰涧偷,則算作命中)
private int missCount;//未命中數(shù)量(如果取用緩存簸喂,緩存已經(jīng)被淘汰,則算未命中)
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進行了賦值喻鳄。其次,在創(chuàng)建LinkedHashMap<K确封,V>對象的時候除呵,傳入了3個參數(shù):0,0.75F爪喘,true颜曾。這里稍微解釋一下:
第一個參數(shù)為initialCapacity,即初始容量秉剑,定義了LinkedHashMap的初始大小泛豪。
第二個參數(shù)為loadFactor,即加載因子侦鹏,默認值0.75F诡曙,意為如果LinkedHashMap中的元素數(shù)量達到了總容量的75%,就會擴容為原來的兩倍略水。例如价卤,定義一個HashMap,初始容量默認為16渊涝,加載因子0.75F慎璧,那么此HashMap的初始實際容量為12,當HashMap內(nèi)元素數(shù)量達到12時跨释,會自動擴容至2倍胸私,即32。這塊各位讀者可以參閱HashMap源碼鳖谈,日后我也會寫一些HashMap源碼分析岁疼。
第三個參數(shù)為accessOrder,定義了LinkedHashMap<K蚯姆,V>的排序模式五续。當為true時,LinkedHashMap<K龄恋,V>為access-order(訪問順序模式疙驾,也就是LruCache算法的模式),當為false時郭毕,LinkedHashMap<K它碎,V>為insertion-order(插入順序模式)。建議讀者參閱LinkedHashMap<K显押,V>源碼扳肛。
接下來看一下LruCache類中的四個核心方法:獲取數(shù)據(jù)、緩存數(shù)據(jù)乘碑、調整總緩存大小及刪除數(shù)據(jù)挖息。
(一)獲取數(shù)據(jù)
LruCache.java
//獲取數(shù)據(jù)
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) {
//命中數(shù)量+1,并返回mapValue
hitCount++;
return mapValue;
}
missCount++;//未命中數(shù)量+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;
}
}
對獲取數(shù)據(jù)方法get(K key)進行分析兽肤。首先從LinkedHashMap中get(key)套腹,如果取出數(shù)據(jù)不為null,說明數(shù)據(jù)命中资铡,命中數(shù)計數(shù)+1电禀,同時返回取出的數(shù)據(jù)。如果取出數(shù)據(jù)為null笤休,未命中數(shù)計數(shù)+1尖飞,get(K key)方法繼續(xù)向下執(zhí)行,調用了方法體內(nèi)的create(key)方法店雅。create(key)方法執(zhí)行了什么操作呢政基?查看源碼。
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)方法在獲取數(shù)據(jù)失敗(未命中緩存)的時候調用亥揖,用戶可以覆寫該方法珊擂,在未命中緩存的時候返回特定的數(shù)據(jù),默認情況下返回了null费变。
對接下來的一個同步代碼塊進行分析摧扇,可能讀者對這塊代碼非常疑惑,如下段對map數(shù)據(jù)重新覆蓋的邏輯挚歧。
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);
}
}
首先將創(chuàng)造的緩存數(shù)量計數(shù)+1扛稽。
注:調用HashMap.put(key,value)方法具有返回值滑负。如果原本HashMap<String在张,String>中key = “key”用含,對應的value = “preValue”,調用put(“key”帮匾,“newValue”)方法后啄骇,則put()方法會返回“preValue”,現(xiàn)HashMap中的該鍵值對為:key = “key”瘟斜,對應的value = “newValue”缸夹。即如果key對應的value原本就有值,若調用put()方法放入新value螺句,則put()方法會返回原本的舊value虽惭。
回到源碼中,調用map.put(key, createdValue)方法蛇尚,返回該key值的原本數(shù)據(jù)mapValue芽唇。如果原本的mapValue不為null,則再次調用put(key佣蓉,mapValue)將原本數(shù)據(jù)mapValue放回去披摄。這里會比較疑惑,為什么mapValue可能不為null勇凭?之前調用get(key)方法不是已經(jīng)說明該key對應的mapValue為null了嗎疚膊?為什么還要用mapValue覆蓋掉createdValue?
這里體現(xiàn)了源碼作者的嚴謹性虾标。前文已經(jīng)說過寓盗,在這塊代碼之前已經(jīng)調用了 create(key)方法,默認返回了null璧函。而實際情況中傀蚌,用戶可能覆寫該方法,在未命中緩存的情況下蘸吓,返回自定義的數(shù)據(jù)善炫。而用戶覆寫的邏輯可能是耗時操作,同時此處的代碼并不是線程安全的库继,因此在調用上述同步代碼塊的時候箩艺,map.put(key, createdValue)方法可能會返回曾經(jīng)已經(jīng)放進去的mapValue。那么接下來的操作就是將原本放進去的mapValue再次覆蓋createdValue宪萄,即再次調用map.put(key, mapValue)艺谆,銷毀掉createdValue。這里請讀者仔細體悟拜英。
size += safeSizeOf(key, createdValue)的作用是重新計算此時已經(jīng)占用的緩存數(shù)量静汤。接下來if(mapValue != null)的分支中執(zhí)行了entryRemoved(false, key, createdValue, mapValue)方法,這是要實現(xiàn)啥?看看源碼:
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) {}
看原注釋虫给,了解到這是一個空實現(xiàn)藤抡,如果有需要的話,用戶可以覆寫這個方法抹估,這個方法會在緩存數(shù)據(jù)被淘汰或移除時調用杰捂。回到之前的代碼棋蚌,if(mapValue != null)的else分支執(zhí)行了trimToSize(maxSize)來對超過最大緩存數(shù)量外的緩存數(shù)據(jù)進行了淘汰,下文再對trimToSize(maxSize)方法進行分析挨队。
(二)緩存數(shù)據(jù)
LruCache.java
//緩存數(shù)據(jù)
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;
}
首先對加入的緩存計數(shù)putCount+1谷暮,并執(zhí)行size += safeSizeOf(key, value)對已緩存數(shù)據(jù)容量重新計算。然后調用map.put(key, value)獲取原本舊緩存previous盛垦。如果previous不為null湿弦,需要再次執(zhí)行size -= safeSizeOf(key, previous)對已緩存數(shù)據(jù)容量重新計算。
entryRemoved(false, key, previous, value)方法前文已經(jīng)分析過腾夯,跳過颊埃。
最終又調用了trimToSize(maxSize)對超過最大緩存數(shù)量外的緩存數(shù)據(jù)進行了淘汰。
(三)調整緩存大小
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該鍵值對,重新計算已緩存數(shù)量榨呆,并對淘汰緩存數(shù)量計數(shù)evictionCount+1罗标。
(四)刪除數(shù)據(jù)
LruCache.java
//刪除數(shù)據(jù)
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;
}
最后看一下刪除數(shù)據(jù)的邏輯,比較簡單积蜻,留給讀者自行閱讀闯割。