前言
图片加载框架Picasso相信大家都已经用过很多次了,对它们的使用方法也早就熟稔于心了,那么本文就Picasso的源码进行剖析,学习设计者的优秀的代码设计理念和方法。
几个重要的类
在源码解析开始之前,笔者认为有必要对Picasso的几个重要的类进行简单梳理,以便于后面遇到这些组件的时候可以马上知道它的作用是什么。
1、OkHttp3Downloader
Picasso借助该类来下载图片,并把图片缓存在磁盘空间上。实际上,它用的是OkHttp3
这个网络通信库来完成下载任务。我们看看它的构造方法:
public OkHttp3Downloader(final Context context) {
this(Utils.createDefaultCacheDir(context));
}
public OkHttp3Downloader(final File cacheDir) {
this(cacheDir, Utils.calculateDiskCacheSize(cacheDir));
}
public OkHttp3Downloader(final File cacheDir, final long maxSize) {
this(new OkHttpClient.Builder().cache(new Cache(cacheDir, maxSize)).build());
sharedClient = false;
}
public OkHttp3Downloader(OkHttpClient client) {
this.client = client;
this.cache = client.cache();
}
通过Utils.createDefaultCacheDir(context)
方法来创建缓存文件夹,通过Utils.calculateDiskCacheSize(cacheDir)
来确定磁盘缓存空间的大小。由此我们可以知道,Picasso利用了OkHttp3
的下载机制来缓存图片,并且磁盘缓存的大小也是可以配置的,默认实现是可用空间的2%且不少于5MB.
2、LruCache
如果说OkHttp3Downloader
实现了磁盘缓存,那么LruCache
则是实现了内存缓存。内存缓存的意义在于避免图片过多地堆积在内存中而导致OOM。这里使用的是Lru算法(Least recently used,最近最少使用算法),该算法可以使得经常使用的图片驻留于内存中,避免了反复从磁盘加载图片而导致内存抖动的问题。
3、PicassoExecutorService
这个实际上是一个线程池,它的主要作用就在于把下载任务分配到各个子线程中去执行。
class PicassoExecutorService extends ThreadPoolExecutor {
private static final int DEFAULT_THREAD_COUNT = 3;
PicassoExecutorService() {
super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
}
}
从构造方法可以看出,该线程池的默认实现是3个核心线程且最大线程数不超过3条。也就是说,默认情况下Picasso在下载图片的时候,最大的同时下载数量是3。但实际上,核心线程和最大线程数是会随着设备的网络状态而改变的,比如WIFI状态下是4条核心线程,而4G状态下是3条核心线程,以此类推。
4、Dispatcher
顾名思义,该类是一个调度器,负责分发、调度和处理Picasso产生的各种事件。在这个调度器内,需要关注的分别是dispatcherThread
和handler
这两个成员变量。可以先看一下Dispatcher的构造方法:
Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
Downloader downloader, Cache cache, Stats stats) {
this.dispatcherThread = new DispatcherThread();
this.dispatcherThread.start();
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
//省略...
}
其中,DispatcherThread
继承自Thread,是一条子线程;而DispatcherHandler
则继承自Handler,熟悉Handler的同学,肯定知道这是用于处理线程间通信的常见方法。由此可知,Dispatcher这个调度器的主要工作都是在DispatcherThread
这条线程内完成,而线程切换的任务则是DispatcherHandler
来完成。
5、Request
Request封装了有关一次图片请求的所有信息,比如图片的url、图片的变换策略等,这些都是不可更改的信息。举个例子来说,Picasso.get().load(url).centerCrop().rotate(15).into(imageview);
上面的调用链,Request会封装centerCrop、rotate等信息。与Request相关的是RequestCreator,它可以看作是一个建造器,配置了图片请求的信息。
6、RequestHandler
上面说到Picasso将图片请求封装成了一个Request,而处理Request的组件则是RequestHandler
,因为图片的请求是多种多样的,有的是提供了一个URL从网络获取图片;有的则是提供了一个resourceId,从本地加载图片,不同的请求会有不同的加载方式。因此Picasso提供了多个RequestHandler
来应对不同的情况,用户也可以自定义RequestHandler
来实现自己的需求,只需要重写canHandleRequest
方法和load
方法,如下所示:
public abstract class RequestHandler {
public abstract boolean canHandleRequest(Request data);
@Nullable public abstract Result load(Request request, int networkPolicy) throws IOException;
}
由此看出,Picasso在处理不同的图片请求的时候,将不同请求的实现方式放在了所对应的handler去实现,这样便实现了图片请求和处理请求的解耦合,这样用户自行拓展以适应不同场景下的图片加载需求。
加载图片流程的详细分析
1、Picasso.get()
该方法的调用是一切流程的起点,通过该方法我们可以获取一个Picasso的实例。在Picasso以前的版本,我们是通过Picasso.with(context)
的方式来获取实例的,这限制了我们只能在有上下文context
的环境下使用Picasso。我们来看看这个方法的实现以及探究下为什么新版本的Picasso不用context
这个参数了。
//代码清单:Picasso#get()
public static Picasso get() {
if (singleton == null) {
//第一次判空
synchronized (Picasso.class) {
if (singleton == null) {
//上锁后的第二次判空
if (PicassoProvider.context == null) {
//确保有context
throw new IllegalStateException("context == null");
}
singleton = new Builder(PicassoProvider.context).build();
}
}
}
return singleton;
}
从上面的代码,我们可以看出Picasso使用了DCL(double check lock)形式的单例模式,确保全局只有一个Picasso对象。同时我们注意到context
对象是由PicassoProvider.context
来提供的,显然PicassoProvider
是一个ContentProvider,是Android的四大组件之一,通过它也是可以获取到我们应用的上下文环境的。Picasso通过这样形式的改动,使得Picasso可以适应更多不同的环境,比如在没有context的条件下仅仅利用Picasso进行图片的预下载。
1-1、Picasso实例的构造
Picasso实例的构造是通过构造器模式来进行创建的,Picasso.get()
方法获取的是默认配置的Picasso实例,我们也可以通过Picasso.Builder
来灵活配置适合我们需求的Picasso实例。我们来看看Picasso.Builder.build()
方法,看它是怎样创建一个实例的:
//代码清单1-1:Picasso.Builder#build()
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