Picasso源代码分析:1、跟随代码的角度,当我们添加了一个任务,到底发生了什么?

Volley是一个轻量级的网络库,而Picasso是一个轻量级的图片缓存库。我们在分析代码的时候,发现Picasso的源代码远远没有Volley那么清晰,该怎么办?还是跟着代码走,看看当我们将一个任务添加到Picasso的时候,在Picasso内部,到底都做了哪些工作? 一个典型的用法:

Picasso.with(Context).load(“http://www.baidu.com/img/bdlogo.png").into(imageview);

如上,这段代码就是通过Picasso,将地址http://www.baidu.com/img/bdlogo.png的代码下载下来,然后填充到ImageView当中。那我们就依次分析这几个代码,看看具体都有哪些工作? Picasso.with(Context):

public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}

该段代码为Picasso的初始化代码,有以下特征:

  • 一个典型的单例模式
    • 使用的是延时初始化来保证Picasso只有在使用到的时候,才会初始化对象
    • 使用同步代码块的办法来保证多个线程中同时请求的时候,能够保证只创建唯一的单例对象。
      • 同步对象为类对象,保证同步范围整个JVM中。
  • 使用构造者模式,通过Builder类构建了一个默认配置的Picasso的对象。看看默认构造都使用了什么参数:

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

    if (downloader == null) {

    downloader = Utils.createDefaultDownloader(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, indicatorsEnabled, loggingEnabled);
    }

    默认初始化了以下的参数:

    • Downloader
      • DownLoader就是下载用的工具类,在Picasso当中,如果OKHttp可以使用的话,就会默认使用OKHttp,如果无法使用的话,就会使用UrlConnectionDownloader(默认使用HttpURLConnection实现)。
    • Cache
      • 默认实现为LruCache,就是使用LinkedHashMap实现的一个Cache类,注意的一个地方就是,在其他的地方,我们一般默认的是限制的capacity,但是这个地方我们是限制的总共使用的内存空间。因此LruCache在实现的时候,其实简单理解就是将LinkedHashMap封装,然后基于LinkedHashMap的方法实现Cache的方法,在Cache的set()方法的时候,会不断计算当前还可以使用的空间大小,要是超出范围,则删除之前保存的数据。
    • ExecutorService
      • 默认的实现为PicassoExecutorService,该类也比较简单,其实就是ThreadPoolExecutor,在其功能的基础上继续封装,在其中有一个比较细心的功能就是,Picasso通过PicassoExecutorService设置线程数量,来调整在2G/3G/4G/WiFi不同网络情况下的不同表现。
    • RequestTransformer
    • Stats
      • 主要是一些统计信息,比如cache hit/miss,总共下载的文件大小,下载过的图片数量,转换的图片数量等等。
    • Dispatcher
      • Picasso当中,分发任务的线程,这是我们以后要重点研究的一个类,先标记一下,这个Dispatcher主要做了以下的事情:
        • 启动了一个DispatcherThread线程
        • 初始化了一个用来处理消息的DispatcherHandler,注意,根据Dispatcher中默认配置,该Handler所有数据的处理是在DispatcherThread之上。
        • 初始化并注册了一个网络状态广播接收器。

好了,看完Picasso.with(Context),接下来就是load方法: load(String url):

