先預祝大家湯圓節快樂!很久沒寫博客了。今天我們來探索一下Glide是如何支持Gif圖片加載的。
為什么會有這么一個想法呢,一來一直對Glide是知其名而不知其所以然,二來還主要是工作中需要對它研究研究,以便更好的支持工作內容。
我想很多同學都希望自己可以對某種著名的開源框架了解貫通,但是很多時候研究一款框架實在是費神費力,很容易就會放棄。
造成這樣的困局主要有三點:
一來因為我們在探究源碼時沒有明確的目標。二來是因為我們沒有合適順手的工具。三來是因為找不到重點,容易被其它不相干代碼迷惑。接下來我們就對上面這些問題一一帶入。
1,首先我的目標很明確,我需要了解Glide是否支持Gif圖片,以及它是如何支持Gif圖片的。這樣我才可以在應用層對其做良好的支持。
因為我們的工作要求是:所有的ImageView都必須支持Gif圖片
我的解決辦法有三種:
1.如果Glide支持Gif圖片,那么我只需要在圖片調用層全部加上Gif支持開關。(事實上Glide默認就支持Gif,不需要我單獨添加控制。)2.如果Glide支持Gif圖片,但是它的檢測開銷成本很大,那我就必須手動對資源進行解析,判斷是否是Gif,如果是,則調用Gif圖片的加載邏輯。如果不是,則走一般的圖片加載邏輯。3.如果Glide不支持Gif圖片,那么我必須對ImageView進行擴展,然后更改應用內所有的ImageView的繼承關系。這個工作量是巨大的。因為有以上判斷條件,所以我決定先從Glide的Gif支持入手。
2,因為我們需要對Glide研究、分析,那么手上必須有Glide的最新代碼。我們在Glide的主頁上找到源代碼的下載地址,下載即可。
Glide首頁: https://github.com/bumptech/glide/releases Glide源碼地址: https://github.com/bumptech/glide/releases/download/v3.7.0/glide-3.7.0-sources.jar
3.準備工作已經做的差不多了,最后還剩代碼分析利器Android Studio以及Source Insight,當然放在手邊為我們做輔助記錄的筆和紙是少不了的。
Source Insight的主頁為:https://www.sourceinsight.com/ Source Insight的功能很強大,我也只是懂一點點基本用法而已,不過足夠用了。下載好的代碼需要使用Source Insight打開,我們需要實時檢索文件使用。這里不再說明Source Insight的用法,請自行學習了解。它在這里的作用是幫我們做一些引用關系檢查。
除了Source Insight之外,我們主要使用Android Studio進行代碼分析調試。需要將剛剛下載好的源代碼解壓,然后作為我們工程的一部分:
然后按照Glide的使用說明開始我們的分析入口編寫:
// For a simple view:@Override public void onCreate(Bundle savedInstanceState) { ... ImageView imageView = (ImageView) findViewById(R.id.my_image_view); Glide.with(this).load("http://QQ.yh31.com/tp/zjbq/201612231514480890.gif").into(imageView);}為了輔助我們一次次分析Glide的網絡訪問,我們在onDestroy方法中加入以下代碼:
PRotected void onDestroy() { super.onDestroy(); Glide.get(this).clearMemory(); Glide.get(this).clearDiskCache();}我們如果需要了解Glide是否默認支持Gif圖片,那么只需要在load方法內替換成gif圖片的地址即可。
我們發現,它支持。
那么它是如何完成網絡資源獲取、Gif類型識別、Gif資源解析這些工作的呢?下面讓我們一起來一探究竟。
Glide對Gif資源的獲取也是Glide網絡請求的核心,我想大家對這些框架一般都看中的是這部分。讓我們從這里究其所以然。
在這里聲明一下,我們剛開始拿到代碼時,就算會使用,也不知道真正的分析入口在哪里。但是不要灰心,就算是對代碼再熟悉的人,也會迷失在這結構復雜的代碼海洋里。請記住,分析的過程是總是需要來回反復查看、嘗試的。所以手邊的紙和筆對我們的幫助就體現出來了,我們需要通過紙和筆來記錄我們走過的重要流程。
PS: 以后的分析過程會將沒有歧義的過程自動略過,并且會將無關代碼自動省略。
PS: 我們的分析手段主要有兩種,一是通過斷點調試來分析,二是通過上下文來分析。其中第一種比較方便,文章中主要采用第一種方法。
我們先來分析這段代碼:
Glide.with(this)由于我們是在Activity中使用的,所以這里的this應當是Activity,我們進入這個方法查看:
public static RequestManager with(FragmentActivity activity) { RequestManagerRetriever retriever = RequestManagerRetriever.get(); return retriever.get(activity); }好,從上面得知,這個方法返回了一個RequestManager對象,接下來分析
.load("http://qq.yh31.com/tp/zjbq/201612231514480890.gif")這里的load方法則調用的是RequestManager的load方法,我們看一下:
public DrawableTypeRequest<String> load(String string) { return (DrawableTypeRequest<String>) fromString().load(string); }我們看到,load方法返回了一個DrawableTypeRequest對象,我們先記住它。接下來我們需要分析
.into(new ImageView(this));我們跟著這個into方法一路追蹤,最后來到了GenericRequestBuilder的into方法:
public <Y extends Target<TranscodeType>> Y into(Y target) { ... Request request = buildRequest(target); target.setRequest(request); lifecycle.addListener(target); requestTracker.runRequest(request); return target; }這里我們看到構建了一個Request對象,我們進去看一下是如何構建這個對象的,最后我們在GenericRequestBuilder類中定位到了這個方法:
private Request obtainRequest(Target<TranscodeType> target, float sizeMultiplier, Priority priority, RequestCoordinator requestCoordinator) { return GenericRequest.obtain( loadProvider, model, signature, context, priority, target, sizeMultiplier, placeholderDrawable, placeholderId, errorPlaceholder, errorId, fallbackDrawable, fallbackResource, requestListener, requestCoordinator, glide.getEngine(), transformation, transcodeClass, isCacheable, animationFactory, overrideWidth, overrideHeight, diskCacheStrategy); }看來上面提到的Request對象實則為GenericRequest的實例,我們先記下。
然后返回進入requestTracker.runRequest(request)中查看,看起來像是運行這個請求的意思。
runRequest的內部實現是這樣的:
public void runRequest(Request request) { requests.add(request); if (!isPaused) { request.begin(); } else { pendingRequests.add(request); } }它內部調用了request對象的begin方法,也就是說這里調用了GenericRequest的begin()方法。我們找到這個方法:
public void begin() { ... if (Util.isValidDimensions(overrideWidth, overrideHeight)) { onSizeReady(overrideWidth, overrideHeight); } else { target.getSize(this); } ... }在這里走的else條件,我們可能已經不太記得target到底是誰實現的,它只是個接口,幸好有AS,我們通過調試知道這個target其實為:GlideDrawableImageViewTarget,具體它是什么時候被設置到這里的,我們先不去深究它,肯定能找到地方,但找它不是我們的目的。
我們找到它對應的getSize()方法:
public void getSize(SizeReadyCallback cb) { sizeDeterminer.getSize(cb); }我們不要在這里停留,繼續往下走,最后我們會走到com.bumptech.glide.request.GenericRequest的onSizeReady方法中,我們在這里注意重點部分:
public void onSizeReady(int width, int height) { ... loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder, priority, isMemoryCacheable, diskCacheStrategy, this); ... }從Engine的load方法我們進去看,這里是我們繼續執行的重點,我們進入到com.bumptech.glide.load.engine.Engine的load方法:
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) { ... 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); EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority); jobs.put(key, engineJob); engineJob.addCallback(cb); engineJob.start(runnable); ... return new LoadStatus(cb, engineJob); }在這路上一定不能被其它代碼迷惑,要感知哪部分是重點,嘗試自己分析一下這部分。有沒有很像任務及線程池?沒錯,你如果看各個類之間的繼承關系的話,它們確實是,我們就不再看它們之間的關系,我們只用看EngineRunnable的run()方法。
public void run() { ... Exception exception = null; Resource<?> resource = null; try { resource = decode(); } catch (Exception e) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Exception decoding", e); } exception = e; } ... if (resource == null) { onLoadFailed(exception); } else { onLoadComplete(resource); } }這段代碼主要由兩部分組成,這先簡單描述一下它們的工作流程,首先進入decode方法嘗試從緩存中獲取資源,第一次當然是null,然后進入onLoadFailed方法。onLoadFailed會將這個任務再次提交,再次重新執行,這次會進入decodeFromSource方法:
private Resource<?> decodeFromSource() throws Exception { return decodeJob.decodeFromSource(); }我們一路向下,最后來到com.bumptech.glide.load.engine.DecodeJob的decodeSource方法,這個過程千萬別掉隊了,這里馬上就要見到如何訪問網絡了:
private Resource<T> decodeSource() throws Exception { ... final A data = fetcher.loadData(priority); ... decoded = decodeFromSourceData(data); ... return decoded; }這里有兩部分重點,一個是獲取資源,一個是對資源進行解析。這里的fetcher也是一個接口,它的實現類中有HttpUrlFetcher,很明顯的網絡資源獲取類,我們通過調試也發現這里的對象是ImageVideoFetcher,而它的內部正是調用了HttpUrlFetcher的loadData方法,我們再繼續往下,我們很快就發現了Glide的網絡訪問核心方法:
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException { ... urlConnection = connectionFactory.build(url); for (Map.Entry<String, String> headerEntry : headers.entrySet()) { urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue()); } urlConnection.setConnectTimeout(2500); urlConnection.setReadTimeout(2500); urlConnection.setUseCaches(false); urlConnection.setDoInput(true); ... final int statusCode = urlConnection.getResponseCode(); if (statusCode / 100 == 2) { return getStreamForSuccessfulRequest(urlConnection); } ... }好,是不是很熟悉呢?原來Glide內部使用了Android的HttpURLConnection來進行網絡訪問,而且這里的訪問訪問超時時間是固定的:2500毫秒。
到目前為止,我們所處的位置為HttpUrlFetcher的loadDataWithRedirects方法,當然,我們并不在主線程:
at com.bumptech.glide.load.data.HttpUrlFetcher.loadDataWithRedirects(HttpUrlFetcher.java:49) at com.bumptech.glide.load.data.HttpUrlFetcher.loadData(HttpUrlFetcher.java:44) at com.bumptech.glide.load.data.HttpUrlFetcher.loadData(HttpUrlFetcher.java:20) at com.bumptech.glide.load.model.ImageVideoModelLoader$ImageVideoFetcher.loadData(ImageVideoModelLoader.java:70) at com.bumptech.glide.load.model.ImageVideoModelLoader$ImageVideoFetcher.loadData(ImageVideoModelLoader.java:53) at com.bumptech.glide.load.engine.DecodeJob.decodeSource(DecodeJob.java:170) at com.bumptech.glide.load.engine.DecodeJob.decodeFromSource(DecodeJob.java:128) at com.bumptech.glide.load.engine.EngineRunnable.decodeFromSource(EngineRunnable.java:122) at com.bumptech.glide.load.engine.EngineRunnable.decode(EngineRunnable.java:101) at com.bumptech.glide.load.engine.EngineRunnable.run(EngineRunnable.java:58)所以,到目前為止,我們已經知道了Glide是如何訪問網絡的。
接著上面的部分繼續,因為我們已經得到了從網絡傳回的數據流,那么接下來就需要對這些數據進行解析,我們回到com.bumptech.glide.load.engine.DecodeJo的decodeSource方法,也就是回到這里:
private Resource<T> decodeSource() throws Exception { Resource<T> decoded = null; try { long startTime = LogTime.getLogTime(); final A data = fetcher.loadData(priority); ... decoded = decodeFromSourceData(data); } finally { fetcher.cleanup(); } return decoded; }因為我們是從fetcher.loadData中返回的,所以接下來我們需要進入decodeFromSourceData方法內,然后再一路向下追蹤,最后來到com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecode的decodeStream方法內:
private GifBitmapWrapper decodeStream(ImageVideoWrapper source, int width, int height, byte[] bytes) throws IOException { InputStream bis = streamFactory.build(source.getStream(), bytes); bis.mark(MARK_LIMIT_BYTES); ImageHeaderParser.ImageType type = parser.parse(bis); bis.reset(); ... return result; }我們會注意到有段代碼,將InputStream解析為了ImageHeaderParser.ImageType類型的對象,我們可以猜測,這極有可能是對各種網絡流進行分類的地方,我們進去繼續向下追蹤一探究竟,最后來到com.bumptech.glide.load.resource.bitmap.ImageHeaderParser的getType方法:
public ImageType getType() throws IOException { int firstTwoBytes = streamReader.getUInt16(); // JPEG. if (firstTwoBytes == EXIF_MAGIC_NUMBER) { return JPEG; } final int firstFourBytes = firstTwoBytes << 16 & 0xFFFF0000 | streamReader.getUInt16() & 0xFFFF; // PNG. if (firstFourBytes == PNG_HEADER) { // See: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha-color-type streamReader.skip(25 - 4); int alpha = streamReader.getByte(); // A RGB indexed PNG can also have transparency. Better safe than sorry! return alpha >= 3 ? PNG_A : PNG; } // GIF from first 3 bytes. if (firstFourBytes >> 8 == GIF_HEADER) { return GIF; } return UNKNOWN; }果不其然,在這個方法內部對所有的數據進行識別,我們在最后面看到了gif數據的識別原理:firstFourBytes >> 8 == GIF_HEADER。
好,既然知道了現在的數據流是gif了,那么接下來就是解析過程了,我們回到com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceDecoder的decodeStream方法處,繼續往下走,我們很快就在該方法內看到有這么一行代碼:
if (type == ImageHeaderParser.ImageType.GIF) { result = decodeGifWrapper(bis, width, height); }原來這個方法對GIF類型的圖片做了專門的處理,我們進入這個方法并一路向下,最后我們會來到com.bumptech.glide.load.resource.gif.GifResourceDecoder的decode(byte[] data, int width, int height, GifHeaderParser parser, GifDecoder decoder)方法:
private GifDrawableResource decode(byte[] data, int width, int height, GifHeaderParser parser, GifDecoder decoder) { ... Bitmap firstFrame = decodeFirstFrame(decoder, header, data); ... GifDrawable gifDrawable = new GifDrawable(context, provider, bitmapPool, unitTransformation, width, height, header, data, firstFrame); return new GifDrawableResource(gifDrawable); }我們注意到在這個方法內解析了Gif資源的第一幀。我們進到decodeFirstFrame方法看一下它是如何解析的:
private Bitmap decodeFirstFrame(GifDecoder decoder, GifHeader header, byte[] data) { decoder.setData(header, data); decoder.advance(); return decoder.getNextFrame(); }這里最后調用了decoder.getNextFrame()方法,這里的decoder為GifDecoder,也就是專門用于解析Gif資源的解碼器,我們進入getNextFrame()方法一探究竟:
public synchronized Bitmap getNextFrame() { ... status = STATUS_OK; GifFrame currentFrame = header.frames.get(framePointer); GifFrame previousFrame = null; int previousIndex = framePointer - 1; if (previousIndex >= 0) { previousFrame = header.frames.get(previousIndex); } ... // Transfer pixel data to image. Bitmap result = setPixels(currentFrame, previousFrame); ... return result; }這里的代碼還挺長的,我們只挑最主要的看,它最后調用了setPixels()方法:
private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { ... // Decode pixels for this frame into the global pixels[] scratch. decodeBitmapData(currentFrame); // Copy each source line to the appropriate place in the destination. int pass = 1; int inc = 8; int iline = 0; for (int i = 0; i < currentFrame.ih; i++) { int line = i; if (currentFrame.interlace) { if (iline >= currentFrame.ih) { pass++; switch (pass) { case 2: iline = 4; break; case 3: iline = 2; inc = 4; break; case 4: iline = 1; inc = 2; break; default: break; } } line = iline; iline += inc; } line += currentFrame.iy; if (line < header.height) { int k = line * header.width; // Start of line in dest. int dx = k + currentFrame.ix; // End of dest line. int dlim = dx + currentFrame.iw; if ((k + header.width) < dlim) { // Past dest edge. dlim = k + header.width; } // Start of line in source. int sx = i * currentFrame.iw; while (dx < dlim) { // Map color and insert in destination. int index = ((int) mainPixels[sx++]) & 0xff; int c = act[index]; if (c != 0) { dest[dx] = c; } dx++; } } } ... // Set pixels for current image. Bitmap result = getNextBitmap(); result.setPixels(dest, 0, width, 0, 0, width, height); return result; }這段代碼還是很長,我們將不主要的代碼隱去,中間很長一部分推測應該是進行數據轉換。最終是調用了Bitmap的setPixels方法完成位圖的創建。
好,到此為止,我們知道了Gif圖是如何解析成位圖的了,然后我們返回,回到com.bumptech.glide.load.resource.gif.GifResourceDecoder的decode方法繼續向下走:
private GifDrawableResource decode(byte[] data, int width, int height, GifHeaderParser parser, GifDecoder decoder) { ... Bitmap firstFrame = decodeFirstFrame(decoder, header, data);//這里是剛剛出來的地方,從這里繼續向下 if (firstFrame == null) { return null; } Transformation<Bitmap> unitTransformation = UnitTransformation.get(); GifDrawable gifDrawable = new GifDrawable(context, provider, bitmapPool, unitTransformation, width, height, header, data, firstFrame); return new GifDrawableResource(gifDrawable); }我們很快就發現,剛才解析好的位圖被用作創建了GifDrawable對象,然后GifDrawable對象又用來創建了GifDrawableResource對象,然后返回,回到最開始的com.bumptech.glide.load.engine.EngineRunnable的run方法:
public void run() { if (isCancelled) { return; } Exception exception = null; Resource<?> resource = null; try { resource = decode();//我們剛剛從這里返回 } catch (Exception e) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Exception decoding", e); } exception = e; } ... if (resource == null) { onLoadFailed(exception); } else { onLoadComplete(resource);//然后代碼繼續向下執行會從這里走 } }我們回到最開始的EngineRunnable的run方法。然后我們知道這里的resource不是null,所以進入onLoadComplete方法。到這里為止,我們就完成了Gif資源的解析過程分析。
從onLoadComplete方法開始就是Gif資源的輪播流程了,由于篇幅有限,在這里就不再涉及,有興趣的同學可以自行分析鍛煉一下。
最后希望同學們可以嘗試使用本方法舉一反三,分析一下其它框架,反復學習,加深印象。
我建了一個QQ群,歡迎對學習有興趣的同學加入。我們可以一起探討、深究、掌握那些我們會用到的技術,讓自己不至于太落伍。
新聞熱點
疑難解答