Android开源图片加载框架

同事写的一篇文章,mark下。

1. 开始

       本文主要介绍了android现下流行的图片加载库中的3个,通过读取源码,比较设计架构,来总结移动应用中图片加载的共同特点,同时比较这三个库的设计模式,博取众家之长,补己之短,第一篇文章,词法不好之处还请海涵,若有错误不到之处,还望指正!

2. App图片加载的通用策略

       Android应用中使用的图片来源不外乎这几种:本地资源drawable/assets/content/file,网络资源https/http。单纯从文件加载来说,android系统原生sdk已经提供了读取和解码的方法,但是这些加载任务需要和UI主线程并存,共享有限的硬件资源,为保证页面流畅性,通常UI线程享有最高的优先级,工作者线程的优先级总是比UI线程要低,所以图片加载策略在这方面会考虑得很细致,同时图片加载要尽可能节省无线流量,一方面是为用户流量费用考虑,另一方面也是考虑到系统性能问题。
       基于上面的设计思路,图片加载策略都会包含这些技术实现:两级缓存(内存/磁盘)、一级网络请求、线程池、降采样解码、重复请求合并等,下面用这张图来说明通用的加载策略流程:

       

       在Android应用中,将处理当前UI操作的线程称为主线程(MainThread),主线程的流畅性直接决定了用户对一款应用的打分,频繁的界面卡顿或者ANR(Application Not Response)就是因为主线程的负担太重(磁盘操作)或者线程被阻塞(网络请求),所以必须把费时的任务(Task)从主线程移出,放到后台线程去处理,其中磁盘文件读取和网络请求就是两大典型的任务。
       从图片加载的实例来看,主线程要为ImageView加载一张可显示的图片,通常都会提供一个URI,目标资源可能在内存、磁盘或者远程服务器,三者的时间消耗就如颜色的警示:内存<<磁盘<<网络。Android为了线程安全,规定UI操作必须在主线程内执行,内存Cache查询属于高效的操作也可以在主线程中运行,除此之外的其他操作都应该异步化,放在Worker线程去执行,这是保证UI流畅性的必然策略!Worker线程通常放在线程池中做统一管理,完成任务后,通过Handler通知主线程更新UI。
       以上是Android应用开发中图片加载的通用策略介绍,本文剩余部分将按顺序介绍3个图片加载库的具体设计方案,这3个库都是面向接口编程的,具备很好的扩展性,每个库都按照架构图、流程图、类关系图以及核心类解析的顺序进行说明。
  1. Volley: https://android.googlesource.com/platform/frameworks/volley
  2. UIL:https://github.com/dodola/Android-Universal-Image-Loader
  3. Picasso:http://square.github.io/picasso/

3. Volley

3.1 简介

       Volley 是 Google 推出的 Android 异步网络请求框架和图片加载框架。在 Google I/O 2013 大会上发布。

3.2 架构图

       

       Volley的设计架构非常清晰,以任务的不同阶段可以将Volley的设计框架分为以上四层,每个请求对象对应一项Task,Task通过全局单例RequestQueue对象加入调度线程的等待队列,调度线程从阻塞等待队列取得Request,根据目标数据的存储位置调用不同的请求数据接口,得到数据后通过实现ResponseDelivery接口的对象,把任务执行结果通知到主线程。

3.3 流程图


       Volley的调用接口非常简单,同名的Volley类提供了静态方法newRequestQueue(),用于返回一个任务管理对象RequestQueue,并在这个方法内完成了RequestQueue对象的所有初始化操作。
       RequestQueue对象聚合了1个Set和3个队列,这四个对象构成Request对象生命周期的中转站,作用不言而喻:
