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}.
 * <p>
 * <em>Note:</em> 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 <b>strongly</b> 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还做了什么工作,还需要继续分析源代码。

 

About: happyhls


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

发表评论

电子邮件地址不会被公开。 必填项已用*标注