Picasso是如何实现异步加载图片的?
图片加载的需求在Android中是非常常见的需求,同时由于Bitmap是内存大户,所以如何高效的加载图片是一个非常重要的问题。目前来说,UIL、Glide、Picasso、Fresco等优秀的图片加载框架都可以满足我们的需求。本文以开源图片加载库Picasso为例,介绍图片加载库的通用设计框架及部分细节处理。
Picasso简单使用
在介绍整体架构之前,我们先看一下使用Picasso如何完成一张图片的加载。
最简单情况下,使用一句代码即可完成一张图图片的加载过程,包括发起网络请求、处理返回值、对文件进行本地硬盘缓存和内存缓存、维护线程池等操纵。
Picasso.with(context).load(URL).into(imageView);下面我讲从源代码的角度,讲解这一行代码的底层到底做了哪些操作,以便于我们对通用图片加载框架有比较完整的了解。
Picasso的初始化操作
Picasso是面向开发者的一个接口,这个接口的意思并非是Interface,而是指如果想完成图片加载工作,我们就需要和Picasso打交道。你还记得前面我说过的Activity吗?Activity也是一个面向开发者的接口,我们用Activity来完成界面的显示、交互、销毁功能,这里我们就用Picasso完成图片加载的功能,是一样的思想。
好了,我们要从Picasso开始入手了!
当调用Picasso.with(context)的时候,系统会完成Picasso对象的实例化操作。
static volatile Picasso singleton = null;
public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}在这里,使用了线程安全的单例模式完成了Picasso的初始化,同时,由于Picasso的初始化过程可以自定义很多参数,所以使用了建造者模式完成Picasso对象的构建。
使用这种方式会调用默认的参数来构造:
下载器downloader的设置策略为:若当前项目中依赖okhttp,则使用OkHttpDownloader,否则使用HttpURLConnection实现的UrlConnectionDownloader,这一点从Utils.createDefaultDownloader(context)的实现过程可以看出来。
内存缓存cache的默认实现是LruCache,关于LruCache的具体实现将在内存管理机制一章中详细介绍。
线程池service的默认实现为PicassoExecutorService,它继承自ThreadPoolExecutor,主要添加了根据当前设备的网络状况调整线程池核心线程数的功能。关于这一功能点的实现机制我将在下面章节中介绍。
请求转换器transformer的默认实现并未对Request进行任何处理,如果有需求的话,我么可以自定义转换器,然后对Request统一进行处理,比如对请求连接地址进行统一处理等等。
除此之外,Stats负责统计信息,如cache命中率、丢失率、下载文件总大小、总数量等参数,任务分发器Dispatcher负责任务的分发等工作,这几个类的具体功能和实现将在下面介绍。
至此,Picasso单例的初始化动作算是完成了,下面可以使用中Picasso发起请求,完成图片的加载工作了。
创建图片加载请求
前文说到,使用Picasso.with(context)完成了单例对象的创建,接下来就应该使用Picasso.load(URL)发起图片加载请求了。
Picasso
通过Picasso.load()可以创建RequestCreator对象,RequestCreator负责完成请求的发起工作。这里需要注意的一点是,请求地址Path可以为null,这个时候不会发起任何请求,如果设置占位图片的话,会显示占位图片,但是Path的内容不能为空,这会引发Crash,这一点需要特别注意,调用之前需要对Path进行判断以免引发Crash。
可以通过以下方式设置占位图片
通过Picasso.load(URL)完成RequestCreator的创建,在构造函数中会完成以下操作:
在这个过程中,又完成了Request.Builder实例的创建,Request.Builder用于接受Request的自定义参数,完成Request的创建。
这里需要明确的一点是,每次发起一个图片请求,都会创建一个RequestCreator。
实际上,当获取到RequestCreator对象之后,我们可以通过链式调用自定义很多的参数,包括设置tag,设置图片对齐模式,设置加载出错图片,设置占位图片,旋转,自定义大小,图片自定义处理等操作。