(1)mCurrentRuqests:保存当前正在进行或者等待的非重复请求(Uri不同);
(2)mWaitingQueue:合并相同uri对应的请求,在此等候;
(3)mCacheQueue:读取本地缓存的请求队列,缓存miss时将会加入mNetworkQueue;
(4)mNetworkQueue:需要网络连接的请求队列,请求不能缓存时从主线程直接加入,请求可缓存但缓存未命中时,由CacheDispatcher线程加入。
       mWaitingQueue是一个Map对象,用于缓存相同的请求,在请求处理完成后,相同请求通过调用RequestQueue的finish()方法被回调,这是volley处理相同请求的方式。
       Volley的Request处理方式使用的是生产者-消费者模式,这一步Volley实现了通用设计框架的任务异步化。Worker线程包含一个读本地缓存的线程CacheDispatcher,4个网络请求线程NetWorkDispatcher(使用默认配置时),CacheDispatcher作为RequestQueue的一个成员对象,NetWorkDispatcher[n]作为RequestQueue的成员数组对象,并都持有mNetworkQueue阻塞队列,NetWorkDispatcher线程作为队列的消费者,在这里Volley并没有选择使用线程池管理Worker线程,这5个Worker线程在队列为空时会阻塞等待。
       需要指出的是默认情况下Volley并没有实现内存缓存,在构造ReuqestQueue的时候只添加了DiskBasedCache这个磁盘缓存,这个类实现了Cache接口,为了快速查询磁盘中缓存的文件,这个类定义了CacheHeader这个内部类,并且定义了Map<string, cacheheader> mEntries = new LinkedHashMap<string, cacheheader>(16, .75f, true);成员对象,用于在内存中缓存磁盘文件的描述信息,mEntries可以认为是磁盘文件在内存中的索引,占用空间很小,这个设计值得借鉴。LinkedHashMap的accessOrder入参为true,开启LRU排序,每次缓存新对象前都会调用pruneIfNeeded()方法,预留足够的空间,Volley在这个地方又做了一个小优化策略,每次裁剪后的缓存空间是最大缓存空间(默认5M)的HYSTERESIS_FACTOR倍(默认=0.9),这样避免了在缓存空间满的情况下,加入新对象时每次都要进行prune操作。
       Volley缓存的磁盘文件内容是原始HttpResponse的ByteStream,也就是保存原始的数据,并且在DiskBasedCache中自定义了缓存文件的格式。
       请求处理完成后通过ExecutorDelivery对象post到主线程,在RequestQueue的构造方法中需要一个ResponseDelivery对象,默认传入的ExecutorDelivery对象持有向主线程发送消息的Handler,new ExecutorDelivery(new Handler(Looper.getMainLooper())),这是Volley实现通用设计框架的最后一步。

3.4 类关系图


       Volley的类关系图很简洁,所有的请求类都派生于Request类,默认提供了4个子类,数据请求都是面向接口编程(interface Cache / NetWork),Response的分发也是通过interface ResponseDelivery来定义,可以说是一个高度解耦的框架。

3.5 核心类

       从类关系图可以看出Volley包含四个核心类:
       (1)Request:所有请求都必须继承的抽象类,约定了请求处理流程中用到的所有必备接口方法,同时这个抽象类实现了Comparable<request>接口,可以对Request的优先级进行排序。
       (2)RequestQueue:Volley的对象管理核心,聚合了4个请求队列、1个CacheDispatcher对象、1个NetWorkDispatcher对象数组、引用实现了Cache/Network/ResponseDelivery接口的对象。通过RequestQueue.add(Request)方法添加请求到阻塞队列,可以认为是生产者,加入队列中的请求由消费者线程取走。
       (3)CacheDispatcher:继承于Thread类,本身代表了一个Worker线程类,从RequestQueue.mCacheQueue队列获取Request对象,如果缓存命中,就在自己线程内处理完请求,否则将请求移交给网络请求队列RequestQueue.mNetworkQueue,相当于网络请求队列的生产者。
       (4)NetworkDispatcher:继承于Thread类,作为Worker线程类,在Volley中作为RequestQueue.mNetworkQueue队列的消费者,负责网络请求的具体实现。

3.6 思考  

       (1)Volley面向接口编程,各个模块间高度解耦,代码组织上也很清晰,在此基础上用户可以很容易地将其定制为符合自己需要的lib
       (2)Volley能够加载任意的对象资源,不仅仅是图片,作为一个通用的框架,没有为图片加载过程的细节做太多的考虑,比如没有留图片预处理和后处理的操作接口、列表滚动时不能暂停网络请求、缺少图片的内存Cache、不能设置加载前的占位图,有这些需求要自己添加改造,所以使用Volley作为图片加载库的功能还不够完善。

4. UIL

4.1 简介

       UIL(Android Universal Image Loader)为App提供异步图片加载、缓存和显示的解决方案,支持Android2.0+。

