Glide原始碼分析4 -- 快取,編解碼和網路請求
1. 概述和核心類
在Glide原始碼分析 – request建立與傳送過程一文中,我們談到request最終通過GenericRequest的onSizeReady()方法進行,其中呼叫了engine.load()方法去實際獲取資料。本文主要講述engine.load()之後發生的那些事,讓大家能夠對底層資料獲取有個更清晰的認識。從這點也可以看出Glide設計分層的精妙。主要涉及的核心類如下
1)GenericRequest:定義了很多對request的處理方法,我們比較關心的是request的傳送,它的入口是begin(),會呼叫到onSizeReady(),最終呼叫到engine.load(),也就是資料獲取部分的入口
2)Engine:封裝了資料獲取的很多關鍵方法,向request層提供這些API,比如load(), release(), clearDiskCache()等方法。可以認為是一個外觀模式。
3)MemoryCache:記憶體快取類,先從快取中獲取資料,如果沒有才做後面的工作。這是第一級快取。Glide採用了兩級快取模式。第二級快取為DiskLruCache,為磁碟快取。獲取磁碟快取比較耗時,需要在子執行緒中進行,故而在DecodeJob中得到呼叫。此處不會呼叫磁碟快取。
4)EngineJob, EngineRunnable:EngineJob是一個控制類,作為EngineRunnable的管理者,提供start(), cancel()等很多操作runnable的方法。一般會以執行緒池的方式向子執行緒提交EngineRunnable任務。而EngineRunnable就是我們在子執行緒中需要執行的任務,也是特別關鍵的一個類。
5)DecodeJob,DataFetcher,ResourceDecoder,Transformation:DecodeJob流程為從快取或網路或本地獲取資料,然後轉碼為所需的格式,最後編碼並儲存到DiskLruCache中。這是資料獲取階段很關鍵的一個類。DataFetcher負責根據不同途徑資料獲取(如本地File,url,URI等),ResourceDecoder負責根據不同檔案格式解碼(如Bitmap,GIF等)Transformation負責編碼為不同格式檔案(如Bitmap,GIF等)。後面在類層次關係中會詳細講解這幾個類的關係以及它們的子類。現在只需要知道這幾個類的作用就可以了。
上面講解了五個方面,大概十多個類。我們可以把整個過程分為兩個階段:任務提交階段和任務執行階段。為了更清晰的理清邏輯關係,可以看下面這張圖。
任務提交階段:
任務執行階段
2. 任務提交階段原始碼分析
1)GenericRequest的begin()方法是整個提交階段的入口,它會呼叫engine來完成任務的提交,並回調一些listener。這些listener是我們經常使用的,從下面的原始碼中我們可以清晰的看見這些listener的呼叫時機。
public void begin() {
startTime = LogTime.getLogTime();
if (model == null) {
// loadModel為空時,會回撥requestListener的onException()
onException(null);
return;
}
status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
// size驗證通過後,會提交請求
onSizeReady(overrideWidth, overrideHeight);
} else {
target.getSize(this);
}
if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
// onLoadStarted回撥時機,任務提交最開始的時候
target.onLoadStarted(getPlaceholderDrawable());
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
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();
// 獲取DataFetcher,我們可以自定義DataFetcher
final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
if (dataFetcher == null) {
// 會回撥requestListener的onException()
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));
}
}
2)engine.load(), 先嚐試從記憶體快取獲取資料,再嘗試從當前活躍Resources中獲取資料,再看看這個任務是否當前已經提交過了。這些都沒有的話,最後提交任務。它規範了整個任務提交的流程,可以看做是一個模板方法。
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();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
// 先嚐試從記憶體快取獲取資料
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
// 獲取資料成功,會回撥target的onResourceReady()
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
// 在嘗試從活動Resources map中獲取,它表示的是當前正在使用的Resources
// 它也是在記憶體中,與記憶體快取不同之處是clear快取時不會clear它。
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;
}
// 然後看看當前jobs中是否包含這個任務了,如果包含說明任務之前已經提交了,正在執行
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);
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority);
// runnable是關鍵,它是任務執行階段的入口
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
// 開始提交job
engineJob.start(runnable);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
3)engineJob.start(runnable),提交任務到執行緒池
class EngineJob implements EngineRunnable.EngineRunnableManager {
public void start(EngineRunnable engineRunnable) {
this.engineRunnable = engineRunnable;
// 提交任務,diskCacheService預設是一個AbstractExecutorService
future = diskCacheService.submit(engineRunnable);
}
}
public abstract class AbstractExecutorService implements ExecutorService {
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 執行緒池中執行子執行緒的任務,子執行緒得到呼叫後任務就可以執行了
execute(ftask);
return ftask;
}
}
3. 任務執行階段原始碼分析
1)EngineRunnable的run(),它是任務執行的入口
class EngineRunnable implements Runnable, Prioritized {
public void run() {
if (isCancelled) {
return;
}
Exception exception = null;
Resource<?> resource = null;
try {
// 使用DecodeJob來完成資料的獲取,編解碼等
resource = decode();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}
if (isCancelled) {
if (resource != null) {
// 取消則回收各種資源防止記憶體洩露,此處的子類一般採用物件池方式來回收,防止反覆的建立和回收。如BitmapPool
resource.recycle();
}
return;
}
if (resource == null) {
// 任務執行最終失敗則回撥onLoadFailed,可以從此處分析出target callback的最終回撥時機
onLoadFailed(exception);
} else {
// 任務執行最終成功則回撥onLoadComplete
onLoadComplete(resource);
}
}
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
// 從DiskLruCache中獲取資料並解碼,比較簡單,讀者可自行分析
return decodeFromCache();
} else {
// 從其他途徑獲取資料並解碼,如網路,本地File,資料流等
return decodeFromSource();
}
}
private Resource<?> decodeFromSource() throws Exception {
// 呼叫decodeJob來完成資料獲取和編解碼
return decodeJob.decodeFromSource();
}
}
2)decodeJob.decodeFromSource()
class DecodeJob<A, T, Z> {
public Resource<Z> decodeFromSource() throws Exception {
// 獲取資料,解碼
Resource<T> decoded = decodeSource();
// 編碼並儲存到DiskLruCache中
return transformEncodeAndTranscode(decoded);
}
private Resource<T> decodeSource() throws Exception {
Resource<T> decoded = null;
try {
long startTime = LogTime.getLogTime();
// 利用不同的DataFetcher來獲取資料,如網路,本地File,流檔案等
final A data = fetcher.loadData(priority);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Fetched data", startTime);
}
if (isCancelled) {
return null;
}
// 解碼為所需的格式
decoded = decodeFromSourceData(data);
} finally {
fetcher.cleanup();
}
return decoded;
}
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
// 根據ImageView的scaleType等引數計算真正被ImageView使用的圖片寬高,並儲存真正寬高的圖片。
// 比如centerCrop並且圖片超出被ImageView裁剪時,我們沒必要儲存原圖的寬高,而應該是裁剪之後的寬高,這樣節省儲存空間。
// 這也是Glide相對於Picasso的一個很大的優勢
Resource<T> transformed = transform(decoded);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transformed resource from source", startTime);
}
// 寫入到DiskLruCache中,下次就可以直接從它裡面拿了
writeTransformedToCache(transformed);
startTime = LogTime.getLogTime();
// 轉碼,將源圖片轉碼為ImageView所需的圖片格式
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}
}
3)DataFetcher的loadData(priority), DataFetcher的子類很多,參見概述和核心類部分。此處我們分析下從url獲取的情形,這是我們碰到最多的情形。從url獲取的DataFetcher是HttpUrlFetcher,它採用了android原生的HttpURLConnection網路庫,如下
public class HttpUrlFetcher implements DataFetcher<InputStream> {
// 採用HttpURLConnection作為網路庫,
// 我們可以自定義DataFetcher,從而使用其他網路庫,如OkHttp
private HttpURLConnection urlConnection;
public InputStream loadData(Priority priority) throws Exception {
return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
}
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
throws IOException {
if (redirects >= MAXIMUM_REDIRECTS) {
throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
} else {
// Comparing the URLs using .equals performs additional network I/O and is generally broken.
// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
try {
if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
throw new IOException("In re-direct loop");
}
} catch (URISyntaxException e) {
// Do nothing, this is best effort.
}
}
// 靜態工廠模式建立HttpURLConnection物件
urlConnection = connectionFactory.build(url);
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
}
// Do our best to avoid gzip since it's both inefficient for images and also makes it more
// difficult for us to detect and prevent partial content rendering. See #440.
if (TextUtils.isEmpty(urlConnection.getRequestProperty(ENCODING_HEADER))) {
urlConnection.setRequestProperty(ENCODING_HEADER, DEFAULT_ENCODING);
}
// 設定HttpURLConnection的引數
urlConnection.setConnectTimeout(2500);
urlConnection.setReadTimeout(2500);
urlConnection.setUseCaches(false);
urlConnection.setDoInput(true);
// Connect explicitly to avoid errors in decoders if connection fails.
urlConnection.connect();
if (isCancelled) {
return null;
}
final int statusCode = urlConnection.getResponseCode();
if (statusCode / 100 == 2) {
return getStreamForSuccessfulRequest(urlConnection);
} else if (statusCode / 100 == 3) {
String redirectUrlString = urlConnection.getHeaderField("Location");
if (TextUtils.isEmpty(redirectUrlString)) {
throw new IOException("Received empty or null redirect url");
}
URL redirectUrl = new URL(url, redirectUrlString);
return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
} else {
if (statusCode == -1) {
throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
}
throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
}
}
}
4)ResourceDecoder的decode(),ResourceDecoder的子類也很多,同樣參見概述和核心類部分。我們分析下FileDescriptorBitmapDecoder,它是decode bitmap的關鍵所在
public class FileDescriptorBitmapDecoder implements ResourceDecoder<ParcelFileDescriptor, Bitmap> {
public Resource<Bitmap> decode(ParcelFileDescriptor source, int width, int height) throws IOException {
Bitmap bitmap = bitmapDecoder.decode(source, bitmapPool, width, height, decodeFormat);
// 物件池管理方式,從bitmapPool中獲取一個BitmapResource物件
return BitmapResource.obtain(bitmap, bitmapPool);
}
public Bitmap decode(ParcelFileDescriptor resource, BitmapPool bitmapPool, int outWidth, int outHeight,
DecodeFormat decodeFormat)
throws IOException {
// 通過MediaMetadataRetriever來解碼
MediaMetadataRetriever mediaMetadataRetriever = factory.build();
mediaMetadataRetriever.setDataSource(resource.getFileDescriptor());
Bitmap result;
if (frame >= 0) {
result = mediaMetadataRetriever.getFrameAtTime(frame);
} else {
result = mediaMetadataRetriever.getFrameAtTime();
}
mediaMetadataRetriever.release();
resource.close();
return result;
}
}
5)Transformation的transform(), 根據ImageView的實際寬高來裁剪圖片資料。這樣可以減小儲存到DiskLruCache中的資料大小。它的子類也很多,我們分析下BitmapTransformation。
public abstract class BitmapTransformation implements Transformation<Bitmap> {
public final Resource<Bitmap> transform(Resource<Bitmap> resource, int outWidth, int outHeight) {
if (!Util.isValidDimensions(outWidth, outHeight)) {
throw new IllegalArgumentException("Cannot apply transformation on width: " + outWidth + " or height: "
+ outHeight + " less than or equal to zero and not Target.SIZE_ORIGINAL");
}
Bitmap toTransform = resource.get();
int targetWidth = outWidth == Target.SIZE_ORIGINAL ? toTransform.getWidth() : outWidth;
int targetHeight = outHeight == Target.SIZE_ORIGINAL ? toTransform.getHeight() : outHeight;
// 裁剪,根據CenterCrop和fitCenter有兩種裁剪方式
Bitmap transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight);
final Resource<Bitmap> result;
if (toTransform.equals(transformed)) {
result = resource;
} else {
// 從bitmapPool物件池中獲取BitmapResource
result = BitmapResource.obtain(transformed, bitmapPool);
}
return result;
}
}
public class CenterCrop extends BitmapTransformation {
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
final Bitmap toReuse = pool.get(outWidth, outHeight, toTransform.getConfig() != null
? toTransform.getConfig() : Bitmap.Config.ARGB_8888);
// 按centerCrop方式裁剪
Bitmap transformed = TransformationUtils.centerCrop(toReuse, toTransform, outWidth, outHeight);
if (toReuse != null && toReuse != transformed && !pool.put(toReuse)) {
toReuse.recycle();
}
return transformed;
}
}
6)ResourceTranscoder的transcode(), 編碼。有興趣的讀者可以自行分析
4 總結
Glide的Engine部分分為資料獲取任務提交階段和任務執行階段。提交階段先從記憶體快取和當前活動Resources中獲取,然後再線上程池中新開子執行緒,之後就只需要等待子執行緒得到執行了。任務執行階段先嚐試從DiskLruCache中獲取Resources,然後利用DataFetcher獲取資料,利用ResourceDecoder解碼,利用Transformation裁剪圖片資料,利用ResourceTranscoder轉碼為ImageView所需格式,這樣就獲取到了最終所需的Resources。
這一篇也是Glide原始碼解析的最終篇,感謝大家能看完我寫的這些。不正確的地方,還希望指出來。謝謝!