那么怎么构造这么复杂的对象呢?是的,建造者模式。实际上,在设置这些参数的时候,改变的都是Request.Builder对象,这样在创建Request的时候,就可以将这些自定义参数传递过去。
为了实现链式调用,在设置完参数之后,都将当前对象作为返回值返回。
Ok,现在我们已经把所有的参数都设置好了,接下来该做什么了?是的,创建请求,然后将请求发送出去。
使用RequestCreator发送图片请求有两种方式,这两种方式用于不同场景。
RequestCreator.into(target),通过这种方式可以直接完成图片在target上的显示,最常用的也是这种用法
RequestCreator.fetch(),通过这种方式可以完成图片的预加载工作,即将目标图片下载至本地,并加入到内存缓存中,这样当下次使用上面的方式加载相同图片时,会直接从内存缓存中获取,极大的提高加载速度
下面将介绍使用第一种方式发送Request的流程。
这里需要重点看的是,Request的生成过程。
至此,一个图片请求被封装成了ImageViewAction,添加到了Picasso的请求队列中。
Picasso如何完成请求的分发
在上文中,通过Picasso.enqueueAndSubmit()完成了ImageViewAction到线程池的添加:
在Picasso.enqueueAndSubmit()中,会首先对请求进行检测,如果有相同的请求的话,就会取消已经存在的请求,并把新请求添加到targetToAction中,targetToAction保存的是target与Action之间的映射,根据target来区分是否是同一个请求。如果target是ImageView类型,还要从targetToDeferredRequestCreator中去除对应数据,targetToDeferredRequestCreator保存的是延时任务,即设置fit()属性的任务。
将这一切的准备工作完成之后,就利用Picasso.submit(action)提交任务了。
还记得前面说过的Diapatcher的作用吗?Diapatcher是用来完成任务分发的,下面重点看一下任务分发的流程。
首先Diapatch是Picasso的成员变量,由于Picasso是单例模式,因此dispatch也只会存在一个实例,默认的dispatch是在Picasso单例初始化的时候实例化的,而且Picasso不支持Diapatch的自定义,只能使用默认实现。
从Dispatcher构造函数的参数来看,Diapatcher的功能应该是很强大的,因为与很重要的几个模块都扯上了关系,比如线程池,Handler,网络下载器,内存缓存,状态统计等。
Dispatcher在构造时确实完成了很多的工作。
Ok,这里可能有些成员变量不太好理解作用,不过没关系,在下面的流程中将会重点介绍。我们看一下dispatcher.dispatchSubmit(action)之后的操作。
有一个问题值得我们思考,这里为什么要用Handler呢?
要回答这个问题,我们需要先弄明白:handler是与哪个线程绑定的?
从代码中可以很容易得到答案,handler是和dispatcherThread绑定的,消息会在dispatcherThread进行处理。
dispatcherThread是什么?就是一个简单的HandlerThread:
那么为什么这么做呢?我觉得有两个作用:
将消息从主线程发送至工作线程,可以不影响主线程消息队列的处理速度
Diapatcher的工作不需要和UI打交道,这样可以减缓主线程的压力
通过Handler从主线程发送消息至工作线程,这也是Handler的一个重要用法,需要重点掌握。
Ok,现在下面的工作都是在工作线程DispatcherThread中执行了。
从上面的流程中,我们可以很清楚的的看到一个任务是如何被添加到线程池的,但是这里还有两个疑问:
BitmapHunter是什么?有什么作用?
任务是如何被分发到合适的处理器的?由谁来决定从哪个渠道进行加载?比如资源、本地硬盘、Asses文件夹、网络等。
下面我来回答这两个问题。
首先BitmapHunter实现了Runnable,当他被扔进线程池之后,就会有线程来找他,并运行run()。BitmapHunter的主要作用就是从对应渠道获取到Bitmap。
至于任务是如何被分发给合适的处理器,这个过程也比较简单,在获取BitmapHunter对象的时候,是通过BitmapHunter.forRequest()来获取的,下面看下代码实现。
BitmapHunter#forRequest
通过遍历RequestHandler集合来创建合适的BitmapHunter,既然是遍历,那就肯定有先后顺序,这个是靠Picasso初始化的时候确定的。
从上面这些代码我们可以获取到非常多的信息,也可以看出Picasso在实现细节上处理上也非常优雅,从数据结构的选择、便利方式的选择、集合最大长度的设定、集合不可变的处理上都非常值得我们学习。
同时,在上面的代码里面我们可以看出,Picasso支持非常多途径的图片加载,包括:
系统内资源
联系人头像
媒体库
ContentResolver
Asset
本地文件
网络
自定义渠道
那么如何判定一个请求应该由谁进行处理呢?所有的处理器都继承子RequestHandler,并且重写了下面的方法
因此我们只需要根据每种处理器的方法实现即可,不可能列出所有的代码,我只给出个别的实现,其他类似。
NetworkRequestHandler负责处理网络请求,所以它对uri的schem进行了处理:
联系人头像使用的是ContentProvider获取,所以处理方式如下:
经过以上操作,一个包含有合适的RequestHandler的BitmapHunter就创建完毕了,接下来,就通过service.submit(hunter)被无情的扔进了线程池!
线程池的那些猫腻
Picasso的线程池在初始化的时候就设定好了,而且全局只有一个线程池,实现类是PicassoExecutorService,下面说一下Picasso在线程池上的处理。
PicassoExecutorService的处理非常简单,默认的核心线程数为3,最大线程数也为3,这也就决定了如果发起的请求多余3个的话,剩下的请求就会被缓存在PriorityBlockingQueue里面。
关于核心线程数的处理上有一点值得思考,前面说过,AsyncTask也是有一个全局的线程池,在核心线程数的选择上是根据CPU个数动态变化的,个人觉得AsyncTask的处理上更灵活一些,在现在多核处理器满大街的情况下,充分的利用这个优势可以提高执行效率。
虽然Picasso没有提供根据CPU数量动态改变核心数,不过添加了根据当前网络状态改变CPU核心数的功能。
那么在不同网络情况下改变核心线程数有什么作用呢?个人认为,随着网络情况越来越差,采用更小的核心线程数,可以更快的显示目标图片,毕竟在相同网速下,一个请求的加载速度肯定比多个请求的加载速度快。而在WIFI情况下,网速的限制比较小,多个网络请求可以带来更好的用户体验。
还有一个问题你想过吗:如何实现请求的优先级排序?考虑这样一个情景,如果滑动ListView,你最想哪一些图片先加载呢?当然是最后添加的请求先加载,因为这代表着用户当前关注的内容,所以这就需要维护一个根据优先级排序的集合,Picasso是如何实现的呢?
如果你有仔细看PicassoExecutorService的构造函数,就会发现缓存队列使用的是PriorityBlockingQueue这个数据结构,PriorityBlockingQueue的特点就是当元素添加的时候,会根据Comparable.compareTo()进行优先级排序。
但是添加到线程池的BitmapHunter并没有实现这个接口呀!
这是因为在PicassoExecutorService.submit()中进行了处理
PicassoExecutorService
Priority属性在生成Request的时候被指定,一共有三个等级:
Picasso
当使用fetch()发送任务时,默认等级为LOW,其他情况下默认优先级为NORMAL,如果你想指定一个请求的优先级,可以这样指定:
但是这样指定的是总体上的优先级,线程集合中会存在很多相同优先级的请求,这个时候会根据sequence来确定最终的优先级。
而sequence的赋值发生在实例化BitmapHunter的过程中。
SEQUENCE_GENERATOR是一个线程安全的Integer加减类,所以我们可以得出结论,如果两个请求的优先级相同,那么后创建的任务优先级会更高。
这个结论也验证了我前面提出的猜测,即最后添加的任务最先执行可以提供更好的用户体验。
扯得有点远了,下面再看一下BitmapHunter被添加到线程池之后的run()到底做了什么。
网络加载图片是如何完成的?
在BitmapHunter.run()中完成了图片的获取操作。
主要的工作都在hunt()完成,"hunt"意为搜寻、打猎,我们要开始打猎Bitmap了!
加载的动作发生在RequestHandler.load(),不同的处理器有不同的实现方式。下面看一下网络获取器的实现。
到此为止,一张图片已经加载完毕,等待显示了。
Picasso是如何实现硬盘缓存的?
一般来说,图片加载框架都会有硬盘缓存,来减少流量消耗和加快加载速度,但是自始至终,我们都没有发现硬盘缓存的影子,那么Picasso是如何完成的硬盘缓存呢?
答案就在下面这段代码中。
图片资源的下载和本硬盘缓存都在这里完成。
UrlConnectionDownloader
通过上面的源码可以知道,Picasso是通过HTML的Cache语义来实现本地缓存的。如果需要使用HttpUrlConnection实现缓存,运行版本必须在14及以上,因为HttpResponseCache是新添加的Cache类,而如果使用OkHttp则没有这个问题。
Bitmap终于要显示出来了!
在BitmapHunter.run()中,在加载成功后,会调用Diapatcher.dispatchComplete(BitmapHunter)完成最后的回调。
这里有一个小地方需要注意,batch()操作的功能是每隔200ms批量发送一次消息至主线程。
batch的默认长度为4,猜测这是经过测试得到的最大值,即200ms内最多完成的任务是4个,可能没有科学依据,但是实践出真知。
每隔200ms,就会将一批次最多4个BitmapHunter发送至主线程Handler。
在主线程Handler中的处理则比较简单:
在Picasso.complete()中完成了图片的最终显示。
通过Picasso.deliverAction()会将结果分发至某一个Action,完成图片设置。
这里以ImageViewAction为例:
很奇怪,最后没有直接设置ImageView,而是交给了PicassoDrawable,这个类是做什么的?
PicassoDrawable是什么?
这要涉及到Picasso的一个小功能,使用下面的方式,可以在每张图片的左上角显示一个角标。
红色表示网络加载,蓝色表示硬盘加载,绿色表示内存加载,使用这一个特性可以很方便的查看每张图片的加载途径。
那么这个功能是如何实现的呢?关键就在于PicassoDrawable,PicassoDrawable继承自BitmapDrawable,并重写onDraw()。
实现方式非常简单,就是画了两个Path,第二个Path小1dp,这样就出现了下图的效果。

