前言
图片缓存系列之
Picasso
正文
一. 概述
在开始之前, 我想先贴两张比较具有概括性的流程图
下面这张是对Picasso
核心交互类的概括, 来自Android 三大图片缓存原理、特性对比
下面这张是Picasso
下载一张图片的时候的执行流程图, 来自Picasso学习笔记
注: 笔者在看Picasso
源码的时候, 发现其执行流程和上图有些不符, 主要是没有最后的Downloader
, 下载过程完全交给了NetworkRequestHandler
去做, 应该是不同版本造成的; (本文对应Picasso
源码版本为picasso:2.71828
)
Picasso
中比较重要的类在上面第一张图片已经列出, 主要是Picasso
, Dispatcher
, RequestHandler
; 下面分别总述一下其作用:
-
Picasso
: 该类是我们使用的起点, 是图片下载, 图片转换, 图片缓存的Manager
; 其提供的静态内部类Builder
, 用于接收用户配置, 一般而言, 我们都使用Picasso
的一个全局实例(单例); -
Dispatcher
: 顾名思义, 就是一个调度中转站, 用于分发请求(下载请求, 暂停请求, 恢复请求等), 传递数据(数据成功返回时进行分发绑定), 错误处理分发等 -
RequestHandler
: 这是一个抽象类, 提供了数据处理的公共抽象;Picasso
默认实现的有AssetRequestHandler
,ResourceRequestHandler
,ResourceDrawableRequestHandler
,NetworkRequestHandler
,ContentStreamRequestHandler
,ContactsPhotoRequestHandler
; 分别对应于不同的处理场景和模式, 其中最重要和最常见的就是NetworkRequestHandler
了; 当然, 也可以自定义实现该类, 用于处理Picasso
所没有涵盖的特殊情况(需要使用Picasso.Builder.addRequestHandler(RequestHandler)
注册)
下面, 笔者将主要分析网络请求这一部分, 包括: 一条URL
被处理的完整流程, 暂停,恢复和取消请求机制, 缓存机制; 当然, Picasso
还处理了很多其他情况, 如程序监控部分, 图片处理部分等, 但这些不是最主要的, 本文不再赘述
二. URL处理流程
开始之前, 需要先看一下Picasso
的典型使用, 如下; 各部分的作用都已经标注清楚了, 下面将对每一个过程进行讲解
PicassoProvider.get() // 单例, 用于得到一个全局唯一Picasso实例
.load(url) // 加载url
.placeholder(R.drawable.placeholder) // 设置图片加载过程中的占位图片
.error(R.drawable.error) // 设置出错后的占位图片
.fit() // 图片处理部分, 表示将图片适应ImageView大小
.tag(context) // 设置tag, 用于暂停, 恢复, 取消请求时使用
.into(view); // 加载成功后设置到哪个View中
PicassoProvider.get()
是实现的一个单例模式, 如下; 该模式比较常见, 不多讲
public static Picasso get() {
if (instance == null) {
synchronized (PicassoProvider.class) {
if (instance == null) {
Context autoContext = PicassoContentProvider.context;
if (autoContext == null) {
throw new NullPointerException("context == null");
}
instance = new Picasso.Builder(autoContext).build();
}
}
}
return instance;
}
从上面我们可以看出, Picasso
的实例是通过其静态内部类Builder
进行build
的, 这也是一种典型的设计模式–Builder
模式; 该模式对于有很多参数要设置时是很方便的, 这里没有进行额外的参数设置, 使用的是Picasso
内部的默认参数(比如: 默认缓存大小, 使用自带RequestHandler
等进行请求处理等)
之后的过程是将URL
加载进去, 然后将该URL
封装为一个Request
实例, 返回一个RequestCreator
实例, 然后在into
的时候, 进行异步请求图片; 当然, 在之前, 还有一个placeholder()
的过程, 该过程上面说了就是设置一个请求过程中的占位图片, 其内部做的也很简单, 只是将ID
保留下来, 在into()
中进行统一的逻辑处理
接下来, 主要的处理逻辑都指向了into()
函数, 下面我们来详细看其逻辑; 如下, 先设置占位图片, 然后将请求封装为一个Action
实例提交到线程池中处理
public void into(@NonNull ImageView target, @Nullable Callback callback) {
...
if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable()); // 设置占位图片
}
return;
}
...
Action action = new ImageViewAction(picasso, wrapper, request, callback);
picasso.enqueueAndSubmit(action); // 提交请求
}
而picasso.enqueueAndSubmit()
又只是单纯的去调用submit()
, 在submit()
中将任务分配给Dispatcher
进行调度
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
而Dispatcher
进行调度实际上是通过Handler
进行消息传递的, 该Handler
实际上是DispatcherHandler
, 我们直接去看其handleMessage()
就好;
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
public void handleMessage(final Message msg) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
...
}
在performSubmit()
中进行处理, 将请求再次封装为一个BitmapHunter
, BitmapHunter
是一个Runnable
, 之后提交给线程池进行异步加载, 即下面的service.submit()
, 这里的service
实际上是PicassoExecutorService
, 继承于ThreadPoolExecutor
, 默认开了3
个线程, 当然, 这部分与网络请求相关的Picasso
内部也进行了判断, 比如在Wifi
或者2G
环境下采用不同的请求策略, 感兴趣的话可以自己跟着走一遍, 这里的主要目的是看一个网络请求的完整处理过程
void performSubmit(Action action, boolean dismissFailed) {
...
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
...
}
我们上面说了, BitmapHunter
是一个Runnable
, 线程池执行的时候, 就是去执行其run()
方法, 在run()
中, 有一个hunt()
, 其作用是将请求得到的Bitmap
封装为Result
返回, 这里的hunt()
是一个阻塞耗时方法, 但是run()
异步运行, 所以也没有问题; 而在hunt()
中, 又是通过RequestHandler
的一个实现类NetworkRequestHandler
去处理的, 处理过程也是常规的图片下载过程, 如下;
public void run() {
...
result = hunt();
...
}
public void load(@NonNull Picasso picasso, @NonNull final Request request, @NonNull
final Callback callback) {
...
ResponseBody body = response.body();
...
Bitmap bitmap = decodeStream(body.source(), request);
callback.onSuccess(new Result(bitmap, loadedFrom));
...
}
在图片接受完成之后, 再一步步函数返回, 封装为Result
, 回传至run()
中; 通过Dispatcher
进行通知图片接受成功还是失败; 最终会在deliver()
中调用主线程的Handler
通知进行处理;
public void run() {
...
if (result.getBitmap() == null && result.getDrawable() == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
...
private void deliver(BitmapHunter hunter) {
...
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_COMPLETE, hunter));
}
而mainThreadHandler
的处理逻辑在Picasso
类中, 很明显, 到这里之后就是将图片设置到View
了
static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
@Override public void handleMessage(Message msg) {
switch (msg.what) {
case HUNTER_COMPLETE: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
hunter.picasso.complete(hunter);
break;
}
...
}
}
};
至此, 从网络请求图片的一个完整过程就讲完了, 当然, 中间还涉及很多代码细节和处理, 最好自己跟着流程再一遍 ~
三. 暂停, 恢复, 取消请求处理
暂停请求使用picasso.pauseTag();
; 恢复请求使用picasso.resumeTag();
; 取消请求使用picasso.cancelRequest();
; 一般来说, 这些的使用场景是, 考虑当我们从网络加载图片填充到GridView
的时候, 如果我们在用户不断滑动过程中不暂停请求的话, 那么就会造成加载的延迟和卡顿(因为在不断滑动过程中会产生许多网络请求); 一般比较通用的解决方法是给GridView
添加ScrollListener
(GridScrollListener
), 监听滑动过程, 在滑动过程中暂停请求, 停止滑动时恢复请求即可
那么暂停, 恢复, 取消请求的实现机制又是如何的呢, 下面我们将一一分析
3.1 暂停请求
我们直接看picasso.pauseTag();
做了什么, 如下; 其直接交给了Dispatcher
进行请求分发, 这也符合我们上面总述的时候讲的Dispatcher
的功能; 之后Dispatcher
仍然通过Handler
进行事件分发, 如下; 而其对TAG_PAUSE
事件的处理则是直接转交给了dispatcher.performPauseTag(tag);
中
public void pauseTag(@NonNull Object tag) {
checkNotNull(tag, "tag == null");
dispatcher.dispatchPauseTag(tag);
}
void dispatchPauseTag(Object tag) {
handler.sendMessage(handler.obtainMessage(TAG_PAUSE, tag));
}
在performPauseTag()
中, 其实就是将tag
保存到pausedActions
中, 这是一个Map
void performPauseTag(Object tag) {
// Trying to pause a tag that is already paused.
if (!pausedTags.add(tag)) { // 如果已经添加, 则直接返回
return;
}
// Go through all active hunters and detach/pause the requests
// that have the paused tag. // 否则, 遍历BitmapHunter, 将对应tag保存下来
for (Iterator<BitmapHunter> it = hunterMap.values().iterator(); it.hasNext();) {
...
pausedActions.put(single.getTarget(), single);
...
}
}
那么我们什么时候会用到该tag
呢, 就是在使用Dispatcher
分发请求的时候, 这里先去判断在pausedTags
中是否有对应的tag
记录, 如果有的话, 就不进行请求分发, 那么自然也就不会去下载图片了 ~
void performSubmit(Action action, boolean dismissFailed) {
if (pausedTags.contains(action.getTag())) {
pausedActions.put(action.getTarget(), action);
...
return;
}
...
}
3.2 恢复请求
恢复请求使用的是picasso.resumeTag();
; 恢复请求的过程和暂停请求的过程差不多, 前面都是通过Dispatcher
直接进行请求分发, 然后通过Handler
去发送消息, 然后在performResumeTag()
进行处理; 在performResumeTag()
中, 一个是将tag
从pausedTags
中移除, 另一个是将原来的Action
(封装了请求等信息)通过mainThreadHandler
传递, 而mainThreadHandler
对REQUEST_BATCH_RESUME
的处理, 则是对每一个Action
去调用picasso.resumeAction()
void performResumeTag(Object tag) {
// Trying to resume a tag that is not paused.
if (!pausedTags.remove(tag)) {
return;
}
List<Action> batch = null;
for (Iterator<Action> i = pausedActions.values().iterator(); i.hasNext();) {
Action action = i.next();
if (action.getTag().equals(tag)) {
if (batch == null) {
batch = new ArrayList<>();
}
batch.add(action);
i.remove();
}
}
if (batch != null) {
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(REQUEST_BATCH_RESUME, batch));
}
}
在resumeAction()
中, 其实就是将请求交给了enqueueAndSubmit()
, 而该过程在上面我们分析一个URL
的完整请求过程的时候, 已经看到过该函数了; 接下来的过程就不赘述了
void resumeAction(Action action) {
...
enqueueAndSubmit(action);
...
}
3.3 取消请求
取消请求用的是picasso.cancelRequest();
, 该函数一共有三个重载函数, 但是最终都是交给了cancelExistingRequest()
处理, 接下来的过程和上面的过程差不多, dispatcher.dispatchCancel()
分发消息, 然后交给Handler
传递消息, 最终调用performCancel()
处理请求; 在performCancel()
中, 其实就是将BitmapHunter
移除就好了, 前面我们说过, BitmapHunter
是一个Runnable
, 所以将其移除之后, 自然就取消请求啦 ~
void performCancel(Action action) {
String key = action.getKey();
BitmapHunter hunter = hunterMap.get(key);
if (hunter != null) {
hunter.detach(action);
if (hunter.cancel()) {
hunterMap.remove(key);
...
}
}
...
}
3.4 小结
到这里, 暂停, 恢复, 取消请求的过程我们已经有了比较详细的了解, 当然, 对于一个图片加载库来说, 这些接口也都是必备的
四. 缓存
Picasso
的缓存也分为内存缓存和磁盘缓存, 内存缓存基于LruCache
可以自行配置缓存大小等, 磁盘缓存依赖与Http
缓存, 不可配置
至于内存缓存部分, 逻辑很常规, 在图片三级缓存之内存缓存和磁盘缓存中已经比较详细的讲过LruCache
的源码了, 可以参照该博客; 在Picasso
中默认使用的内存缓存最大容量是可用内存的15%
磁盘缓存, 实际上Picasso
是交给了OkHttp
去实现, 而没有明确的使用DiskLruCache
去缓存; 使用的是OkHttp
中的okhttp3.Cache
, 该类中有一个DiskLruCache
用于磁盘缓存; DiskLruCache
的缓存策略可以参见图片三级缓存之内存缓存和磁盘缓存, 至于OKHttp
的缓存策略会单独抽取一篇博客出来;
这里只是简单提一下Picasso
中默认的磁盘缓存策略, 即使用存储容量的2%
, 但是不多于50M
不少于5M
五. 总结
Pciasso
的主要源码就分析到这里, 最后还想贴一下Picasso
的总体特性
-
轻量级的图片加载库
-
自带监控功能, 可以检测
cache hit/内存大小
等等数据 -
支持图片预加载
-
线程并发数依网络状态变化而变化, 优先级调度
-
支持图片变换, 图片压缩, 自适应
-
易扩展
Picasso
是一个优秀的图片加载库, 其也具备了一个图片加载库应该具有的模块:
-
请求分发模块。负责封装请求,对请求进行优先级排序,并按照类型进行分发。
-
缓存模块。通常包括一个二级的缓存,内存缓存、磁盘缓存。并预置多种缓存策略。
-
下载模块。负责下载网络图片。
-
监控模块。负责监控缓存命中率、内存占用、加载图片平均耗时等。
-
图片处理模块。负责对图片进行压缩、变换等处理。
-
本地资源加载模块。负责加载本地资源,如assert、drawable、sdcard等。
-
显示模块。负责将图片输出显示。