4.2 架构图

       

       UIL通过ImageLoader对外提供两个图片加载的调用方法,图片加载的请求根据是否命中MemeoryCache,被封装为两种任务(实现Rnnable接口),ImageLoader将任务提交给ImageLoaderEngine的线程池开始处理,封装好的任务流程调用数据请求接口,得到Bitmap对象后,通过Handler通知UI主线程。

4.3 流程图




       UIL是专门下载图片的Lib,不提供下载其他数据的接口(不同于Volley),几乎支持所有的图片的来源,这是UIL代码中枚举出的可支持的图片来源scheme:HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("")
       使用UIL需要通过ImageLoaderConfiguration对象对ImageLoader进行初始化,初始化项包括:内存Cache、磁盘Cache、ImageDownloader、ImageDecoder,3个后台线程池。最顶层的ImageLoader被设计为全局单例,通过init(ImageLoaderConfiguration)方法接收配置参数,同时在内部构造了ImageLoaderEngine对象。
       UIL为用户提供了两个图片加载方法:void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener)是通用的图片加载方法,也就是上面主流程的入口,loadImage不接收ImageView入参,用于加载窗口小部件的图片,最终也是调用displayImage方法加载图片。图片加载过程的特定步骤可以通过入参ImageLoadingListener得到通知。
       UIL对图片加载可能用到的处理方法都预留了接口,预处理PreProcessor在加入内存缓存前执行,后处理PostProcessor在加入内存缓存后执行,所以如果要对图片做统一的处理,可以添加PreProcessor对象,特定处理可以添加PostProcessor对象(比如旋转、缩放)。磁盘中保存的是压缩后的原始图片,这些处理任务都是在工作者线程中完成的。
       实际任务的处理流程被封装为:ProcessAndDisplayImageTask和LoadAndDisplayImageTask这两个任务,第一个任务在内存Cache命中时处理PostPrecessor,再通过Handler.post(DisplayBitmapTask·)分发出去,任务很轻;第二个任务包含内存Cache Miss后的全部任务流程:调用Decoder从本地磁盘加载图片,失败时调用ImageDownloader从远程下载图片,处理PreProcessor和PostProcessor,写DiskCache和MemoryCache,回调ImageLoadingListener,最后把任务处理结果通过Handler.post(DisplayBitmapTask)分发出去。
       在Volley中我们说明了相同请求的处理方法,使用的是Map<string, queue<request<?>>> mWaitingQueue;这个结构,在UIL中使用的则是ReentranceLock锁,每个锁实例通过Map<string, reentrantlock> uriLocks与uri关联,相同的uri被映射到同一个锁实例,在第一个任务启动后,锁即被Lock,相同的uri只能阻塞等待,直到第一个任务将锁Unlock,等待锁的任务被唤醒后,会先查询一次MemoryCache,如果前一个任务成功写了MemoryCache,这个重复的任务可以直接命中MemoryCache。Uri不同的任务照常执行,这里抢占锁的几率比较低,所以使用锁的性能消耗不大。
       流程图的下半部分有多个红色的圆圈,这是LoadAndDisplayImageTask任务在执行过程中检查是否要取消当前任务的测试点,任务取消的来源可以是:(1) ImageView重用绑定了另一个uri;(2) 线程被interrupted中断。
       ImageLoaderEngine提供了resume ()和 pause ()方法来启动或暂停引擎任务,在LoadAndDisplayImageTask入口的地方同步了engine.paused对象,所以未启动的任务将被阻塞,已启动的任务继续执行。为此UIL还提供了PauseOnScrollListener接口,用于监听滚动列表的状态变化事件,在进入OnScrollListener.SCROLL_STATE_FLING状态时,调用pause()停止任务,在进入OnScrollListener.SCROLL_STATE_IDLE状态时,调用resume()启动任务。

4.4 类关系图

950
       UIL对图片加载流程的各个细节和步骤考虑得很细致,对应提供的类也比较多,所有被聚合的类成员对象都是通过接口定义的,从类关系图可以清晰地看到各个功能模块之间的依赖关系,通过接口定义将模块间的耦合度降到最低。

