【Android】Square 图片加载框架 Picasso 源码解析


img

Picasso 是 Square 公司出品的一款十分优秀的开源图片框架,也是目前 Android 开发中十分流行的一款图片加载框架。提到 Square 公司大家一定不会陌生,OkHttp、Retrofit、LeakCanary 等等 Android 开发者十分熟悉的开源库都出自他们之手,个人认为他们公司的开源库都十分值得研究,今天就让我们来研究一下 Picasso 这款图片加载框架。

Picasso 属于三大图片框架(Glide、Picasso、Fresco)之一。相比其他两个框架,它的特点是轻量,占用的体积更少,同时功能相对来说也比较完善。那么今天就来跟我一起分析一波 Picasso 这个图片选择框架的源码。

此篇文章的源码解析基于 2.71828 版本。

初始化

以我的阅读源码的习惯,都是从使用的时候的入口开始入手,因此我们这里从 Picasso 类入手。旧版本的 Picasso 使用了 with 方法作为入口,而在新版本中 with 方法则被 get 方法所替代,并且不再需要传入 Context 参数。那么它是如何实现的呢?下面我们看到它的 get 方法:

可以看到,这里是一个单例类,而它的 Context 则由一个没有任何实现的 PicassoProvider 这个 ContentProvider 来提供,从而使用户不再需要传入一个 Context。

之后,它调用了 Builder 的 build 方法返回了一个 Picasso 对象。我们先看到 Builder 的构造方法:

可以看到仅仅是判空并赋值。接着我们看看 build 方法:

build 方法中对 downloader、cache 等变量进行了初始化,同时返回了一个新的 Picasso 对象,前面的变量我们先不关心。先看到 Picasso 的构造方法:

可以看到,主要是对 requestHandlers 这个 List 进行初始化以及各个变量进行初始化。通过上面的几个名字可以看出来 RequestHandler 就是 Picasso 对各种类型的图片加载的抽象。通过实现 RequestHandler 接口可以实现不同的图片加载策略。

创建请求

之后我们调用了 load 方法并传入了具体的参数。它有许多重载,可以传入 Uri、String、File、resourceId 等等类型的数据。

我们以 load(String) 为例:

可以看到,它最终调用的还是 load(Uri) 方法。其实所有的其他重载最后都会指向 load(Uri) 方法,也就是说我们-各种形式的数据源最后都是以 Uri 的形式存在于 Picasso 中。我们下面看到 load(Uri):

它构造了一个 RequestCreator 并返回。接下来我们看到 RequestCreator 的构造方法:

它调用了 Request.Builder 的构造方法来为 data 进行赋值,我们看到这个构造方法:

可以看到,这里主要是对 Bitmap.Config 等属性进行设置。

配置加载属性

在我们创建了 RequestCreator 后,可以调用它的 placeholder、error 等等方法为本次加载设置占位图、错误图等等各种属性的设置,下面我们以 placeholder(int) 方法举例:

其实这里就是为 RequestCreator 中的这些属性赋值。

那么所有通过 RequestCreator 设定的属性都是放在 RequestCreator 这个类中的么?

其实不是的,与加载过程有关的属性是放在 RequestCreator 中的,而与图片相关的属性则是放在 Request.Builder 中。

可能看到这里有点乱,大概解释一下。

比如 placeholder、error、memoryPolicy 这些属性就是与加载过程有关而与图片无关的

而比如 resize、centerCrop 这些就是与图片的显示效果有关的属性,也就是图片相关属性。

我们以 resize 为例来看看整体流程:

我们看到 Request.Builder 中的 resize 方法:

这里就是将 Request.Builder 中的一些属性进行了赋值。

加载图片

当属性都设定完后,我们便可以调用 into 方法来加载图片,我们看到 into(ImageView):

它调用了 into(ImageView, Callback):

这里代码比较长,我们慢慢分析,先看看整体大体流程。

首先在注释 1 处进行了一系列判断操作,具体可看注释

之后在注释 2 处,是 fit() 的具体实现。如果外部调用了 fit 使图片自适应 target 的大小,则获取 target 的大小并调用 resize 方法进行设置。这里要特别注意的是如果宽高为 0 则说明 ImageView 的尺寸还没有获取到,此时会延时该图片请求直到获取到 ImageView 的宽高。

之后 3 处构建了一个 Request,并调用 createKey 方法由该 Request 及其各种信息构建了一个 String 类型的 key。

之后在注释 4 处,在使用内存缓存策略的情况下,先调用 quickMemoryCacheCheck 方法获取到了内存缓存中的 BitMap,如果找到则调用 setBitmap 方法将图片应用到 target 中。

然后在注释 5 处,如果内存没有缓存,且设置了占位图,则给它添加占位图。

最后在注释 6 处,构造了一个 Action 对象然后调用了 picasso 的 enqueueAndSubmit 进行网络请求。

