Picasso原始碼分析(五):into方法追本溯源和責任鏈模式建立BitmapHunter

王世暉發表於2016-06-21

Picasso原始碼分析(一):單例模式、建造者模式、面向介面程式設計
Picasso原始碼分析(二):預設的下載器、快取、執行緒池和轉換器
Picasso原始碼分析(三):快照功能實現和HandlerThread的使用
Picasso原始碼分析(四):不變模式、建造者模式和Request的預處理
Picasso原始碼分析(五):into方法追本溯源和責任鏈模式建立BitmapHunter
Picasso原始碼分析(六):BitmapHunter與請求結果的處理

Picasso非同步載入圖片流程回顧

首先通過with方法建立單例Picasso物件

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

在單例模式裡邊又通過建造者模式構建Picasso物件,並不是直接通過new建立。
載入圖片需要通過load方法告訴Picasso圖片的地址

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

load方法構造了一個RequestCreator物件並返回。
RequestCreator提供了圖片的載入和變換規則,這些變換規則包括設定佔點陣圖,圖片壓縮裁剪,顯示模式設定等,提供的設定方法如下:
RequestCreator提供的載入和變換規則

在載入和變換規則設定完成後,最終通過into方法進行網路非同步請求

into方法分析

使用into方法就是將載入到的圖片注入(顯示)在控制元件上

  /**
   * Asynchronously fulfills the request into the specified  ImageView.
   * Note: This method keeps a weak reference to the  ImageView instance and will
   * automatically support object recycling.
   */
  public void into(ImageView target) {
    into(target, null);
  }

into(ImageView target)方法呼叫了into的兩個引數的過載方法,並設定第二個方法引數為null,表示使用者沒有傳入載入成功或失敗後的回撥,所以不執行回撥。

  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 (shouldReadFromMemoryCache(memoryPolicy)) {
      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, memoryPolicy, networkPolicy, errorResId,
            errorDrawable, requestKey, tag, callback, noFade);

    picasso.enqueueAndSubmit(action);
  }

into方法首先記錄了請求的開始時間戳,隨後會根據這個事件戳建立Request請求。
接著檢查了是否在主執行緒呼叫into方法,控制元件是否為null,在非主執行緒呼叫into方法,或者控制元件為null,均會丟擲異常,對於程式中的異常進行要早發現早治療。
之後判斷該請求是否設定了uri或者資源id,如果沒有的話就取消Picasso在此控制元件上的請求,然後設定佔點陣圖並返回。

    private final Request.Builder data;
    ...
    boolean hasImage() {
      return uri != null || resourceId != 0;
    }
    ....
    if (!data.hasImage()) {
      picasso.cancelRequest(target);
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }

接著判斷使用者是否呼叫了fit方法,讓圖片自適應控制元件大小,因為必須等到控制元件完全載入顯示出來後才能夠獲取控制元件佔據的空間大小,因此需要延遲執行呼叫了fit方法的請求,控制元件顯示出來後再執行。

  public RequestCreator fit() {
    deferred = true;
    return this;
  }
  ...
  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);
    }

從原始碼中可以看到如果呼叫了fit自適應壓縮圖片,就不能呼叫resize方法手動壓縮圖片,否則丟擲”Fit cannot be used with resize.”的IllegalStateException異常;
當然呼叫了fit方法並不是一定會延遲處理,只有在控制元件沒有繪製出來的時候才會延遲處理,沒有繪製出來的控制元件呼叫getWidth()獲取到的寬度是0或者呼叫getHeight()獲取到的高度為0,此時需要設定佔點陣圖。如果獲取到的寬高都不是0,則說明控制元件已經繪製出來,此時不必設定延遲處理。
假如需要延遲處理的話,就交給Picasso的defer方法延遲處理,然後直接返回

        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;

如果不需要延遲處理,說明獲取到的控制元件的寬高都不是0,那麼就通過resize方法對圖片進行尺寸壓縮

      data.resize(width, height);

接著要為此次請求建立一個Request型別的物件,並建立相應的requestKey

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

然後如果可以從快取中獲取圖片的話,就根據requestkey
從快取中獲取對應的Bitmap物件,獲取成功的話就取消此次請求,將獲取到的bitmap顯示在控制元件上,如果設定了回撥的話還要進行請求成功的回撥,然後直接返回。

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

然後終於開始網路請求了。
通過構造一個ImageViewAction型別的物件action,然後將此action遞交給Picasso,讓Picasso去排程Dispatcher進行相應的處理。

如何構建一個Request請求

