Picasso原始碼分析(四):不變模式、建造者模式和Request的預處理

王世暉發表於2016-06-14

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

Request的不變模式(Immutable Pattern)

  不變模式可增強物件的強壯型,允許多個物件共享某一個物件,降低了對該物件進行併發訪問時的同步化開銷。如果需要修改一個不變物件的狀態,那麼就需要建立一個新的同型別物件,並在建立時將這個新的狀態儲存在新物件裡。
  不變模式只涉及到一個類。一個類的內部狀態建立後,在整個生命週期都不會發生變化時,這樣的類稱作不變類。
  不變模式的優點:

  1. 因為不能修改一個不變物件的狀態,所以可以避免由此引起的不必要的程式錯誤;換言之,一個不變的物件要比可變的物件更加容易維護。
  2. 因為沒有任何一個執行緒能夠修改不變物件的內部狀態,一個不變物件自動就是執行緒安全的,這樣就可以省掉處理同步化的開銷。一個不變物件可以自由地被不同的客戶端共享。

Picasso中的Request類使用了不變模式,Request類被final修飾,絕大部分屬性也被final修飾。final的作用有三個:

  1. final修飾類表示類不可以被繼承
  2. final修飾方法表示方法不可被子類覆蓋,但可以被繼承
  3. final修飾變數,如果是基本資料型別的變數,則其數值一旦在初始化之後便不能更改;如果是引用型別的變數,則在對其初始化之後便不能再讓其指向另一個物件。
/** Immutable data about an image and the transformations that will be applied to it. */
public final class Request {
  ...
  public final String stableKey;
  /** List of custom transformations to be applied after the built-in transformations. */
  public final List<Transformation> transformations;
  /** Target image width for resizing. */
  public final int targetWidth;
  /** Target image height for resizing. */
  public final int targetHeight;
  ...

Request類有三個屬性並沒有被final修飾,因為這三個屬性賦值時機並非在建構函式期間或程式碼塊中,所以如果被final修飾就必須在宣告時賦值或者在構造或者程式碼塊中賦值,顯然邏輯不通,而其他被final修飾的屬性均在Request的建構函式中進行了賦值。

  private Request(Uri uri, int resourceId, String stableKey, List<Transformation> transformations,
      int targetWidth, int targetHeight, boolean centerCrop, boolean centerInside,
      boolean onlyScaleDown, float rotationDegrees, float rotationPivotX, float rotationPivotY,
      boolean hasRotationPivot, Bitmap.Config config, Priority priority) {
    this.uri = uri;
    this.resourceId = resourceId;
    this.stableKey = stableKey;
    if (transformations == null) {
      this.transformations = null;
    } else {
      this.transformations = unmodifiableList(transformations);
    }
    this.targetWidth = targetWidth;
    this.targetHeight = targetHeight;
    this.centerCrop = centerCrop;
    this.centerInside = centerInside;
    this.onlyScaleDown = onlyScaleDown;
    this.rotationDegrees = rotationDegrees;
    this.rotationPivotX = rotationPivotX;
    this.rotationPivotY = rotationPivotY;
    this.hasRotationPivot = hasRotationPivot;
    this.config = config;
    this.priority = priority;
    }

可見final屬性均在建構函式中進行了賦值初始化。
Request的建構函式非常有特色,第一是用private進行修飾,第二是引數非常多,有15個引數,從這兩點可以看到Request必然要使用建造者模式建立物件。

Request的建造者模式(Builder Pattern)

由於Request的建構函式引數非常多,因此必然要使用建造者模式來建立Request物件,通過方法鏈來設定眾多屬性,屬性設定後通過build方法建立Request物件。

  /** Builder for creating {@link Request} instances. */
  public static final class Builder {
    private Uri uri;
    private int resourceId;
    private String stableKey;
    private int targetWidth;
    private int targetHeight;
    private boolean centerCrop;
    private boolean centerInside;
    private boolean onlyScaleDown;
    private float rotationDegrees;
    private float rotationPivotX;
    private float rotationPivotY;
    private boolean hasRotationPivot;
    private List<Transformation> transformations;
    private Bitmap.Config config;
    private Priority priority;
    ...

可見Request的內部類Builder的所有屬性正是Request的所有final屬性。

  /**
   * The image URI.
   * This is mutually exclusive with {@link #resourceId}.
   */
  public final Uri uri;
  /**
   * The image resource ID.
   * This is mutually exclusive with {@link #uri}.
   */
  public final int resourceId;

對於一個Request來說,其uri和resourceId是互斥的,只能二選一。
Builder內部類保證了二者只能選其一。

    /**
     * Set the target image Uri.
     * This will clear an image resource ID if one is set.
     */
    public Builder setUri(Uri uri) {
      if (uri == null) {
        throw new IllegalArgumentException("Image URI may not be null.");
      }
      this.uri = uri;
      this.resourceId = 0;
      return this;
    }
    /**
     * Set the target image resource ID.
     * This will clear an image Uri if one is set.
     */
    public Builder setResourceId(int resourceId) {
      if (resourceId == 0) {
        throw new IllegalArgumentException("Image resource ID may not be 0.");
      }
      this.resourceId = resourceId;
      this.uri = null;
      return this;
    }

以設定優先順序為例分析如何通過方法鏈設定屬性