守护线程
Picasso内部还维护着一个守护线程,负责取消已经被GC回收的Target对应的请求。
首先这里用到了一个非常有用的数据结构ReferenceQueue,在生成Action的时候,Picasso在内部进行了封装:
我们可以发现,RequestWeakReference中连接着Action和Target,在实例化WeakRefrence时,如果传入ReferenceQueue,那么这个弱引用被GC回收️时,会被添加进ReferenceQueue,因此不断轮训可以获取到被GC的对象,在Handler中取消对应的任务。
总结
我们总算是将Picasso比较详细的学习了一遍,现在是总结时间,有总结才能有提高~
首先我们思考一下,一个通用的图片加载库应该有哪几部分组成?
我感觉至少有以下几部分:
Manager,指的就是Picasso,负责请求的发起、暂停、恢复、取消等操作
ThreadPool,指的就是PicassoExecutorService,负责线程数量的维护,合理的设置线程池参数会很大的影响到一个框架的整体实力
Diapatcher,指的就是分发器,负责任务的下发和管理,把一个BitmapHunter交给合适的RequestHandler
RequestHandler,指的就是处理器,不同的处理器有不同的加载策略,满足不同的需求
Cache,包括本地硬盘缓存和内存缓存,负责提高同一个请求的响应速度
Displayer,即显示器,这里指的就是PicassoDrawable或者说是ImageView
Ok,现在咱们已经有了这几部分了,那么整个图片加载流程也就很清楚了:使用Picasso发起一个图片加载请求,通过RequestCreator来控制Request的参数,比如设置错误图片、占位图片、优先级等,然后根据这些参数创建对应的Action,并且将Action添加到Dispatcher中等待分发,Dispatcher会根据Action的uri来选择不同的RequestHandler创建BitmapHunter,在这之后一个包含着各种参数的BitmapHunter就被无情的扔进了线程池中,然后会在BitmapHunter.run()中根据不同的处理器来获取数据,最后又将结果通过Dispatcher和Handler传递到UI线程,将PicassoDrawable设置给ImageView,最终完成图片的显示工作。
除了学习到整个流程,还有什么收获吗?
我觉得是有的,Picasso在很多集合类的选择上是非常合适的,根据不同的需求选择不同的集合类,可以提高代码的质量。比如WeakReference的使用可以避免内存泄露,使用Collections.unmodifiableList()处理不可变List,使用PriorityBlockingQueue作为线程池的缓存可以按照优先级自动排序,对ArrayList的遍历采用for循环比迭代器更有效率、使用ReferenceQueue来取消无用任务等等,在这些地方都可以体现出Picasso代码的优秀。
除此之外,我们已经深入了解了Picasso的实现原理和代码细节,所以在使用的时候就可以发挥Picasso的最大功效和避免一些坑,比如:
使用fit()之后就不能resize(),否则会报错
可以使用Picasso.cancleTag()避免无用请求和内存泄露
在ListView滑动的时候,可以监听滚动事件,使用Picasso.pauseTag()来暂停加载,使用Picasso.resumeTag()来恢复加载,避免ListView的卡顿
如果使用默认的网络加载,在API 14以下不支持硬盘缓存,换成OkHttp则全版本支持
使用Picasso.fetch()可以预加载图片
可以使用Picasso.setIndicatorsEnabled(true)来便捷的查看图片来源
使用Picasso.with().load(URL).get()可以同步获取数据,但是没有内存缓存
使用resize()可以压缩图片到指定尺寸
使用Picasso.with().load(URL).transform()可以对图片进行变换
当设置了图片的宽高(使用fit()、resize())之后,会利用BitmapFactory.Options.inSampleSize和BitmapFactory.Options..inJustDecodeBounds对图片进行压缩
使用Picasso.getSnapshot().dump()可以获取到所有的缓存数据
至此,我们对Picasso的学习就算是基本完成了。
Last updated
Was this helpful?