图片缓存之Picasso源码分析

Picasso源码分析

Posted by 袁平 on September 1, 2018

前言

图片缓存系列之Picasso


正文


一. 概述

在开始之前, 我想先贴两张比较具有概括性的流程图

下面这张是对Picasso核心交互类的概括, 来自Android 三大图片缓存原理、特性对比

Picasso

下面这张是Picasso下载一张图片的时候的执行流程图, 来自Picasso学习笔记

picasso流程图

: 笔者在看Picasso源码的时候, 发现其执行流程和上图有些不符, 主要是没有最后的Downloader, 下载过程完全交给了NetworkRequestHandler去做, 应该是不同版本造成的; (本文对应Picasso源码版本为picasso:2.71828)

Picasso中比较重要的类在上面第一张图片已经列出, 主要是Picasso, Dispatcher, RequestHandler; 下面分别总述一下其作用:

  1. Picasso: 该类是我们使用的起点, 是图片下载, 图片转换, 图片缓存的Manager; 其提供的静态内部类Builder, 用于接收用户配置, 一般而言, 我们都使用Picasso的一个全局实例(单例);

  2. Dispatcher: 顾名思义, 就是一个调度中转站, 用于分发请求(下载请求, 暂停请求, 恢复请求等), 传递数据(数据成功返回时进行分发绑定), 错误处理分发等

  3. 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()中, 一个是将tagpausedTags中移除, 另一个是将原来的Action(封装了请求等信息)通过mainThreadHandler传递, 而mainThreadHandlerREQUEST_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的总体特性

  1. 轻量级的图片加载库

  2. 自带监控功能, 可以检测cache hit/内存大小等等数据

  3. 支持图片预加载

  4. 线程并发数依网络状态变化而变化, 优先级调度

  5. 支持图片变换, 图片压缩, 自适应

  6. 易扩展


Picasso是一个优秀的图片加载库, 其也具备了一个图片加载库应该具有的模块:

  1. 请求分发模块。负责封装请求,对请求进行优先级排序,并按照类型进行分发。

  2. 缓存模块。通常包括一个二级的缓存,内存缓存、磁盘缓存。并预置多种缓存策略。

  3. 下载模块。负责下载网络图片。

  4. 监控模块。负责监控缓存命中率、内存占用、加载图片平均耗时等。

  5. 图片处理模块。负责对图片进行压缩、变换等处理。

  6. 本地资源加载模块。负责加载本地资源,如assert、drawable、sdcard等。

  7. 显示模块。负责将图片输出显示。


六. 参考链接

  1. Picasso源代码分析

  2. Picasso学习笔记

  3. Android 三大图片缓存原理、特性对比