Picasso-原始碼解析(三)

weixin_34370347發表於2019-02-25

前言

前面介紹了比較重要的Picasso物件的使用,以及他的一些重要方法,其實還有一個物件也很重要 ---- RequestCreator,在第一篇文章中其實我已經介紹了,RequestCreator的最終是為了生成一個Request物件。

public方法

image.png

太多了,但是這些方法其實實現都非常的簡單,舉一個例子。

 public RequestCreator centerCrop() {
    data.centerCrop(Gravity.CENTER);
    return this;
  }

//--------Request.java--------
 public Builder centerCrop(int alignGravity) {
      if (centerInside) {
        throw new IllegalStateException("Center crop can not be used after calling centerInside");
      }
      centerCrop = true;
      centerCropGravity = alignGravity;
      return this;
    }
複製程式碼

其實RequestCreator的方法每個都很簡單就是用來給Request.Builder物件設定引數的。Request.Builder是用來生成Request的,所以還是Request這個物件,然後來看裡面的一些配置引數,以及引數的作用。

image.png

一個個來介紹下,public方法和屬性

  1. buildUpon
 public Builder buildUpon() {
    return new Builder(this);
  }
複製程式碼

根據request重新構建出一個新的Request.Builder物件,可以在原來的基礎之上再呼叫鏈式方法建立新的一個Request.Builder物件。

  1. hasSize
 public boolean hasSize() {
    return targetWidth != 0 || targetHeight != 0;
  }
複製程式碼

很簡單,就是看你有沒有resize 寬高

上面是2個方法,下面說屬性。

  1. centerCrop
 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;
      }
...
複製程式碼

在新增centerCrop的時候必須設定targetWidthtargetHeight

 Picasso.get().load("http://i.imgur.com/DvpvklR.png")
                .centerCrop()
                .into(ivTest)
複製程式碼

這樣會報錯, 應該這樣

Picasso.get().load("http://i.imgur.com/DvpvklR.png")
                .centerCrop()
                .resize(100,100)
                .into(ivTest)
複製程式碼

真正實現centerCrop是在圖片載入後進行處理。

 if (data.centerCrop) {
        //獲取圖片要縮放的寬和圖片真正的寬的比例
        float widthRatio =
            targetWidth != 0 ? targetWidth / (float) inWidth : targetHeight / (float) inHeight;
        //獲取圖片要縮放的高和圖片真正的高的比例
        float heightRatio =
            targetHeight != 0 ? targetHeight / (float) inHeight : targetWidth / (float) inWidth;
        float scaleX, scaleY;
        //如果寬的比例大於高的比例,就說明圖片在y方向上需要被裁減
        if (widthRatio > heightRatio) {
          int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
          //裁剪後有個對齊的方向,是上對齊,還是下對齊,預設是中對齊
          if ((data.centerCropGravity & Gravity.TOP) == Gravity.TOP) {
            drawY = 0;
          } else if ((data.centerCropGravity & Gravity.BOTTOM) == Gravity.BOTTOM) {
            drawY = inHeight - newSize;
          } else {
            drawY = (inHeight - newSize) / 2;
          }
          drawHeight = newSize;
          scaleX = widthRatio;
          scaleY = targetHeight / (float) drawHeight;
        } else if (widthRatio < heightRatio) {
          //這裡跟前面是一樣的邏輯,只不過是x軸方向上需要被裁減,以及左對齊,還是右對齊
          int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
          if ((data.centerCropGravity & Gravity.LEFT) == Gravity.LEFT) {
            drawX = 0;
          } else if ((data.centerCropGravity & Gravity.RIGHT) == Gravity.RIGHT) {
            drawX = inWidth - newSize;
          } else {
            drawX = (inWidth - newSize) / 2;
          }
          drawWidth = newSize;
          scaleX = targetWidth / (float) drawWidth;
          scaleY = heightRatio;
        } else {
          drawX = 0;
          drawWidth = inWidth;
          scaleX = scaleY = heightRatio;
        }
        if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
          matrix.preScale(scaleX, scaleY);
        }
      }
複製程式碼

