Picasso源代码分析:2、ReqeustCreator

在Volley当中,我们很清晰的能够看到,建立请求,都会放在Reqeust当中,当结果的反馈是通过ResponseDelivery调用对应的Response反馈。Picasso和Volley相比,更加专注在图形,但应该也是遵循类似的逻辑。那我们今天就来分析一下,在不同的执行阶段,一个请求封装成的RequestCreator、Request、Target、Action。

RequestCreator

RequestCreator用来做什么?用来构建一个图片下载的Request,那它是如何组织的呢? 静态变量

private static int nextId = 0;

有一个静态的用来获取ReqeustId()的方法,代码如下:

private static int getRequestId() {
if (isMain()) {
return nextId++;
}

final CountDownLatch latch = new CountDownLatch(1);
final AtomicInteger id = new AtomicInteger();
Picasso.HANDLER.post(new Runnable() {
  @Override public void run() {
    id.set(getRequestId());
    latch.countDown();
  }
});
try {
  latch.await();
} catch (InterruptedException e) {
  sneakyRethrow(e);
}
return id.get();

}

  • 此处是设置该Reqeust的Id,有两种情况:
    • 如果是在主线程上调用:则直接将返回nextId,并自增–>貌似不是线程安全的
    • 如果是在其他的线程上调用,那么通过handler发送给主线程,在主线程中同样从这里取一个nextId,然后自增,然后再设置给AtomicInteger。使用了CountDownLatch来在其他线程中等待主线程设置Id。
  • 有个问题:这个线程是不是线程安全的?
    • 我认为这个方法并不是线程安全的,其所做的工作只是保证所有的nextId的分配都在主线程上进行,但并没有保证不会同时有两个线程去请求获取ReqeustId()。
    • 不对,这个应该是线程安全的,因为这个方法在返回nextId之前,保证调用是来自主线程。换句话说,只有在主线程调用getRequestId()的时候,才会去生成并返回reqeustId,既然都在一个线程上进行的,那肯定是线程安全的。

类成员变量:

private final Picasso picasso;

//注意,其实在Reqeust.Builder是保存了所有的与图片显示相关的信息,包括图片的URL或者ResourceId,宽度,长度,是否旋转裁剪等等。
private final Request.Builder data;

private boolean skipMemoryCache;
private boolean noFade;
private boolean deferred;
private boolean setPlaceholder = true;
private int placeholderResId;
private int errorResId;
private Drawable placeholderDrawable;
private Drawable errorDrawable;
private Object tag;

成员变量中,则是保存了一Picasso对象的引用,还有一个Reqeust.Builder来保存图片的相关的信息等等,具体用的时候就会明白。 构造函数:

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);
}

注意,构造函数的访问限定符是package。 在ReqeustCreator当中有大量的getter\setter函数,我们这里先不去分析,分析几个关键的点: public Bitmap get() throws IOException;

/**
* Synchronously fulfill this request. Must not be called from the main thread.
* 同步填充request,必须从主线程以外的线程上调用。
*


* Note: The result of this operation is not cached in memory because the underlying
* {@link Cache} implementation is not guaranteed to be thread-safe.
* 该操作的结果并不会在内存中cache,因为底层的{@link Cache}实现并不保证是线程安全的。
*/
public Bitmap get() throws IOException {
long started = System.nanoTime();
checkNotMain();

if (deferred) {
throw new IllegalStateException(“Fit cannot be used with get.”);
}
if (!data.hasImage()) {
return null;
}

//创建Request
Request finalData = createRequest(started);
//根据Reqeust,创建CacheKey
String key = createKey(finalData, new StringBuilder());
//获取对应的Action
Action action = new GetAction(picasso, finalData, skipMemoryCache, key, tag);
//首先创建一个BitmapHunter,并调用其中的hunt()方法。
return forRequest(picasso, picasso.dispatcher, picasso.cache, picasso.stats, action).hunt();
}

该方法的作用:同步填充一个请求,必须在主线程上调用,且不会写入Cache。其具体的方法是:

  1. 首先检查是否工作在主线程,然后检查是否已经设置请求的地址Uri或者Resource Id
  2. 然后创建一个Reqeust
  3. 根据创建的Reqeust创建CacheKy
  4. 再创建一个具体的Action
  5. 调用forReqeust()方法构造一个BitmapHunter,并调用其中的hunt()方法。