4.5 核心类

       (1)ImageLoader:是universalimageloader.core包下的类,也是UIL最顶层的核心类,在整个应用上下文中是单例,对用户提供两个调用方法:void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener)和void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options, ImageLoadingListener listener),接口使用极其简单,内部组合了1个ImageLoaderEngine对象,负责执行实际的图片加载任务。
       (2)ImageLoaderConfiguration:为ImageLoader提供配置参数,配置项包括内存Cache、磁盘Cache、ImageDownloader、ImageDecoder和FileNameGenerator等核心模块,查看UIL提供的源码,应用启动时在UILApplication.onCreate()中通过建造者模式构造了这个对象,作为ImageLoader核心类的默认配置,从类关系图可以看到ImageLoaderConfiguration聚合的核心模块都是通过接口定义的,模块之间高度解耦,并且可定制。
       (3)ImageLoaderEngine:UIL的图片加载引擎,称之为引擎是因为这个类提供了3个完成Task任务的线程池对象。taskExecutor是负责从远程网络加载图片的线程池对象,默认大小为3;taskExecutorForCachedImages负责从本地缓存加载图片,默认大小也是3; taskDistributor是向taskExecutor和taskExecutorForCachedImages线程池分发Task任务的线程池,其工作线程不会阻塞,所以池的大小没有限制,其实用单线程来代替也是完全可以的。
       (4)LoadAndDisplayImageTask:定义具体图片加载任务的类,负责从磁盘缓存或远程网络加载图片数据,并写入MemCache和DiskCache,图片加载完成后通过handler.post(DisplayBitmapTask)通知主线程,使用到的加载工具对象都来源于内部持有的ImageLoaderConfiguration对象,与初始化ImageLoader的配置对象是同一个,具体的任务执行流程请看UIL流程图部分。
       (5)BaseImageDownloader的网络请求实现基于HttpUrlConnection,HttpClientImageDownloader继承于BaseImageDownloader,重写了基类的getStreamFromNetwork()方法,使用org.apache.http.client.HttpClient取而代之

4.6 思考

       (1)UIL同样基于接口聚合各个功能模块,模块之间解耦可定制;
       (2)UIL从网络加载Bitmap到本地后,先根据压缩格式写入了磁盘,然后把这个Bitmap recycle()掉,接着又从磁盘decode出这个url对应的图片,所以UIL在从远程网络加载图片时多了一次从磁盘decode图片的操作。
        (我对于第二点的理解有偏差,请看14F笔歌的说明,下面第(3)点是根据笔歌的建议,避免误导他人,我重新纠正论述一下)
      (3)UIL从网络加载图片的动作可以分两步:第一步从网络加载图片时,进行的降采样decode实际上是根据全局配置对象ImageLoaderConfiguration 中设置的 maxImageWidthForDiscCache 和 maxImageHeightForDiscCache 这两个变量限制的,得到的图片压缩后存入磁盘;接着从磁盘再次decode图片时,是根据实际view的尺寸加载的,bitmap会小很多。所以对于同一个uri,在磁盘中只对应一张图片,并且这张图片在下载时会尽可能细致,而decode到内存时是按需加载,所以内存中会有这张图片不同size的版本,内存Cache的memoryCacheKey为了保存这些不同size的图片,会在key的末尾加后缀,格式为:" uri_WxH "如 " uri_128x256 "," uri_256x512 "。UIL这么设计是为了各个分辨率的图片能有最佳的显示效果,否则下载小图再放大质量就太差了。

5. Picasso

5.1 简介

       Picasso是Square公司的开源项目,是一款强大的图片加载和缓存库。

5.2 架构图


       

       Picasso下载图片的参数使用Builder模式进行配置,所以写成一句话就可以完成图片加载;请求被封装为Action,并提供RequestHandler的实现类对不同Action进行处理;任务的调度使用Dispatcher对象进行调度,耗时的加载流程放在后台线程池执行;数据接口包含内存Cache、DownLoader,磁盘Cache使用Http缓存,完成任务时通过Handler通知UI主线程。