其實這裡的程式碼很多,但是我們可以從效果上去看,就是對圖片進行了 裁剪和縮放,然後以及對齊。這樣就很清楚了,上面程式碼只是為了獲取到。

  • scaleX 圖片寬的縮放
  • scaleY 圖片高的縮放
  • drawX 根據對齊操作,判斷從圖片的哪個點開始繪製
  • drawY 根據對齊操作,判斷從圖片的哪個點開始繪製
  1. centerCropGravity 上面已經介紹了,就是centerCrop的對齊的屬性

  2. centerInside 其實跟centerCrop差不多,就是對圖片的一個適配。

else if (data.centerInside) {
        // Keep aspect ratio if one dimension is set to 0
        float widthRatio =
            targetWidth != 0 ? targetWidth / (float) inWidth : targetHeight / (float) inHeight;
        float heightRatio =
            targetHeight != 0 ? targetHeight / (float) inHeight : targetWidth / (float) inWidth;
        float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
        if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
          matrix.preScale(scale, scale);
        }
      }
複製程式碼

程式碼其實就非常的簡單,分別獲取到寬高的縮放比例,然後取最小的,這樣保證圖片在imageview裡面都看得到,並且不變形。

  1. config 配置圖片的格式,可以是ARGB_8888,RGB_565等
//---------RequestCreator.java---------
//呼叫鏈式方法,可配置,最終其實是傳到`Request`物件中
 public RequestCreator config(@NonNull Bitmap.Config config) {
    data.config(config);
    return this;
  }

//-------RequestHandler.java--------
  static BitmapFactory.Options createBitmapOptions(Request data) {
    final boolean justBounds = data.hasSize();
    final boolean hasConfig = data.config != null;
    BitmapFactory.Options options = null;
    if (justBounds || hasConfig || data.purgeable) {
      options = new BitmapFactory.Options();
      options.inJustDecodeBounds = justBounds;
      options.inInputShareable = data.purgeable;
      options.inPurgeable = data.purgeable;
      if (hasConfig) {
        //最終其實是在建立 BitmapFactory.Options的時候當作引數傳入,這樣子生成的圖片就會按照配置,如果不對圖片質量有很高要求的話可以選擇RGB_565,節省了一半的記憶體
        options.inPreferredConfig = data.config;
      }
    }
    return options;
  }

//---------BitmapFactory.java----------
       /**
       * If this is non-null, the decoder will try to decode into this
         * internal configuration. If it is null, or the request cannot be met,
         * the decoder will try to pick the best matching config based on the
         * system's screen depth, and characteristics of the original image such
         * as if it has per-pixel alpha (requiring a config that also does).
         * 
         * Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by
         * default.
         */
        public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
複製程式碼

其實呢這裡已經說的很清楚了,如果配置config,那麼就按你配置的來,如果沒有配置,預設是Bitmap.Config.ARGB_8888

  1. hasRotationPivot,rotationDegrees,rotationPivotX,rotationPivotY
//----------Request.java------------
 public Builder rotate(float degrees, float pivotX, float pivotY) {
      rotationDegrees = degrees;
      rotationPivotX = pivotX;
      rotationPivotY = pivotY;
      hasRotationPivot = true;
      return this;
    }
複製程式碼

其實這裡就可以看出4個引數的作用 hasRotationPivot:是否設定了旋轉引數 rotationDegrees:旋轉角度 rotationPivotX,rotationPivotY:旋轉中心點x,y

6.onlyScaleDown

 if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) {
          matrix.preScale(scaleX, scaleY);
        }

....
 private static boolean shouldResize(boolean onlyScaleDown, int inWidth, int inHeight,
      int targetWidth, int targetHeight) {
    return !onlyScaleDown || (targetWidth != 0 && inWidth > targetWidth)
            || (targetHeight != 0 && inHeight > targetHeight);
  }
複製程式碼

onlyScaleDown,顧名思義,只允許縮小,所以每次在縮放的時候,先判斷下。

  1. priority 這個其實就是優先順序的意思
//-------Picasso.java--------
 public enum Priority {
    LOW,
    NORMAL,
    HIGH
  }
複製程式碼

從定義上看是3種優先順序,從高到低。在構建Request物件的時候會給預設值