在into方法中通過createRequest方法建立了一個請求,接下來觀察下該方法是如何實現的。

  /** Create the request optionally passing it through the request transformer. */
  private Request createRequest(long started) {
    int id = nextId.getAndIncrement();

    Request request = data.build();
    request.id = id;
    request.started = started;

    boolean loggingEnabled = picasso.loggingEnabled;
    if (loggingEnabled) {
      log(OWNER_MAIN, VERB_CREATED, request.plainId(), request.toString());
    }

    Request transformed = picasso.transformRequest(request);
    if (transformed != request) {
      // If the request was changed, copy over the id and timestamp from the original.
      transformed.id = id;
      transformed.started = started;

      if (loggingEnabled) {
        log(OWNER_MAIN, VERB_CHANGED, transformed.logId(), "into " + transformed);
      }
    }
    return transformed;
}

首先通過原子整數AtomicInteger型別的nextId物件建立一個唯一的請求id

    int id = nextId.getAndIncrement();

接著呼叫Request.Builder的build方法構建Request物件(建造者模式)。
Request類有三個屬性沒有被final修飾

  /** A unique ID for the request. */
  int id;
  /** The time that the request was first submitted (in nanos). */
  long started;
  /** The  NetworkPolicy to use for this request. */
  int networkPolicy;

分別是請求id,請求時間started和網路策略networkPolicy,因為這三個屬性是非final的,所以建造者模式中的build方法並未為Request物件設定這三個屬性,所以需要建立出Request物件後直接設定這些屬性。

    Request request = data.build();
    request.id = id;
    request.started = started;

建立Request物件後還需要對該Request進行加工變換處理,變換後返回Request物件。

為請求Request建立快取的key

在into方法中,會根據建立的Request物件建立一個key,這個key就是用來建立和讀取快取的key。

    String requestKey = createKey(request);
  static String createKey(Request data) {
    String result = createKey(data, MAIN_THREAD_KEY_BUILDER);
    MAIN_THREAD_KEY_BUILDER.setLength(0);
    return result;
  }
    static String createKey(Request data, StringBuilder builder) {
    if (data.stableKey != null) {
      builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);
      builder.append(data.stableKey);
    } else if (data.uri != null) {
      String path = data.uri.toString();
      builder.ensureCapacity(path.length() + KEY_PADDING);
      builder.append(path);
    } else {
      builder.ensureCapacity(KEY_PADDING);
      builder.append(data.resourceId);
    }
    builder.append(KEY_SEPARATOR);

    if (data.rotationDegrees != 0) {
      builder.append("rotation:").append(data.rotationDegrees);
      if (data.hasRotationPivot) {
        builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
      }
      builder.append(KEY_SEPARATOR);
    }
    if (data.hasSize()) {
      builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
      builder.append(KEY_SEPARATOR);
    }
    if (data.centerCrop) {
      builder.append("centerCrop").append(KEY_SEPARATOR);
    } else if (data.centerInside) {
      builder.append("centerInside").append(KEY_SEPARATOR);
    }

    if (data.transformations != null) {
      //noinspection ForLoopReplaceableByForEach
      for (int i = 0, count = data.transformations.size(); i < count; i++) {
        builder.append(data.transformations.get(i).key());
        builder.append(KEY_SEPARATOR);
      }
    }

    return builder.toString();
}

可見為請求建立key的策略是這樣的,先設定key的頭部,三種互斥情況,請求的stableKey屬性,請求的uri,資源id。

    if (data.stableKey != null) {
      builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);
      builder.append(data.stableKey);
    } else if (data.uri != null) {
      String path = data.uri.toString();
      builder.ensureCapacity(path.length() + KEY_PADDING);
      builder.append(path);
    } else {
      builder.ensureCapacity(KEY_PADDING);
      builder.append(data.resourceId);
    }

之後新增一個換行符(之後的各種資訊之間也以換行符分割)

    builder.append(KEY_SEPARATOR);

如果該請求設定了旋轉角度的話,key中要包含該旋轉資訊

    if (data.rotationDegrees != 0) {
      builder.append("rotation:").append(data.rotationDegrees);
      if (data.hasRotationPivot) {
        builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
      }
      builder.append(KEY_SEPARATOR);
    }

如果該請求呼叫了resize方法進行手動壓縮圖片,需要把設定的大小也要新增進快取的key中,之後還有顯示位置資訊和一系列變換規則都會新增到key中。

提交請求任務Action給Picasso排程

在into方法的最後會建立一個ImageViewAction物件,該物件繼承自Action,Action是一個抽象類,表示一個獲取圖片的抽象動作。抽象類Action有兩個抽象方法必須由子類實現,分別是獲取圖片成功的complete方法和獲取圖片失敗的error方法。ImageViewAction對這兩個抽象方法進行了重寫,獲取圖片成功就在控制元件顯示獲取到的圖片,失敗就顯示佔點陣圖。

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

    picasso.enqueueAndSubmit(action);

