Picasso 图片加载流程分析

Picasso 是目前目前比较流行的轻量级的图片加载框架。可以让调用者无感知的情况下完成了图片的加载、显示、缓存,并提供了灵活的扩展接口。Picasso可以从多个网络、文件、资源等等多个途径进行加载图片,同时可以将图片渲染ImageView、RemoteView、自定义 Target 等多个渠道上。由于加载的思路基本一致,所以本文主要基于从网络加载图片显示到 ImageView 这个情景,基于写作时的2.5.2版本。来分析其中比较主要的类,并在最后看看这个类之间如何有序的连接在一起进行工作的。

一般的调用方式

下面是一个简单的示例用来将一张网络图片显示在 ImageView 上:

Picasso.get().load("http://via.placeholder.com/350x150").into(imageView);

一个漂亮的链式调用完成了一张网络图片的加载。我们将其拆分开,看看每一步发生了什么。

Picasso.get()

链式调用的入口发生在 Picasso对象的get方法上。这个方法比较简单,用于返回单例的Picasso 对象:

  public static Picasso get() {
    if (singleton == null) {
      synchronized (Picasso.class) {
        if (singleton == null) {
          if (PicassoProvider.context == null) {
            throw new IllegalStateException("context == null");
          }
          singleton = new Builder(PicassoProvider.context).build();
        }
      }
    }
    return singleton;
  }

我们直接来看看这个Picasso对象是如何构建的。

    /** Create the {@link Picasso} instance. */
    public Picasso build() {
      Context context = this.context;

      if (downloader == null) {
        downloader = new OkHttp3Downloader(context);
      }
      if (cache == null) {
        cache = new LruCache(context);
      }
      if (service == null) {
        service = new PicassoExecutorService();
      }
      if (transformer == null) {
        transformer = RequestTransformer.IDENTITY;
      }

      Stats stats = new Stats(cache);

      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

      return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
          defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
    }
  }

Picasso对象是由executorService、cache、downloader、transformer等多个组件构成,我们可以自定义这些组件的实现。如果我们没有提供的话, Picasso会有自己的默认实现。我们可以来看看这些默认实现是如何工作的。

Cache

Cache用于缓存一些常用的图片。和目前大多数的图片加载实现类似,Picasso 默认使用LruCache来进行缓存。LruCache底层基于 LinkedHashMap实现。会尽可能的保留使用次数比较多的数据。Picasso 将默认缓存的带下设置为 app 堆大小的1/7。

Downloader

Downloader 是一个用于描述下载外部资源(硬盘、网络)的接口。其主要的方法是:

  @NonNull Response load(@NonNull Request request) throws IOException;

这里的RequestResponse就是 okhttp 中的RequestResponse。所以Picasso 的 Downloader 的默认实现也是基于 okhttp 的。因为 okhttp 是自带硬盘缓存功能的。所以Picasso 是没有提供硬盘缓存接口的。如果我们需要自己来实现Downloader接口,就需要考虑自己来实现硬盘缓存了。或者将缓存逻辑放在 Cache接口里。

ExecutorService

ExecutorService就是 Java 中的线程池,用来执行耗时的操作。Picasso 默认的线程池有如下的特性:

RequestTransformer

RequestTransformer是一个用于在图片加载请求被正式提交执行之前,给调用者提供了预处理的接口。这个接口需要实现的函数是:

    com.squareup.picasso.Request transformRequest(com.squareup.picasso.Request request);

上面的 Request是 picasso对图片请求的封装,并非okhttp 中的 Request,这个函数的调用发生在网络加载之前。我们可以实现这个接口来对请求进行统一的修改。比如,我们想用 CDN 加速,可以统一更换图片的 HOST到 CDN 对应的域名下。

RequestCreator

上面主要是对Picasso的几个组件默认实现进行了简单分析。接下来我们回到Picasso.get().load("http://via.placeholder.com/350x150")这句代码,它返回的是RequestCreatorPicasso中的组件主要用于全局的控制。而RequestCreator用于描述一个图片请求自身的一些属性。举几个常用的属性:

Action

当我们调用RequestCreator对象的 into方法时,Picasso就会上演一出漂亮的图片加载大戏。在文章的最开始我们提到过Picasso可以将记载的图片作用于 ImageView、RemoteView 等多个渠道。背后的实现就是对每种渠道做了抽象。Action就是用来描述这个场景的类。如 into的参数如果是 ImageView,就会生成一个ImageViewAction,并将这个ImageViewAction提交到picasso.enqueueAndSubmit(action) 方法进行执行。

Dispatcher

Dispatcher就是一个调度器,上面说到的picasso.enqueueAndSubmit(action) 方法也只是将 action 交由Dispatcher 来处理:

  void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    if (target != null && targetToAction.get(target) != action) {
      // This will also check we are on the main thread.
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    submit(action);
  }

  void submit(Action action) {
    dispatcher.dispatchSubmit(action);
  }

Dispatcher的原理就是通过HandlerThread来监听并转发一些事件。如前面生成的ImageViewAction会被封装成BitmapHunter,提交到前文的ExecutorService 中处理。

BitmapHunter

这个类实现了 Runnable接口,主要职责就是解析图片的请求,将其转换为Bitmap。更细分的话,其处理流程如下:

RequestHandler

前面提到了NetworkRequestHandler,这个类继承自RequestHandlerRequestHandler是对不同来源请求的描述。如网络图片的实现是NetworkRequestHandler,资源图片的请求实现是ResourceRequestHandler.RequestHandler主要有两个需要重写的方法:

public boolean canHandleRequest(Request data)

public Result load(Request request, int networkPolicy)

canHandleRequest用于描述该 handler 是否能够处理这个请求,load用于将请求转换为处理结果。

CleanupThread

优秀的框架很大程度在于其对细节的处理。比如当我们的 ImageView 所在的页面已经销毁,ImageView不在被其它类所引用。此时和ImageView对应的 Request 的理论上就没有进行的必要了。所以Picasso会有一个CleanupThread后台线程用来处理这个事情的。其原理就是ImageView 会被放在WeakReference中,CleanupThread回去检测WeakReference对应的ReferenceQueue中是否有将要被 gc 的对象即可。

RecyclerView/ListView 乱序?

Picasso 可以解决RecyclerView或者ListView在 item 重用时的乱序问题,它是如何做到的呢?

在调用RequestCreatorinto(ImageView imgview)方法时,最后执行的语句是:

 Action action =
        new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
            errorDrawable, requestKey, tag, callback, noFade);

picasso.enqueueAndSubmit(action);

进入 enqueueAndSubmit(Action action)方法看一下:

void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    if (target != null && targetToAction.get(target) != action) {
      // This will also check we are on the main thread.
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    submit(action);
}

if 语句在检查当前 ImageView 是否执行了一个加载请求,且和当前需要加载的请求不同,如果是的话就会执行cancelExistingRequest(target)方法取消之前的请求。这样就保证了不会出现前一次加载的请求覆盖了最新请求的问题。

End

最后,附上一张流程图可以让大家更好的理解Picasso的流程:

image