1. 程式人生 > >Glide原始碼分析4 -- 快取,編解碼和網路請求

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原始碼解析的最終篇,感謝大家能看完我寫的這些。不正確的地方,還希望指出來。謝謝!