into方法的最後把建立的action交給了Picasso

  void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    if (target != null && targetToAction.get(target) != action) {
      // This will also check we are on the main thread.
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    submit(action);
  }

Picasso會判斷該action對應的控制元件上是不是已經有請求在進行了,有的話就取消之前的請求,因為一個控制元件上沒必要進行多次請求,只保留最後一次的請求即可,節約資源。

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

Picasso實際上是把action交給了Dispatcher物件去排程

  ...
  static class DispatcherThread extends HandlerThread {
    DispatcherThread() {
      super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
    }
  }
  ...
  this.dispatcherThread = new DispatcherThread();
  this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
  ...
  void dispatchSubmit(Action action) {
    handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
  }

可見排程器的handler繫結了dispatcherThread的looper,所以任務處理方法handlerMessage方法會在dispatchThread所在的執行緒執行。

    @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        case REQUEST_SUBMIT: {
          Action action = (Action) msg.obj;
          dispatcher.performSubmit(action);
          break;
          ...

這樣通過handler達到了執行緒切換的目的,從主執行緒切換到了工作者執行緒,並在工作者執行緒呼叫dispatcher.performSubmit(action)方法,進行真正的耗時請求

  void performSubmit(Action action) {
    performSubmit(action, true);
  }
  void performSubmit(Action action, boolean dismissFailed) {
    if (pausedTags.contains(action.getTag())) {
      pausedActions.put(action.getTarget(), action);
      if (action.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
            "because tag '" + action.getTag() + "' is paused");
      }
      return;
    }

    BitmapHunter hunter = hunterMap.get(action.getKey());
    if (hunter != null) {
      hunter.attach(action);
      return;
    }

    if (service.isShutdown()) {
      if (action.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
      }
      return;
    }

    hunter = forRequest(action.getPicasso(), this, cache, stats, action);
    hunter.future = service.submit(hunter);
    hunterMap.put(action.getKey(), hunter);
    if (dismissFailed) {
      failedActions.remove(action.getTarget());
    }

    if (action.getPicasso().loggingEnabled) {
      log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
    }
  }

performSubmit方法先判斷此action的tag是不是被暫停執行了,是的話就不用處理直接返回就好。

    if (pausedTags.contains(action.getTag())) {
      pausedActions.put(action.getTarget(), action);
      if (action.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
            "because tag '" + action.getTag() + "' is paused");
      }
      return;
    }

該action已經分配BitmapHunter的話也會直接返回,不重複為同一個action分配多個hunter。

    BitmapHunter hunter = hunterMap.get(action.getKey());
    if (hunter != null) {
      hunter.attach(action);
      return;
    }

執行緒池已經關閉的話也會直接返回

    BitmapHunter hunter = hunterMap.get(action.getKey());
    if (hunter != null) {
      hunter.attach(action);
      return;
    }

經過上邊三重檢查,終於可以為該action分配hunter進行圖片請求了

    hunter = forRequest(action.getPicasso(), this, cache, stats, action);

最後將這個hunter提交該執行緒池處理

通過責任鏈模式為Action建立BitmapHunter

  static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
      Action action) {
    Request request = action.getRequest();
    List<RequestHandler> 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);
      }
    }

    return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
    }

Picasso在建構函式裡邊建立了多個RequestHandler,這些RequestHandler各司其職,能從多個地方載入圖片,最常用的是NetworkRequestHandler。

    List<RequestHandler> allRequestHandlers =
        new ArrayList<RequestHandler>(builtInHandlers + extraCount);

    // ResourceRequestHandler needs to be the first in the list to avoid
    // forcing other RequestHandlers to perform null checks on request.uri
    // to cover the (request.resourceId != 0) case.
    allRequestHandlers.add(new ResourceRequestHandler(context));
    if (extraRequestHandlers != null) {
      allRequestHandlers.addAll(extraRequestHandlers);
    }
    allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
    allRequestHandlers.add(new MediaStoreRequestHandler(context));
    allRequestHandlers.add(new ContentStreamRequestHandler(context));
    allRequestHandlers.add(new AssetRequestHandler(context));
    allRequestHandlers.add(new FileRequestHandler(context));
    allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));
    requestHandlers = Collections.unmodifiableList(allRequestHandlers);

forRequest方法中遍歷list,找到能處理該request的RequestHandler後,直接使用該RequestHandler構建Bitmap物件返回。
隨後會分析BitmapHunter的原始碼。

相關文章