整体上理解起来很简单,创建Request,创建具体的Action,然后生成BitmapHunter,然后调用BitmapHunter.hunt()方法。但我们这个时候还没有看到具体都做了哪些工作,继续去看看forRequest都做了哪些事情。

static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
Action action) {
Request request = action.getRequest();
List requestHandlers = picasso.getRequestHandlers();

// Index-based loop to avoid allocating an iterator.
//noinspection ForLoopReplaceableByForEach
for (int i = 0, count = requestHandlers.size(); i < count; i++) {
RequestHandler requestHandler = requestHandlers.get(i);
if (requestHandler.canHandleRequest(request)) {
return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler);
}
}
//此处是没有找到可以处理该请求的reqeustHandler
return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
}

其实,forReqeust就做了一件事情,就是根据Request的类型,找到一个能处理这个Reqeust请求的BitmapHunter,然后生成BitmapHunter并返回。 BitmapHunter.hunt()方法,顾名思义了,这个方法就是用来执行具体的数据请求,并解码图片,然后返回的操作。这个我们先比较一下,BtimapHunter后面回过来继续分析。 还需要注意的是:正如注释当中所描述的,该方法是一个同步的填充请求的方法,而且工作在主线程上,稍不注意就会造成系统卡顿和ANR,所以我们应该尽量避免使用,而且在整个Picasso当中,我们也可以看到,Picasso保留了该接口,但并没有真正的使用它。 看完了上面同步的填充请求的方法,当然有对应的异步方法: public void fetch()

/**
* Asynchronously fulfills the request without a {@link ImageView} or {@link Target}. This is
* useful when you want to warm up the cache with an image.
* 跟上面的get()方法相对应的,这是一个异步的处理方法的请求。该方法是线程安全的,可以从任何方法进行调用。
*


* Note: It is safe to invoke this method from any thread.
*/
public void fetch() {
long started = System.nanoTime();

if (deferred) {
throw new IllegalStateException(“Fit cannot be used with fetch.”);
}
if (data.hasImage()) {
// Fetch requests have lower priority by default.
if (!data.hasPriority()) {
data.priority(Priority.LOW);
}

Request request = createRequest(started);
String key = createKey(request, new StringBuilder());

Action action = new FetchAction(picasso, request, skipMemoryCache, key, tag);
picasso.submit(action);

}
}

异步的方法与同步的方法接近,都需要创建对应的Reqeust,cacheKey,Action,但异步方法和同步方法的区别在于:

  • 两者的Action不同,同步方法直接创建了一个GetAction,而异步的方法fetch()则是创建了一个FetchAction,这两个Action有什么区别?其实两者都是简单的继承了Action父类,但不同的在于,FetchAction中添加了一个变量target,该变量在此处的实际值是ReqeustCreator的tag变量。
  • 同步方法get()会直接访问请求,而异步方法则将action提交到picasso就结束了,我们之前有看过picasso.submit(Action action)函数,忘了?没事,再来看一下。

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

    到这里就明白了,原来异步方法创建了对应的action之后,就直接提交给Dispatcher了。

但这依然不是我们常用的方法,接下来我们继续来看看ReqeustCreator中的into()方法,根据我们的经验,这个应该是常用的了吧。 into()总共有5个方法签名,分别是:

  • into(Target target)
  • into(RemoteViews remoteViews, int viewId, int notificationId,Notification notification)
  • into(RemoteViews remoteViews, int viewId, int[] appWidgetIds)
  • into(ImageView target)
  • into(ImageView target, Callback callback)

其中我们最常用的是into(Target target)和into(ImageView target)。我们首先来看看这两个方法: into(Target target)源代码:

public void into(Target target) {
long started = System.nanoTime();
checkMain();

if (target == null) {
throw new IllegalArgumentException(“Target must not be null.”);
}
if (deferred) {
throw new IllegalStateException(“Fit cannot be used with a Target.”);
}

//如果请求没有设置图片属性,即既没有设置uri,也没有设置resourceId的时候,会取消该request
if (!data.hasImage()) {
picasso.cancelRequest(target);
target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);
return;
}

