前言
图片缓存系列之
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等。
-
显示模块。负责将图片输出显示。