Glide 4.x之请求网络图片数据流程解析
2017-11-01 20:40
375 查看
在《Glide工作总体执行流程概述》一篇博文中简单分析了glide的工作流程,简而言之就是Glide先构建RequestManager对象,然后RequestManager对象构建ReqeustBuilder对象,再由RequsetBuilder对象创建一个Requset对像,最后将Request交给RequsetMangaer管理,然后RequestManager通知request调用begin发起请求将生成的图片资源交给具体的Target的过程。只不过上篇文章只是简单的做了梳理工作,并没有对详细的说明,本篇就承接上文来继续分析。所以建议读此篇博文的童鞋大致瞅一眼《Glide工作执行流程概述》
本篇还是以SingleRequest为例来说明,在资源还没有加载好的时候,SingleRequest的begin方法会调用一个onSizeReady方法:
进入onSizeReady内部翻看一翻,仍然剔除暂时与本文无关的代码:
也就是说onSizeReady方法内部也就是主要时调用来Engine对象的load方法,那么这个engine对象是什么鬼呢?先看看这个对象是时候初始化的,该对象是在初始化Glide对象的时候进行来初始化,具体的可在GlideBuilder的build方法找到:
从Engine类的构造起所需要的参数来看,Engine负责内存缓存,磁盘缓存等等管理工作,这些内存和磁盘缓存等相关的类可以通过Builder模式让客户端自己构建自己的缓存逻辑,这也是Builder模式的强大之处,该模式也算是常用的设计模式之一,其设计理念还是很值得借鉴的,到此为止Engine的初始化已经创建完毕。下面继续沿着SingleRequest的流程分析engine.load方法:
load方法的代码很长,1至4是与缓存有关的逻辑,本篇暂时抛开不谈,在第五步的时候创建来一个EngineJob对象这是一个DecodeJob.Callback接口的实现类,后面会提到此处可以留意下,现在暂且可以理解为EngineJob是Engine这个引擎的最小工作单元。然后第六步又创建来一个DecodeJob对象(该对象是一个Runnable),第八步 engineJob.start(decodeJob);开始来工作,所以此处需要进入start方法探究一二:
EngineJob的start方法只是获取一个ThreadPoolExecutor对象执行DecodeJob这个Runnable而已(当然其内部还是比较复杂的),也就是说EngineJob只是一个壳子,其核心作用的还是DecodeJob这个对象。所以我们来看看DecodeJob这个Runalbe的run方法都做了些神马:
抛开run方法的枝枝蔓蔓,其内部调用来runWrapped方法,感觉距离真相越来越近了有木有,这是真的吗?:
上面有三个case分支,但是弱水三千只取一瓢,博主通过追踪内部逻辑准备只分析INITIALIZE分支,正如glide注释所说该分支INITIALIZE的意思是”The first time we’ve been submitted”,鉴于英语水平差,就不翻译了(事实上如果你打断点debug的话也会进入这个分支)。该case分支先调用了getNextGenerator方法,所以还是来看看该方法是干了啥牛逼的事儿,在分析runWrapped的时候有山重水复疑无路的感觉,现在感觉距离最终目的地尚有西天还有十万八千里呢!
因为上文说过,本篇步分析与缓存有关的东西,只分析数据的最初来源(也就是服务器数据),所以本篇博文就分析SOURCE分支,该分支返回里一个SourceGenerator对象,该对象需要decodeHelper对象,decodeHelper对象是在初始化DecodeJob的时候对decodeHelper进行初始化的:
调用完getNextGenerator方法之后currentGenerator引用指向的对象就是SourceGenerator对象了,紧接着INITIALIZE的case分支又调用了runGenerators() 方法:
上面的方法中有一个while循环,while循环的条件题里面有一句:
如果该方法执行返回了false,则满足进入循环的条件之一,所以我们看看SourceGenerator的startNext都多了些什么:
上面的代码主要做了两个逻辑:
1、先通过getLoadData获取ModelLoder对象的集合,然后获取一个loadData对象
2、通过loadData对象的fetcher对象的loadData方法来获取图片数据。
其实这里简单打个断点,然后debug一下就知道LoadData对象和fetcher都是什么:
上面说的在这里先埋疑问:
ModeLoder 是什么?什么时候初始化的?
LoadData 有是什么时候初始化的?
Fetcher有是干什么,有是怎么初始化的?
以上三个问题将留在下篇博客说明,本篇为了保持主题不偏方向,先不谈。
这个LoadData是ModelLoader接口的一个内部类:
loaddata 对象内部有一个DataFetcher,该类就是Glide实际获取数据的类,如上图我们得到的是一个HttpUrlFetcher 对象,所以我们直接进入HttpUrlFetcher对象的loadData方法看看:
loaddata方法是真正获取图片数据的地方:
1、调用loadDataWithRedirects方法,发起网络请求,讲URL指定的图片数据转换成InputStream输入流,该方法内部就是通过URLConnection对象来完成的(详细的读者可以自行到HttpUrlFetcher类的loadDataWithRedirects查看)。
2、将图片数据InputStream通过callback的onDataReady来传递,此处是我们遇到的第一个callback。
也就是说通过HttpUrlFecther处理之后,我们的图片数据源转换成来InputStream对象了。
那么上文中的callback 是什么呢?,该callback是一个DataCallback类型的接口(这是我们遇到的第一个callback,后面还有好多,希望读者不要绕晕了)。HttpUrlFecther的loadData方法的第二个参数就是一个DataCallback类型的参数,在SourceGenerator种我们传的是this,也就是说我们这个callback就是SourceGenerator对象,所以看看SourceGenerator对象的onDataReady方法都做了什么:
剔除掉缓存的相关逻辑后,我们迎来了第二个callback,调用了该回调的onDataFetcherReady方法后,我们又将数据传给了这个callback。那么这个callback有是什么呢?该callback是FetcherReadyCallback的一个接口实现。那么这个callback 又是什么时候初始化的呢?当然是初始化SourceGenerator对象的时候,见上文我们是在DecodeJob这个类种初始化SourceGenerator的,DecodeJob就是这个callback的实现类,所以我们进入其onDataFetcherReady方法看看:
然后我们看看decodeFromRetrievedData方法:
上面的方法做了两件事:
1、将图片数据InputStream流转换成Resource对象
2、调用notifyEncodeAndRelease将数据继续传递。
此时我们的图片数据由InputStream转换成了Resource对象。看看notifyEncodeAndRelease都做了写什么:
省略了与本文无关的代码之后,如上面所示我们来到了DecodeJob类的notifyComplete方法:
notifyComplete方法中我们见到了第三callback,并且调用其onResourceReady方法,那么这个 callback的具体实现类是什么呢?该对象通过观察源码发现是在初始化DecodeJob初始化的,而根据上文DecodeJob的初始化是在Engine 的load方法里面完成的,在该类初始化的时候我们还初始化了EngineJob这个类,这个类就是这个第三个callback的具体实现(见上文engine.load 的讲解),所以看看EngineJob的onResourceReady方法:
finally,见到了Handler的身影,说明我们距离终点不远了,上文的handler发送了MSG_COMPLETE消息该handler来处理,handler的初始化如下:
所以我们进入MainThreadcallback来看看MSG_COMPLETE消息的处理逻辑是什么,关于handler的具体工作流程参考博主《android消息处理机制详解》:
仅仅是调用了EngineJob的handleResultOnMainThread方法,此时我们已经将图片数据切换到了UI线程:
该方法做了两件事:
1、将resource交给engineResource来持有
2、将engineResource交给本文的
那么这第四个callback又是什么?是ResourceCallback接口,该接口是通过初始化EngineJob的时候调用EngineJob的 addCallback(ResourceCallback cb)方法传进来的。且EngineJob 的初始化是在Engine的load 方法中,而load 的方法的一个参数就是又这个callback, 且load的调用又是在文章开头的SingleReqeust方法,顺藤摸瓜发现第四个callback就是SingleReqeust,所以我们进入该SingleReqeust的onResourceReady方法:
如果你读过博主的《Glide工作总体执行流程概述》这篇文章的话,就可以知道此处的target就是DrawableImageViewTarget,该target的onResourceReady方法的最总逻辑就是调用:
来完成图片的展示。
到此为止,本篇分析完毕,分析源码的过程中四个callback来回调度可把我绕坏了,为了本篇博文的主题连续性,基本上省略了好多东西没讲,什么ModelLoader啦,LoadData啦,Fetcher啦,将在后续博文中讲解,敬请期待,如有不当之处,欢迎批评指正
本篇还是以SingleRequest为例来说明,在资源还没有加载好的时候,SingleRequest的begin方法会调用一个onSizeReady方法:
public void begin() { //省略部分代码 status = Status.WAITING_FOR_SIZE; if (Util.isValidDimensions(overrideWidth, overrideHeight)) { onSizeReady(overrideWidth, overrideHeight); } //省略部分代码 }
进入onSizeReady内部翻看一翻,仍然剔除暂时与本文无关的代码:
private Engine engine; public void onSizeReady(int width, int height) { //省略部分代码 status = Status.RUNNING; loadStatus = engine.load(glideContext,model,//url //省略一大堆参数); }
也就是说onSizeReady方法内部也就是主要时调用来Engine对象的load方法,那么这个engine对象是什么鬼呢?先看看这个对象是时候初始化的,该对象是在初始化Glide对象的时候进行来初始化,具体的可在GlideBuilder的build方法找到:
public Glide build(Context context) { //省略本分代码 //如果客户端没有配置自己的Engine if (engine == null) { engine = new Engine(memoryCache, diskCacheFactory, diskCacheExecutor, sourceExecutor, GlideExecutor.newUnlimitedSourceExecutor()); } return new Glide(context,engine,//省略一大堆参数); }
从Engine类的构造起所需要的参数来看,Engine负责内存缓存,磁盘缓存等等管理工作,这些内存和磁盘缓存等相关的类可以通过Builder模式让客户端自己构建自己的缓存逻辑,这也是Builder模式的强大之处,该模式也算是常用的设计模式之一,其设计理念还是很值得借鉴的,到此为止Engine的初始化已经创建完毕。下面继续沿着SingleRequest的流程分析engine.load方法:
//返回一个LoadStatus对象 public <R> LoadStatus load(//一大堆参数) { //1、生成一个EngineKey对象 EngineKey key = keyFactory.buildKey(model,//url signature, width, height, transformations, resourceClass, transcodeClass, //Drawable.class options); //2、从缓存加载图片资源 EngineResource<?> cached = loadFromCache(key, isMemoryCacheable); if (cached != null) { cb.onResourceReady(cached, DataSource.MEMORY_CACHE); return null; } //3、此处也可以理解为缓存逻辑 EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable); if (active != null) { cb.onResourceReady(active, DataSource.MEMORY_CACHE); return null; } //4、从map中获取一个EngineJob对象 EngineJob<?> current = jobs.get(key); if (current != null) { current.addCallback(cb); return new LoadStatus(cb, current); } //5、创建一个EngineJob对象 EngineJob<R> engineJob = engineJobFactory.build(key, isMemoryCacheable, useUnlimitedSourceExecutorPool); //6、创建一个DecodeJob对象 DecodeJob<R> decodeJob = decodeJobFactory.build( glideContext, model,//url key, //省略一大堆参数, transcodeClass, , engineJob); //将engineJob放入map缓存 jobs.put(key, engineJob); //添加一个callback engineJob.addCallback(cb); //8、开始engineJob engineJob.start(decodeJob); //返回一个LoadStatus对象 return new LoadStatus(cb, engineJob); }
load方法的代码很长,1至4是与缓存有关的逻辑,本篇暂时抛开不谈,在第五步的时候创建来一个EngineJob对象这是一个DecodeJob.Callback接口的实现类,后面会提到此处可以留意下,现在暂且可以理解为EngineJob是Engine这个引擎的最小工作单元。然后第六步又创建来一个DecodeJob对象(该对象是一个Runnable),第八步 engineJob.start(decodeJob);开始来工作,所以此处需要进入start方法探究一二:
public void start(DecodeJob<R> decodeJob) { this.decodeJob = decodeJob; //获取一个GlideExecutor对象,为ThreadPoolExecutor的子类 GlideExecutor executor = decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor(); //执行decodeJob这个ruanable executor.execute(decodeJob); }
EngineJob的start方法只是获取一个ThreadPoolExecutor对象执行DecodeJob这个Runnable而已(当然其内部还是比较复杂的),也就是说EngineJob只是一个壳子,其核心作用的还是DecodeJob这个对象。所以我们来看看DecodeJob这个Runalbe的run方法都做了些神马:
public void run() { DataFetcher<?> localFetcher = currentFetcher; try { if (isCancelled) { notifyFailed(); return; } //runWrapped方法是重点 runWrapped(); } catch (RuntimeException e) { //省略部分代码 } finally { //省略部分代码 } }
抛开run方法的枝枝蔓蔓,其内部调用来runWrapped方法,感觉距离真相越来越近了有木有,这是真的吗?:
private void runWrapped() { switch (runReason) { //本篇分析这个分支 case INITIALIZE: stage = getNextStage(Stage.INITIALIZE); currentGenerator = getNextGenerator(); runGenerators(); break; case SWITCH_TO_SOURCE_SERVICE: runGenerators(); break; case DECODE_DATA: decodeFromRetrievedData(); break; } }
上面有三个case分支,但是弱水三千只取一瓢,博主通过追踪内部逻辑准备只分析INITIALIZE分支,正如glide注释所说该分支INITIALIZE的意思是”The first time we’ve been submitted”,鉴于英语水平差,就不翻译了(事实上如果你打断点debug的话也会进入这个分支)。该case分支先调用了getNextGenerator方法,所以还是来看看该方法是干了啥牛逼的事儿,在分析runWrapped的时候有山重水复疑无路的感觉,现在感觉距离最终目的地尚有西天还有十万八千里呢!
private DataFetcherGenerator getNextGenerator() { switch (stage) { case RESOURCE_CACHE://缓存相关 return new ResourceCacheGenerator(decodeHelper, this); case DATA_CACHE://缓存相关 return new DataCacheGenerator(decodeHelper, this); case SOURCE://分析该分支 return new SourceGenerator(decodeHelper, this); case FINISHED: return null; } }
因为上文说过,本篇步分析与缓存有关的东西,只分析数据的最初来源(也就是服务器数据),所以本篇博文就分析SOURCE分支,该分支返回里一个SourceGenerator对象,该对象需要decodeHelper对象,decodeHelper对象是在初始化DecodeJob的时候对decodeHelper进行初始化的:
//初始化decodeHelper DecodeHelper<R> decodeHelper = new DecodeHelper<>(); DecodeJob<R> init( GlideContext glideContext, Object model,//url //省略了一大堆参数 ) { //调用decodeHelper的init方法初始化相关参数 decodeHelper.init( glideContext, model,//url //省略了一大堆参数 ); //省略了无关代码 return this; }
调用完getNextGenerator方法之后currentGenerator引用指向的对象就是SourceGenerator对象了,紧接着INITIALIZE的case分支又调用了runGenerators() 方法:
private void runGenerators() { boolean isStarted = false; while (!isCancelled && currentGenerator != null && !(isStarted = currentGenerator.startNext())) { stage = getNextStage(stage); //链式调用 currentGenerator = getNextGenerator(); if (stage == Stage.SOURCE) { reschedule(); return; } } //省略了部分代码 }
上面的方法中有一个while循环,while循环的条件题里面有一句:
//currentGenerator为SourceGenerator对象 isStarted = currentGenerator.startNext()
如果该方法执行返回了false,则满足进入循环的条件之一,所以我们看看SourceGenerator的startNext都多了些什么:
public boolean startNext() { //省略了缓存逻辑 loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { //1 、获取一个loadData对像获取LoadData对象 loadData = helper.getLoadData().get(loadDataListIndex++); //省略部分代码 //调用loadData的fetcher对象获取数据 loadData.fetcher.loadData(helper.getPriority(), this); } return started; }
上面的代码主要做了两个逻辑:
1、先通过getLoadData获取ModelLoder对象的集合,然后获取一个loadData对象
2、通过loadData对象的fetcher对象的loadData方法来获取图片数据。
其实这里简单打个断点,然后debug一下就知道LoadData对象和fetcher都是什么:
上面说的在这里先埋疑问:
ModeLoder 是什么?什么时候初始化的?
LoadData 有是什么时候初始化的?
Fetcher有是干什么,有是怎么初始化的?
以上三个问题将留在下篇博客说明,本篇为了保持主题不偏方向,先不谈。
这个LoadData是ModelLoader接口的一个内部类:
class LoadData<Data> { public final Key sourceKey; public final List<Key> alternateKeys; //真正获取数据的类 public final DataFetcher<Data> fetcher; }
loaddata 对象内部有一个DataFetcher,该类就是Glide实际获取数据的类,如上图我们得到的是一个HttpUrlFetcher 对象,所以我们直接进入HttpUrlFetcher对象的loadData方法看看:
public void loadData(Priority priority, DataCallback<? super InputStream> callback) { final InputStream result; //发起网络请求,讲URL对应的数据转换成InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0 , null ,glideUrl.getHeaders()); //将图片数据输入流交给callback处理 callback.onDataReady(result);//第一个callback }
loaddata方法是真正获取图片数据的地方:
1、调用loadDataWithRedirects方法,发起网络请求,讲URL指定的图片数据转换成InputStream输入流,该方法内部就是通过URLConnection对象来完成的(详细的读者可以自行到HttpUrlFetcher类的loadDataWithRedirects查看)。
2、将图片数据InputStream通过callback的onDataReady来传递,此处是我们遇到的第一个callback。
也就是说通过HttpUrlFecther处理之后,我们的图片数据源转换成来InputStream对象了。
那么上文中的callback 是什么呢?,该callback是一个DataCallback类型的接口(这是我们遇到的第一个callback,后面还有好多,希望读者不要绕晕了)。HttpUrlFecther的loadData方法的第二个参数就是一个DataCallback类型的参数,在SourceGenerator种我们传的是this,也就是说我们这个callback就是SourceGenerator对象,所以看看SourceGenerator对象的onDataReady方法都做了什么:
//第二个callback private final FetcherReadyCallback cb; //此处data就是InputStream public void onDataReady(Object data) { //省略缓存逻辑 //第二个callback cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); } }
剔除掉缓存的相关逻辑后,我们迎来了第二个callback,调用了该回调的onDataFetcherReady方法后,我们又将数据传给了这个callback。那么这个callback有是什么呢?该callback是FetcherReadyCallback的一个接口实现。那么这个callback 又是什么时候初始化的呢?当然是初始化SourceGenerator对象的时候,见上文我们是在DecodeJob这个类种初始化SourceGenerator的,DecodeJob就是这个callback的实现类,所以我们进入其onDataFetcherReady方法看看:
@Override public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher, DataSource dataSource, Key attemptedKey) { //将数据交给DecodeJob的currentData持有 this.currentData = data;//InputStream //省略部分代码 decodeFromRetrievedData(); } }
然后我们看看decodeFromRetrievedData方法:
private void decodeFromRetrievedData() { //1、将Inpustream转换成Resource对象 Resource<R> resource = resource = decodeFromData(currentFetcher, currentData, currentDataSource); //2、将数据继续传递 if (resource != null) { notifyEncodeAndRelease(resource, currentDataSource); } else { runGenerators(); } }
上面的方法做了两件事:
1、将图片数据InputStream流转换成Resource对象
2、调用notifyEncodeAndRelease将数据继续传递。
此时我们的图片数据由InputStream转换成了Resource对象。看看notifyEncodeAndRelease都做了写什么:
private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) { Resource<R> result = resource; //省略部分代码 notifyComplete(result, dataSource); //省略部分代码 } }
省略了与本文无关的代码之后,如上面所示我们来到了DecodeJob类的notifyComplete方法:
private Callback<R> callback; private void notifyComplete(Resource<R> resource, DataSource dataSource) { //省略一行代码 //第三个callback callback.onResourceReady(resource, dataSource); }
notifyComplete方法中我们见到了第三callback,并且调用其onResourceReady方法,那么这个 callback的具体实现类是什么呢?该对象通过观察源码发现是在初始化DecodeJob初始化的,而根据上文DecodeJob的初始化是在Engine 的load方法里面完成的,在该类初始化的时候我们还初始化了EngineJob这个类,这个类就是这个第三个callback的具体实现(见上文engine.load 的讲解),所以看看EngineJob的onResourceReady方法:
public void onResourceReady(Resource<R> resource, DataSource dataSource) { //原始数据 this.resource = resource; this.dataSource = dataSource; MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget(); }
finally,见到了Handler的身影,说明我们距离终点不远了,上文的handler发送了MSG_COMPLETE消息该handler来处理,handler的初始化如下:
private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper(), new MainThreadCallback());
所以我们进入MainThreadcallback来看看MSG_COMPLETE消息的处理逻辑是什么,关于handler的具体工作流程参考博主《android消息处理机制详解》:
//MainThreadcallback public boolean handleMessage(Message message) { EngineJob<?> job = (EngineJob<?>) message.obj; switch (message.what) { case MSG_COMPLETE: job.handleResultOnMainThread(); break; } return true; }
仅仅是调用了EngineJob的handleResultOnMainThread方法,此时我们已经将图片数据切换到了UI线程:
void handleResultOnMainThread() { //省略部分代码 //将数据转换成engineResource engineResource = engineResourceFactory.build(resource, isCacheable); hasResource = true; //省略部分代码 for (ResourceCallback cb : cbs) { if (!isInIgnoredCallbacks(cb)) { //特么的第四个callback cb.onResourceReady(engineResource, dataSource); } } }
该方法做了两件事:
1、将resource交给engineResource来持有
2、将engineResource交给本文的
**第四个callback**
那么这第四个callback又是什么?是ResourceCallback接口,该接口是通过初始化EngineJob的时候调用EngineJob的 addCallback(ResourceCallback cb)方法传进来的。且EngineJob 的初始化是在Engine的load 方法中,而load 的方法的一个参数就是又这个callback, 且load的调用又是在文章开头的SingleReqeust方法,顺藤摸瓜发现第四个callback就是SingleReqeust,所以我们进入该SingleReqeust的onResourceReady方法:
public void onResourceReady(Resource<?> resource, DataSource dataSource) { //调用重载方法: onResourceReady((Resource<R>) resource, (R) received, dataSource); } private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) { this.resource = resource; //最终调用target的onResourceReady target.onResourceReady(result, animation); }
如果你读过博主的《Glide工作总体执行流程概述》这篇文章的话,就可以知道此处的target就是DrawableImageViewTarget,该target的onResourceReady方法的最总逻辑就是调用:
imageView.setImageDrawable(resource);
来完成图片的展示。
到此为止,本篇分析完毕,分析源码的过程中四个callback来回调度可把我绕坏了,为了本篇博文的主题连续性,基本上省略了好多东西没讲,什么ModelLoader啦,LoadData啦,Fetcher啦,将在后续博文中讲解,敬请期待,如有不当之处,欢迎批评指正
相关文章推荐
- 关于listview中图片切圆,网络请求数据,并Gson解析后得到list
- SSS___listview多条目加载、使用okhttp请求网络数据,Glide加载图片
- Android使用OKHTTP网络框架请求数据,RecyclerView结合Glide展示图片,瀑布流布局样式
- 网络请求及数据解析(注意事项:带中文的需要转换) +从网络获取图片
- iOS基础8:自定义MyData/自定义SQLite用于网络判断,版本判断,图片缓存处理,下载或者上传的GET或POST请求,加密手段,.数据解析
- Kotlin新语言使用RxJava+Retrofit请求网络数据+lambda表达式+RecyclerView展示+Glide展示图片
- okhttp + RecycleView + Glide 请求网络数据及图片
- 解析接口中的json串网络图片数据,实现效果为无限轮播图+小圆点
- Android之三种网络请求解析数据
- AsyncHttpClient 网络请求+ fastJson解析数据
- ListView展示 网络请求的数据, 并LoaderImage请求网络图片
- Java从网络中请求获取JSon数据以及解析JSON数据----(自创,请注明)
- Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程
- Android开发-数据绑定-DataBinding-AndroidStudio(四)Glide解析图片
- Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程
- IOS 请求网络数据,解析
- MVP实现用Retrofit请求网络数据Fresco加载图片,Recyclerview CheckBox显示并实现全选删除
- Volley请求网络数据,设置图片(二)
- 简单新闻客户端(3)---网络数据请求,json包解析
- [置顶] pull解析请求网络的数据(带分页加载,刷新) xlistview HttpUtils