//--------Request.java--------
public Request build() {
     ...
      if (priority == null) {
        priority = Priority.NORMAL;
      }
     ...
複製程式碼

那麼設定這個優先順序到底有什麼用呢,其實還要看PicassoExecutorService這個關鍵類

PicassoExecutorService() {
    super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
        new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
  }
複製程式碼

這裡非常有意思,在構建一個執行緒池的時候,建立了一個PriorityBlockingQueue有優先順序的一個阻塞佇列。

@Override
  public Future<?> submit(Runnable task) {
    //每次執行的是一個PicassoFutureTask任務
    PicassoFutureTask ftask = new PicassoFutureTask((BitmapHunter) task);
    execute(ftask);
    return ftask;
  }

  private static final class PicassoFutureTask extends FutureTask<BitmapHunter>
      implements Comparable<PicassoFutureTask> {
    private final BitmapHunter hunter;

    PicassoFutureTask(BitmapHunter hunter) {
      super(hunter, null);
      this.hunter = hunter;
    }
    
     //任務提交之後,執行緒池會一個個進行處理,會對加入的物件做下對比,看哪個優先順序高,就先執行哪個
    @Override
    public int compareTo(PicassoFutureTask other) {
      Picasso.Priority p1 = hunter.getPriority();
      Picasso.Priority p2 = other.hunter.getPriority();
      return (p1 == p2 ? hunter.sequence - other.hunter.sequence : p2.ordinal() - p1.ordinal());
    }
  }
}
複製程式碼
  1. purgeable
//----------Request.java---------
public Builder purgeable() {
      purgeable = true;
      return this;
    }
複製程式碼

其實在第5點的config裡面是有涉及到purgeable這個的,其實也是BitmapFactory.Options的一個引數。 這裡我把另外一個引數合起來一起說,就是inInputShareable,這裡我把原始碼的註釋貼出,然後解釋下。

 /**
         * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
         * ignored.
         *
         * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
         * is set to true, then the resulting bitmap will allocate its
         * pixels such that they can be purged if the system needs to reclaim
         * memory. In that instance, when the pixels need to be accessed again
         * (e.g. the bitmap is drawn, getPixels() is called), they will be
         * automatically re-decoded.
         *
         * <p>For the re-decode to happen, the bitmap must have access to the
         * encoded data, either by sharing a reference to the input
         * or by making a copy of it. This distinction is controlled by
         * inInputShareable. If this is true, then the bitmap may keep a shallow
         * reference to the input. If this is false, then the bitmap will
         * explicitly make a copy of the input data, and keep that. Even if
         * sharing is allowed, the implementation may still decide to make a
         * deep copy of the input data.</p>
         *
         * <p>While inPurgeable can help avoid big Dalvik heap allocations (from
         * API level 11 onward), it sacrifices performance predictability since any
         * image that the view system tries to draw may incur a decode delay which
         * can lead to dropped frames. Therefore, most apps should avoid using
         * inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
         * allocations use the {@link #inBitmap} flag instead.</p>
         *
         * <p class="note"><strong>Note:</strong> This flag is ignored when used
         * with {@link #decodeResource(Resources, int,
         * android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
         * android.graphics.BitmapFactory.Options)}.</p>
         */
 @Deprecated
        public boolean inPurgeable;
複製程式碼

首先這是棄用的一個屬性,註釋裡也說的很清楚,LOLLIPOP及以上版本就無效了,在KITKAT及以下版本,有效。 翻譯下: 如果設定為true,那麼生成bitmap而去申請的一塊記憶體,會在系統需要記憶體的時候,被回收。如果說這個bitmap又被呼叫拿去使用了,那麼就跟inInputShareable這個屬性有關,如果這個屬性設定了false,那麼,就會對inputdata做一個深拷貝,如果是true的話,一開始就會先使用input的一個引用,但是最後真正要使用到bitmap的時候,還是會對inputdata做一次深拷貝。

當然,後面還有最後一句比較關鍵的話Therefore, most apps should avoid using inPurgeable to allow for a fast and fluid UI. 意思就是說,如果你想你的app比較流暢,比較快,那麼你就不要去使用,因為重新去解碼一張圖片是要時間的,這樣很可能就會造成你載入圖片的時候白了幾秒。

當然,在記憶體緊張的時候是可以使用的。

static BitmapFactory.Options createBitmapOptions(Request data) {
   ...
      options.inInputShareable = data.purgeable;
      options.inPurgeable = data.purgeable;
     ...
    return options;
  }
複製程式碼

很顯然PicassoinInputShareable,inPurgeable綁在了一起,要麼都true,要麼都false

  1. resourceId 如果要直接載入R.mipmap.**,R.drawable.**這種資源的話,這個引數就!=0

  2. stableKey