Request 的创建

首先,我们看看 Request 是如何创建的,看到 createRequest 方法:

可以看到,这里首先在注释 1 处调用了 Request.Builder 的 build 方法创建了 Request,之后在注释 2 处调用了 picasso 的 transformRequest 方法对 Request 进行转换。

获取 Request

我们先看看 Request.Builder 的 build 方法:

这里就是创建 Request 对象并将各种 Request.Builder 中的属性传递给这个 Request 对象。

转换 Request

然后我们再看看 picasso 的 transformRequest 方法:

这里调用了 requestTransformer 的 transformRequest 方法来进行转换。而这个 requestTrasformer 则是之前在 Picasso.Builder 中的 build 方法中初始化给 transformer 的 RequestTransformer.IDENTITY:

我们看看它的 transformRequest 的实现:

可以看到这里是返回了原始的 Request。

既然都是返回默认 Request,为什么 Picasso 还要在创建的时候添加这一步 transform 的过程呢?

其实这个 transformer 我们是可以通过 Builder 的 requestTransformer 方法来进行设置的。也就是说这里主要是提供给用户对 Request 进行一些特殊处理的渠道,使得我们可以对图片加载的过程进行一定的扩展与定制。这种设计是值得我们去学习的。

之后我们回到 Request 创建的部分,可以看到这里如果对 Request 进行了修改,在注释 3 处会将原 Request 的 id 和 started 赋值过去,从而防止用户对它们进行修改。

key 的生成

我们再来看看 Request 的 key 是如何生成的:

其实这里就是用一个 StringBuilder 构造了一个 String,将 Request 中的各类信息都存放于 key 中。这个 key 其实就是用于内存缓存中的 key。

图片的加载

我们先不去查看内存缓存的部分,留到后面来讲解,我们先看看图片是如何从网络加载的。先看到 into 方法的下面这两句:

构造 Action

我们先看到 FetchAction 的构造方法:

调用了父类的构造方法:

可以看出来,Action 类实际上就是一个携带了需要的信息的类。

分发 Action

接着,调用了 picasso 的 submit 方法:

这里调用了 dispatcher 的 dispatchSubmit 方法:

这里用到了一个 DispatcherHandler 类的对象调用 sendMessage 方法发送一条信息。这里的 DispatcherHandler 的作用主要是根据不同的调用将 Action 分发到不同的方法中。

下面我们看到 DispatcherHandler 的实现,它是 Dispatcher 的一个内部类:

这里根据不同的 Message 调用了不同的方法,我们的 submit 方法调用了 Dispatcher 中的 performSubmit 方法:

它调用了 performSubmit(Action, boolean) 方法:

首先,在注释 1 处根据 Action 获取到了其对应的 BitmapHunter。

之后在注释 2 处检查 service 是否被杀掉。

然后在注释 3 处,调用了 forRequest 获取到了 Action 对应的 BitmapHunter,然后调用了 service 的 submit 方法。

之后将该 action 与 BitmapHunter 放入了 hunterMap 中。

BitmapHunter 的获取

我们看一下前面的步骤中 BitmapHunter 是如何获取的,来到 forRequest方法:

这里主要是依次遍历各个 RequestHandler,找到可以处理该类 Request 的 Handler,并构建 BitmapHunter。

我们先看看 RequestHunter 是如何判断能否处理该类 Request 的,我们以 NetworkRequestHandler 举例:

可以看到,它是通过判断 uri 的 scheme 来判断能否处理该类 Request 的。

我们接着看到 BitmapHunter 的构造函数:

可以看到,这里主要是各种变量的赋值。

接着我们看到 service 的 submit 方法,这里的 service 是 PicassoExecutorService:

这里构建了一个 PicassoFutureTask,然后调用了 execute 方法

我们先看看 PicassoFutureTask 的构造方法:

PicassoFutureTask 是 FutureTask 的子类,这里主要是变量的赋值。

图片资源的获取

接着我们看到 execute 方法,这里其实是调用了 Java 自带的 ThreadPoolExecutor 的 execute 方法。同时这里也说明了这里是一个异步的过程。

其实 BitmapHunter 是一个 Runnable,当调用了 execute 方法后便会执行它的 run 方法。我们可以看到它的 run 方法:

这里调用了 hunt 方法获取到了结果 Bitmap,同时在后面根据不同的结果通过 dispatcher 进行结果的处理:

这里代码很长,我们慢慢分析:

首先在注释 1 处尝试从内存通过 key 获取对应 bitmap,若获取到则直接返回。

之后在注释 2 处,根据 requestHandler 中的 retryCount 来判断是否是网络请求,从而获取不同的 networkPolicy。若 retryCount 为 0 则为离线策略。

之后在注释 3 处,通过 requestHandler 的 load 方法进行数据的加载,若数据加载成功则进行一些变量的赋值,并获取 bitmap。