5.3 流程图



       Picasso的图片加载流程可以根据任务所在的线程来区分不同阶段。
        首先是Picasso的初始化,调用Picasso.with(Context)即可,第一次调用会使用Picasso.Builder构造一个全局的单例对象,在构造过程new了一个Dispatcher对象,这个对象很重要,作为Picasso的任务调度器,将Action从UI主线程调度到Worker线程,任务完成后又从Worker线程调度回UI主线程。启动图片加载任务使用Picasso.with(ApplicationContext).load(imageUrl).centerCrop().into(ImageView); 一句话完成指定url图片的定制和加载,接口再简单不过了,详细使用方法请查看官网说明:http://square.github.io/picasso/
       这里需要重点说明Dispatcher对象的任务调度原理,Dispatcher对象内部持有一个线程类对象:class DispatcherThread extends HandlerThread,可以看到DispatcherHandler继承于Android系统的HandlerThread,因此可以利用这个线程的Handler把各种消息post到Looper上,Looper在收到消息时,又会主动回调Handler的handleMessage方法,根据消息ID区分不同的消息,这是Picasso任务调度的设计方案,显得非常优雅。从上面的流程图可以看到Dispatcher线程在UI主线程和Worker线程之间起到了桥接的作用。Volley和UIL直接使用生产者-消费者模式(PriorityBlockingQueue)将请求任务异步化。在任务执行完毕后,这三个框架都是使用主线程的Handler把处理结果post到主线程,这种做法也是Android异步线程和主线程通信的标准做法。
       Picasso有一些很好的细节设计,需要说明一下,请看流程图中红色①~⑤标记的位置:
       (1)DeferredRequestCreator是Request请求的延迟构造器,这个类实现了ViewTreeObserver.OnPreDrawListener接口,这个接口中的方法在Android对ViewTree调用完layout()之后调用,那这个类存在的意义显然和android的视图绘制机制有关。对于一个View,如果它的布局参数要依赖于其他View,那么系统在对视图树调用layout()之前,这个view的宽高是未知的。由于Picasso在图片加载时要对图片计算降采样率,View的宽高不确定,对于Picasso来说就是不知道应该加载什么样的图片最合适,所以有了这个延迟构造器。
       (2)在UIL中提到了PauseOnScrollListener接口,用于在滚动列表Fling时停止引擎的加载任务,Picasso同样提供了这个功能,请看流程图中②的位置,Tag标识的请求被暂停时,请求直接进入pauseActions等待,Picasso.resumeTag(Object)和Picasso.pauseTag(Object)就是用于唤醒和停止请求任务的API,这里的Object是全局唯一的应用上下文。源码的示例也提供了SampleScrollListener来演示这个功能。
       (3)在Volley和UIL中都提到了重复请求的处理方法:Volley使用mWaitingQueue,UIL使用了ReentranceLock,这里请看流程图中③的位置, Picasso策略和Volley相同:先使用Action的请求参数计算requestKey,作为Action的唯一标识,requestKey相同的Action将被放入BitmapHunter.actions列表中,在任务执行完毕后,一并得到回调通知,通知动作发生在流程图中⑥的位置。
       (4)流程图中④的位置对得到的bitmap进行了变换操作,这里会开辟新的内存用于缓存中间结果的图片,如果多个后台线程同时执行这一步,很可能导致应用OOM而挂掉,所以Picasso借鉴了Volley在解析图片时的保守策略(详见Volley的ImageRequest.parseNetworkResponse()方法),使用synchronized (DECODE_LOCK)同步了一个静态类成员,同一时刻只允许一个线程进行这个操作,避免潜在的OOM bug。这个设计来源在BitmapHunter.java类文件中DECODE_LOCK变量定义的位置是有注释说明的。
       (5)Picasso的任务处理结果是按批次分发出去的,这个动作发生在流程图中⑤的位置,操作流程是这样的:设置任务完成的Messag要延迟200ms(默认时长)后再发送出去,同时这个任务会被加入Dispatcher.batch的这个成员对象中,如果在接下去的200ms内有新完成的任务到达,也会被加入Dispatcher.batch成员对象中,等200ms延时到达时,将batch统一发送到主线程,然后清空batch,用于下一批的Message的容器。
       (6)Volley和UIL都对请求进行优先级排序,Picasso也有相同的功能实现,将BitmapHunter任务包装到PicassoFutureTask中,并实现Comparable接口,class PicassoFutureTask extends FutureTask implements Comparable。Picasso提供了LOW/NORMAL/HIGH三个任务优先级,线程池使用PriorityBlockingQueue,所以优先执行高优先级的任务。
        最后再说一下Picasso的磁盘缓存,Picasso使用OkHttpClient和HttpUrlConnection作为Downloader接口的实现,并且优先使用OkHttpClient,Picasso会默认开启Client的Response缓存,所以不像UIL那样专门提供了DiskCacheAware的磁盘缓存接口。