Picasso.with(Context).load(“http://www.baidu.com/img/bdlogo.png").into(imageview);

load(String url)其实是一个load(Uri uri)方法的一个封装,load(Uri uri)的方法原形如下:

public RequestCreator load(Uri uri) {
return new RequestCreator(this, uri, 0);
}

很明显,到这里,我们可以这样推断,Picasso通过调用load(Uri uri),返回了一个传入Picasso对象和要访问的网址或路径的Uri的ReqeustCreator。所以,我们首先要看看ReqeustCreator方法都做了什么工作。 RequestCreator(this, uri, 0)

RequestCreator(Picasso picasso, Uri uri, int resourceId) {
if (picasso.shutdown) {
throw new IllegalStateException(
“Picasso instance already shut down. Cannot submit new requests.”);
}
this.picasso = picasso;
this.data = new Request.Builder(uri, resourceId);
}

可以看到,该构造函数的工作过程是:首先检查picasso是否在工作,如果在工作,那么则设置启动的picasso对象,并通过建造者模式,调用Builder去构造Reqeust.Builder(注意,此时我们传入的resourceId为0,根据我们之前的理解,Uri和resourceId在一个任务中只有一个生效。),并赋值给data变量,继续查看代码:

public Builder(Uri uri) {
setUri(uri);
}

/** Start building a request using the specified resource ID. */
public Builder(int resourceId) {
setResourceId(resourceId);
}

Builder(Uri uri, int resourceId) {
this.uri = uri;
this.resourceId = resourceId;
}

可以发现,其构造函数仅仅是简单的根据传入的参数设置相应的变量而已,并没有其他的工作。那接下来如何呢?别着急,刚刚我们的那行代码还差最后一句into(ImageView).

Picasso.with(Context).load(“http://www.baidu.com/img/bdlogo.png").into(imageview);

因此,我们接下来去探究ReqeustCreator的into(ImgeView)方法。 into(ImageView imageview)实际上调用的是into(ImageView imageview, Callback callback)方法,我们查看其源代码:

/**
* Asynchronously fulfills the request into the specified {@link ImageView} and invokes the
* target {@link Callback} if it’s not {@code null}.
*


* Note: The {@link Callback} param is a strong reference and will prevent your
* {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If
* you use this method, it is strongly recommended you invoke an adjacent
* {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.
*/
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
checkMain();

if (target == null) {
throw new IllegalArgumentException(“Target must not be null.”);
}

if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}

if (deferred) {
if (data.hasSize()) {
throw new IllegalStateException(“Fit cannot be used with resize.”);
}
int width = target.getWidth();
int height = target.getHeight();
if (width == 0 || height == 0) {
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(width, height);
}

Request request = createRequest(started);
String requestKey = createKey(request);

if (!skipMemoryCache) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), “from “ + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
return;
}
}

if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}

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

picasso.enqueueAndSubmit(action);
}

发现这里面做的工作果然很多,我们画一个原理图来细细的分析。 ReqeustCreator-into 图画的很大,这样大家看的清晰,在文字上梳理一遍,into函数到底做了什么事情?

  1. 首先检查当前是否工作在主线程,如果不是在主线程上的话,直接抛出异常退出。为什么要在主线程上工作?我的理解是,因为在这里面我们需要设置ImageView默认显示的图片,但除了这个原因,有没有考虑说对Picasso队列线程同步的问题?现在还没有看到。
  2. 然后判断data.hasImage(),这里面这个函数命名比较迷惑人了,一开始我还以为是判断是否已经有解析好的图片,但再往下看一看源代码,原来这个代码是判断Uri和resourceId是否有设置了一个,如果没有,则说明该Reqeust没有数据源,是错误的。
    1. 如果没有Uri或者resource Id,那么则取消该请求,并设置默认的显示图片并退出。
  3. 该图片是否需要延时执行(这个属性还不太清楚在哪里配置的,不清楚是手动指定还是自动设置)。如果需要延时执行,则按照以下步骤执行。
    1. 判断是否设置过targetSize,如果已经设置过,则抛出异常退出 (不太明白为什么要检查是否已经要设置过targetSize)
    2. 然后获取target,即ImageView对应的长和宽,如果长和宽都是0,那么就设置默认的图片,并构建一个DeferredRequestCreator,放入Picasso对应的队列当中。
    3. 重新设置data即ReqeustCreator对应的targetWidth和targetHeight
  4. 创建Reqeust,生成requestKey
  5. 判断是否需要跳过MemoryCache,如果不跳过,那么就尝试获取图片,并取消对应的请求,进行回调。
  6. 如果需要设置默认的图片,则在这里进行设置
  7. 生成对应的ImageViewAction
  8. 将生成的ImageViewAction添加到队列当中。

所以into()方法实现的功能我们可以这样总结:检查配置的合法性,然后根据配置决定是放入延时队列还是立刻执行,如果立刻执行,则创建对应的请求并尝试从MemoryCache中获取,如果不成功,则生成对应的Action并提交到Picasso的队列当中。后面要做的事情,其实大体可以想到,就是Picasso的任务调度了。 好了,我们从代码

Picasso.with(Context).load(“http://www.baidu.com/img/bdlogo.png").into(imageview);

看到Picasso的工作就是这些,要明白剩下的Picasso还做了什么工作,还需要继续分析源代码。