若 bitmap 为空则说明我们需要在注释4处将其从流中 decode 出来。

之后在注释 5 处就是 Picasso 的加载过程中支持用户对图片进行定制后再应用的具体实现了。这里首先判断是否需要 transform。

在注释 6 处判断如果需要进行矩阵变换(旋转,放大缩小等),则调用 transformResult 方法进行变换。

在注释 7 处判断如果有自定义变换,则调用 applyCustomTransformations 进行自定义变换。

这里的自定义变换比较类似前面的自定义 Request 转换,用户可以在外部自定义 Transformation,并通过 RequestCreator 的 transform 方法传入,这样就可以在图片应用前对 Bitmap 进行一些自定义 (如高斯模糊等)后再应用于 target。这种设计是我们值得学习的。

RequestHandler 的实现

下面我们以网络图片对应的 NetworkRequestHandler 为例看看它们的实现,其他的子类可以自己去了解。让我们看到它的 load 方法:

可以看到,这里是通过 OkHttp3 来实现的图片的加载。

首先调用 createRequest 方法创建了 OkHttp 的 Request。然后通过自己实现的 OkHttp3Downloader 的 load 方法来实现对这个 Request 的下载请求。

之后根据缓存的相应是否是空判断数据的来源是从本地还是网络。

最终构造了一个 Result 并返回。

OkHttp3.Request 的 创建

我们先看看如何将 Request 转换为 OkHttp3.Request。让我们看到 createRequest 方法:

可以看到,首先根据 Request 和 NetworkPolicy 的参数设置缓存的各种参数,之后调用 okhttp3.Request.Builder 的构造函数并传入 uri 创建了 Request。

OkHttp3 数据的获取

之后我们看到 OkHttp3Downloader 的 load 方法,看看数据获取是如何实现的:

其实就是调用 OkHttpClient 的 newCall 方法并调用 execute 获取一个 Response。

结果的处理

前面提到,在 BitmapHunter 的 run 方法中根据 hunt() 返回的结果成功与否调用了 dispatcher 的不同方法来进行的结果处理,让我们看看是如何处理的

我们先看到 dispatchComplete 方法,它最终通过 handler 调用到了 performComplete 方法中:

可以看到,这里如果获取到了结果,且需要内存缓存,则将其放入内存缓存。然后将这个 BitmapHunter 从 Map 中删除。

之后我们看到 dispatchFailed 方法,它最终通过 handler 调用到了 performError 方法:

这里它将 BitmapHunter 从 Map 中移除,然后就没有进行其他处理了。

内存缓存

为了优化流量消耗,Picasso 加入了内存缓存机制,下面我们来看看 Picasso 内存缓存的实现。

就像其他部分一样,它的内存缓存也考虑到了扩展性,给了用户自己实现的接口。

我们可以调用 Picasso 类的 memoryCache 方法为其设置 Cache 接口的子类,从而实现自己的内存缓存。

若用户不传入指定缓存,则默认使用 Picasso 自己实现的 LruCache。

具体的 LruCache 的设计这里不深入讲解,有兴趣的各位可以去了解一下 LRU 算法,以后可能可以专门开一篇博客讲讲 LRU 算法。

Dispatcher 设计

其实从前面的讲解中,你会发现,其实如图片的加载请求,缓存命中等等事件都是由一个叫 Dispatcher 的类分发的,它内部由 Handler 实现,负责将请求封装,并按优先级排序,之后按照类型分发。

这种设计也很值得我们学习,它作为一个分发中心管理我们的各类请求。使得我们的设计更为清晰,也使得库更容易维护。

线程池设计

之前没有提到的就是 Picasso 对线程池也有一些优化,它自己实现了一个 PicassoExecutorService 类,它可以根据当前的网络状态,采用不同的线程池数量,从而使得网络不会过于拥塞。

具体可以看下面这个方法:

可以看到,线程池最大线程个数如下:

  • 在 WIFI 网络下,采用最多 4 个线程的线程池

  • 在 4G 网络下,采用最多 3 个线程的线程池

  • 在 3G 网络下,采用最多 2 个线程的线程池
  • 在 2 G 网络下,采用最多 1 个线程的线程池

总结

Picasso 是一个非常值得我们学习的轻量级图片加载库,它采用 OkHttp3 来加载网络图片,并使用了二级内存缓存来提高加载速度。它的 Dispatcher 思想以及对外部的扩展开放的思想十分值得我们学习,这次源码的阅读还是给了我很大的启发的。

当然,由于篇幅有限,这篇文章并没有包含 Picasso 的方方面面,它的代码中还有如下的一些点在本文还没有分析,读者们有兴趣的可以从下面的点去研究这个库:

  • 图片加载的暂停与取消
  • 图片的变换实现
  • 请求的优先级
  • 对整体的监控
  • 本地资源的加载

Android Developer in GDUT