5.4 类关系图



       Picasso的类设计大量使用了组合模式,总体架构设计得很巧妙,对于内存Cache和Downloader也是通过接口聚合,这一点和Volley/UIL是相同的。

5.5 核心类

       (1)Picasso:与Lib同名的Picasso类作为最上层的管理类,聚合了lib运行时必须的功能模块,包括Dispatcher、内存Cache、RequestHandler列表等,同时对用户提供简洁的调用接口。
       (2)Dispatcher:Picasso的任务调度类,提供调度任务的接口,通过内部组合的DispatcherThread线程类对象在UI线程和Worker线程间传递任务,内部的调度流程对用户是透明的。
       (3)Action:包装Request请求的抽象类,同时提供请求处理成功或失败时使用的资源,子类必须实现complete()和error()抽象方法,作为任务处理结果的回调。
       (4)RequestHandler:处理异步图片加载任务的抽象类,子类必须实现canHandlerRequest(Request)和load(Request,int)两个抽象方法,前者判断这个RequestHandler能否处理入参Request,后者执行实际的Request请求。Picasso默认提供了7种RequestHandler,如果用户有其他类型的Handler,可以通过Picasso.Builder. addRequestHandler(RequestHandler)进行注册。新加入的RequestHandler从List requestHandlers的第1个下标位置开始放,Picasso遍历调用每个已注册的RequestHandler.canHandlerRequest()方法,找到第一个可以处理Request的RequestHandler即返回,所以能够优先使用用户自定义的RequestHandler。
       (5)BitmapHunter:封装异步任务的具体处理流程,该类实现了Runnable接口,每个BitmapHunter实例对应一个独立不重复的请求(指requestKey独立不相同),任务流程在Worker线程执行,负责从磁盘或远程网络加载图片。

5.6 思考

       (1)Picasso写得很专业,每个功能都有详细的测试用例,理清Picasso的设计思路,其中的细节值得参考借鉴。

6. 总结比较

  Volley UIL Picasso
初始化 Volley.newRequestQueue() ImageLoader.getInstance().
init(ImageLoaderConfiguration)
Picasso.with(Context)
调用接口 RequestQueue.add(Request<T>)

ImageLoader.displayImage(Url, ImageView);
ImageLoader.loadImage(Url, ImageLoadingListener);

Picasso.with(Context).load(url)
.placeHolder().
error(R.drawable.error).into();          
多类型请求 通过构造不同的Request子类实现 只支持图片资源:http, https, 
file,content, assets, 
drawable
通过提供不同的Action子类和RequestHandler子类实现
支持的资源 远程Json、Image、String 远程和本地的Image 远程和本地的Image
重复请求 使用Map<String, Queue<Request<?>>>
mWaitingQueue去重
使用ReentranceLock去重 使用BitmapHunter类的List<Action> actions去重
列表滚动停止加载 Volley本身不提供支持方法 ImageLoaderEngine提供
resume()和pause()方法支持
Picasso提供resumeTag(Object)和PauseTag(Object)方法支持
降采样加载解码 完整读取远程图片的字节流,
在本地进行降采样解码
远程读取的图片尽可能清晰(见4.6(3)的说明)
压缩后保存到本地,
之后根据目标view降采样解码出不同size的图片
远程和本地都支持降采样解码

       Volley是Google官方提供的通用IO框架,并不针对每个细节进行优化,但是总体的框架值得学习;UIL的设计针对客户端图片加载,对各类图片来源都支持,框架的各个模块通过接口定义,耦合度底,使用时对不需要的功能可以进行裁剪;Picasso也是针对客户端图片加载,框架设计的很巧妙,并且每个功能包含详细的测试用例,功能也很完善。要从这三个图片加载框架选择,还是推荐Picasso,这三个Lib只是学习用,当然还有其他的选择。

       注:Volley的架构图、类关系图和UIL的架构图绘制,参考了  http://codekk.com/open-source-project-analysis 上的两篇文章,这两篇文章对Volley和UIL进行了非常详细的介绍,细致到每个方法变量,可以对照源码细读。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值