//key-value存入LruCache中,這裡就是key的生成規則
 static String createKey(Request data, StringBuilder builder) {
    //先看有沒有設定stableKey,有設定的話,就直接生成跟stableKey有關的一段字串,後面可以直接使用stableKey,獲取到需要的value。
    if (data.stableKey != null) {
      builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);
      builder.append(data.stableKey);
    } else if (data.uri != null) {
      //如果有說沒有上面的stableKey,但是有uri,那麼就直接根據uri生成對應的key
      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);

複製程式碼

正常情況下,其實你的圖片的uri不變的話,直接就uri就夠用了,但是如果你圖片的uri可能會發生改變,然後本身其實是一張圖片的話,其實是可以使用stableKey。

比如說,你圖片存放了三方伺服器,然後可以使用http://****/x100/y100這種裁減的方式的話,那麼就可以使用stableKey,因為你本身原圖是一張,然後呢正常情況下你可能會下載了很多不通尺寸的圖片,然後根據uri儲存在LruCache中。

但是這裡有一個優化的方式了,設定一個stableKey,只儲存一份圖片,然後再對圖片進行縮放或者裁減,這樣就防止存放了很多份不同尺寸的圖片。

  1. targetHeight,targetWidth 當呼叫了resize方法,重新定義寬高的話,targetHeight,targetWidth!=0 如:
 Picasso.get().load("http://i.imgur.com/DvpvklR.png")
                .resize(100,100)
                .into(ivTest)
複製程式碼
  1. transformations 用來進行圖片的一個變換的列表。 比如我要切成一個圓圖
 Picasso.get().load("http://i.imgur.com/DvpvklR.png")
                .transform(object : Transformation {
                    override fun transform(source: Bitmap?): Bitmap {
                        //重新建立一個新的bitmap
                        val bitmap = Bitmap.createBitmap(source?.width!!, source?.height!!, Bitmap.Config.ARGB_8888)
                        val canvas = Canvas(bitmap)
                        val p = Paint(Paint.ANTI_ALIAS_FLAG)
                        canvas.drawCircle(100f, 100f, 100f, p)
                        p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
                        canvas.drawBitmap(source, 0f, 0f, p)
                        //這裡必須要對原來的bitmap做recycle,不然會報錯,後面
                        source?.recycle()
                        return bitmap
                    }
                    override fun key() = "test111"
                })
                .resize(200, 200)
                .into(ivTest)

//-------BitmapHunter.java---------
static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
    for (int i = 0, count = transformations.size(); i < count; i++) {
      final Transformation transformation = transformations.get(i);
      Bitmap newResult;
      try {
        newResult = transformation.transform(result);
      } catch (final RuntimeException e) {
        Picasso.HANDLER.post(new Runnable() {
          @Override public void run() {
            throw new RuntimeException(
                "Transformation " + transformation.key() + " crashed with exception.", e);
          }
        });
        return null;
      }

      if (newResult == null) {
        final StringBuilder builder = new StringBuilder() //
            .append("Transformation ")
            .append(transformation.key())
            .append(" returned null after ")
            .append(i)
            .append(" previous transformation(s).\n\nTransformation list:\n");
        for (Transformation t : transformations) {
          builder.append(t.key()).append('\n');
        }
        Picasso.HANDLER.post(new Runnable() {
          @Override public void run() {
            throw new NullPointerException(builder.toString());
          }
        });
        return null;
      }
      //如果有轉換,必須要對原來的bitmap做recycle
      if (newResult == result && result.isRecycled()) {
        Picasso.HANDLER.post(new Runnable() {
          @Override public void run() {
            throw new IllegalStateException("Transformation "
                + transformation.key()
                + " returned input Bitmap but recycled it.");
          }
        });
        return null;
      }
...
複製程式碼
  1. uri 載入圖片的uri,沒有什麼好介紹的。

總結

相對來說圖片框架中,Picasso是比較簡單,比較容易看得懂的,所以,如果想看圖片框架原始碼的話,建議可以從Picasso原始碼入手,先看到一些圖片框架的基本的一些功能,然後可以嘗試GlideFresco,那一定會受益匪淺的。最終可以自己上手寫一份,我就是朝著這個目標前進的。fighting!!!

相關文章