    /** Execute request using the specified priority. */
    public Builder priority(Priority priority) {
      if (priority == null) {
        throw new IllegalArgumentException("Priority invalid.");
      }
      if (this.priority != null) {
        throw new IllegalStateException("Priority already set.");
      }
      this.priority = priority;
      return this;
    }

其他屬性的設定方法類似,都是先檢查是不是傳入的引數是空指標,是不是重複設定了引數,最後對引數進行賦值,返回this指向自身的引用方便鏈式呼叫。
Build方法裡邊對已經設定的屬性進行邏輯檢查和預設屬性設定,最後通過Request的15個引數的私有構造器進行了Request的物件建立

    /** Create the immutable {@link Request} object. */
    public Request build() {
      if (centerInside && centerCrop) {
        throw new IllegalStateException("Center crop and center inside can not be used together.");
      }
      if (centerCrop && (targetWidth == 0 && targetHeight == 0)) {
        throw new IllegalStateException(
            "Center crop requires calling resize with positive width and height.");
      }
      if (centerInside && (targetWidth == 0 && targetHeight == 0)) {
        throw new IllegalStateException(
            "Center inside requires calling resize with positive width and height.");
      }
      if (priority == null) {
        priority = Priority.NORMAL;
      }
      return new Request(uri, resourceId, stableKey, transformations, targetWidth, targetHeight,
          centerCrop, centerInside, onlyScaleDown, rotationDegrees, rotationPivotX, rotationPivotY,
          hasRotationPivot, config, priority);
    }

Picasso中的load方法分析

load方法可以用來載入圖片資源,可以從連結Uri、檔案路徑、檔案File和資源id進行資源載入,因此Picasso的圖片載入源還是比較靈活多樣化的。
而從檔案和從檔案路徑載入圖片會先把檔案或者路徑轉換為Uri,然後從Uri載入圖片

  public RequestCreator load(File file) {
    if (file == null) {
      return new RequestCreator(this, null, 0);
    }
    return load(Uri.fromFile(file));
  }
    public RequestCreator load(String path) {
    if (path == null) {
      return new RequestCreator(this, null, 0);
    }
    if (path.trim().length() == 0) {
      throw new IllegalArgumentException("Path must not be empty.");
    }
    return load(Uri.parse(path));
  }

而從Uri和資源id載入圖片是互斥的,只能二選一,所以從Uri和從資源id去load圖片是分開的。

  public RequestCreator load(Uri uri) {
    return new RequestCreator(this, uri, 0);
  }
  public RequestCreator load(int resourceId) {
    if (resourceId == 0) {
      throw new IllegalArgumentException("Resource ID must not be zero.");
    }
    return new RequestCreator(this, null, resourceId);
  }

載入圖片load方法建立了RequestCreator物件並委託其進行圖片的載入,通過建構函式引數的不同來進行兩種形式的不同載入。按照Effective Java的建議,此處應該使用靜態工廠方式區分兩種不同的型別而不是通過建構函式的賦值不同來區分。

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

而RequestCreator又委託Request的Builder進行Request物件的建立。
而通過RequestCreator,就可以為一個請求設定佔點陣圖、網路策略和記憶體策略 等。

佔點陣圖設定

以圖片載入失敗顯示的佔點陣圖為例

  /** An error drawable to be used if the request image could not be loaded. */
  public RequestCreator error(Drawable errorDrawable) {
    if (errorDrawable == null) {
      throw new IllegalArgumentException("Error image may not be null.");
    }
    if (errorResId != 0) {
      throw new IllegalStateException("Error image already set.");
    }
    this.errorDrawable = errorDrawable;
    return this;
  }

還是先檢查傳入的Drawable資源是不是空指標,是不是重複設定,最後返回this引用方便鏈式呼叫。

fit方法自動壓縮圖片

其中fit方法值得一提,呼叫fit方法可以對圖片進行壓縮到控制元件ImageView的尺寸後再顯示到控制元件,節約了記憶體。這是導致此Request請求延遲執行,直到控制元件完全繪製出來,繪製後才能知道控制元件佔據的空間大小。

  /**
   * Attempt to resize the image to fit exactly into the target ImageView's bounds. This
   * will result in delayed execution of the request until the  ImageView has been laid out.
   * Note: This method works only when your target is an ImageView.
   */
  public RequestCreator fit() {
    deferred = true;
    return this;
  }

resize手動壓縮圖片

fit方法自動壓縮圖片,而resize方法可以手動壓縮圖片尺寸

  ...
  private final Request.Builder data;
  ...
  /** Resize the image to the specified size in pixels. */
  public RequestCreator resize(int targetWidth, int targetHeight) {
    data.resize(targetWidth, targetHeight);
    return this;
  }

data是一個Request.Builder型別的資料,因此RequestCreator的resize方法實際上是直接委託給了Request.Builder的方法。
還有其他的一些設定比如 centerCrop、centerInside、onlyScaleDown、rotate、config、stableKey、transform和tag方法均是委託給了Request.Builder的方法的方法。
這些方法主要是設定了圖片的載入規則和變換規則,這樣就對請求進行了配置,配置後就可以愉快地通過into方法進行非同步載入圖片了,下一節將從into方法開始分析。

相關文章