//创建一个请求
Request request = createRequest(started);
String requestKey = createKey(request);

//如果设置不要跳过MemoryCache,那么就从MemoryCache中取回数据
if (!skipMemoryCache) {
//尝试从cache中获取图片
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
//如果cache hit,那么则取消相应的请求,并从Memory返回图片
if (bitmap != null) {
picasso.cancelRequest(target);
target.onBitmapLoaded(bitmap, MEMORY);
return;
}
}

//到此,图片没有cache hit,需要访问网络获取图片。
//设置默认显示的图片
target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);

//根据需要获取的图片的相关信息,创建一个Action
Action action =
new TargetAction(picasso, target, request, skipMemoryCache, errorResId, errorDrawable,
requestKey, tag);
//将action添加到队列并提交。
picasso.enqueueAndSubmit(action);
}

一看,是不是很熟悉,和刚刚的那个异步执行Reqeust的方法fetch()的思路基本上是一样的嘛:

  1. 首先判断是否工作在主线程上 (这一点和fetch()方法不同,fetch()方法不限制必须要在主线程上执行)
  2. 检查输入的合法性:包括target、deferred、data.hashImage()即是否Uri或者resourceId已经设置
  3. 创建Request、CacheKey
  4. 尝试从Cache中获取图片,如果Cache Hit,那么立刻返回结果。
  5. 如果Cache失败或者不需要Cache,则显示默认图片,然后创建Action
  6. 提交Action到dispatcher,同时将action放进Picasso中的targetToAction的Map中(需要注意,targetToAction是一个WeakReference,如果外部没有代码保持引用的话就会被GC回收掉)。

这样看来,也没有什么难题,等等,Target到底是什么呢?我们知道这个应该和我们要设定的控件有关系,但到底是做什么的呢?我们还没有看明白。 Target接口:

public interface Target {
void onBitmapLoaded(Bitmap bitmap, LoadedFrom from);
void onBitmapFailed(Drawable errorDrawable);
void onPrepareLoad(Drawable placeHolderDrawable);
}

该接口是Picasso为了实现对于任意的图形控制接口都能实现加载,而设计的一个接口,为什么这么说?我们界面中需要设置一张图片,系统只为设置图片保留了接口,但无法直接访问对应的ImageView控件,或者对应的根本就不是ImageView控件,那我们怎么办?难道就没有办法去设置图片了吗?当然不是,因为系统保留的接口,所以我们只需要重新写一个类包含该控件,并实现Target接口,在onBitmapLoaded方法中实现设置图片的逻辑即可。 此外,作者的注释还提到, Objects implementing this class must have a working implementation of {@link Object#equals(Object)} and {@link Object#hashCode()} for proper storage internally. Instances of this interface will also be compared to determine if view recycling is occurring. It is recommended that you add this interface directly on to a custom view type when using in an adapter to ensure correct recycling behavior. 个人的理解是,首先,如果需要使用Target,那么我们一定要实现hashCode()和equals()方法。因为该接口也会用来判断该View是否正在被垃圾回收。在我们使用自定义View的时候,比如Adapter中使用的时候,最好封装一个Target对象,以获得更好的垃圾回收效果。 在ReqeustCreator类中的into(Target target)方法中,作者也在注释当中为我们列举了两个使用的范例:

//实现一个View:
public class ProfileView extends FrameLayout implements Target {
@Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
setBackgroundDrawable(new BitmapDrawable(bitmap));
}

@Override public void onBitmapFailed() {
setBackgroundResource(R.drawable.profile_error);
}

@Override public void onPrepareLoad(Drawable placeHolderDrawable) {
frame.setBackgroundDrawable(placeHolderDrawable);
}
}

//在一个adapter当中实现一个holder
public class ViewHolder implements Target {
public FrameLayout frame;
public TextView name;

@Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
}

@Override public void onBitmapFailed() {
frame.setBackgroundResource(R.drawable.profile_error);
}

@Override public void onPrepareLoad(Drawable placeHolderDrawable) {
frame.setBackgroundDrawable(placeHolderDrawable);
}
}

所以,我们可以记住一条了,在Adapter中使用Picasso的时候,最好将我们的ViewHolder实现Target,不过此外还要记住,作者也特别建议我们在实现Target的时候,覆写equals()和hashCode()方法。