先预祝大家汤圆节快乐!很久没写博客了。今天我们来探索一下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群,欢迎对学习有兴趣的同学加入。我们可以一起探讨、深究、掌握那些我们会用到的技术,让自己不至于太落伍。
新闻热点
疑难解答