簡單說說我最常用的圖片加載庫Picasso

z_jide 7年前發布 | 8K 次閱讀 Bitmap 安卓開發 Android開發 移動開發

簡單說說我最常用的圖片加載庫Picasso

對于一個需要展示很多圖片或較多圖片的App來說,一個好的圖片加載框架是必不可少的。在我剛開始學習Android的時候,什么都想自己寫,我以前做過一個看漫畫的App,里面的圖片加載,緩存等都是自己寫的,但是效果并不理想,當時利用了 LruCache 來做圖片的內存緩存,用 DiskLruCache 做磁盤緩存,加載圖片根據Key先在內存中查詢,如果沒有就再去磁盤中查詢,若果再沒有在去網絡上加載。雖然是個邏輯上非常清楚的事,但是要真的做好還是有一定的難度的。后來我認識到了 Picasso ,一個優雅、強大的圖片加載框架,我使用它制作了許多的項目。雖然 Picasso 不是最強大的,現在有 GlideFresco 這些更強大的圖片加載庫,但在我眼里 Picasso 絕對是最優雅的,從它的API,設計方法,源碼看, Picasso 集輕量、實用于一身。

第一眼

認識它的第一眼肯定是它超級簡短的使用方法,只需要一行代碼,鏈式配置我們要加載的圖片和一些加載配置和指定的加載Target,我們的圖片就正確的加載并顯示在了界面上,不可謂不優雅。

Picasso.with(context).load(url).into(imageView);

我們還可以很方便的配置各種加載選項,比如設置占位圖,設置錯誤圖,設置是否需要對圖片進行變換,圖片顯示是否需要淡入效果,對圖片的大小進行調整等常用的配置,這些都只需要一行代碼就可以解決。

那么它是怎么做到的呢?作為一個開發者,首先要學會使用一個庫,在學會了使用后,我們要去了解它,了解它的設計,了解它的實現,從中學習優秀的設計思想和編碼技巧。

掌握一切的男人——Picasso(畢加索)

從使用上看, Picasso 類無疑是掌握著一切的類,作為一個全局單例類,它掌握著各種配置(內存、磁盤、BitmapConfig、線程池等),提供各種簡單有用的方法并隱藏了內部的各種實現,是我們使用上的超級核心類。Picasso的構造方法提供包級別的訪問權限,我們不能直接new出來,但我們可以很簡單的通過

Picasso.with(context)

來獲取這個全局單例對象,來看一下 with() 方法

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

一個很經典的單例實現,內部使用建造者模式對 Picasso 對象進行構造。對于大多數簡單的應用,只需要使用默認的配置就好了。但是對于一些應用,我們也可以通過 Picasso.Builder 來進行自定義的配置。我們可以根據類似下面的代碼進行Picasso的配置

Picasso picasso = new Picasso.Builder(this)
        .loggingEnabled(true)
        .defaultBitmapConfig(Bitmap.Config.RGB_565)
        .build();
Picasso.setSingletonInstance(picasso);

由于Picasso是一個非常復雜的對象,根據不同的配置會產生不同的表現,這里通過建造者模式來構建對象很好的將構建和其表現分離。

RequestCreator

通過Picasso對象,我們可以load各種圖片資源,這個資源可以字符路徑、Uri、資源路徑,文件等形式提供,之后Picasso會發揮一個 RequestCreator 對象,它可以根據我們需求以及圖片資源生成相應的 Request 對象。之后 Request 會生成 Action* 對象,之后會分析這一系列的過程。由于 RequestCreator 的配置方法都返回自身,于是我們可以很方便的鏈式調用。

RequestCreator requestCreator = Picasso.with(this)
        .load("http://image.com")
        .config(Bitmap.Config.RGB_565)
        .centerInside()
        .fit()
        .memoryPolicy(MemoryPolicy.NO_CACHE)
        .noPlaceholder()
        .noFade();

into target——談加載過程

requestCreator.into(new ImageView(this));

生成了 RequestCreator 后,我們可以調用其into()方法,該方法接受可以接收一個 ImageView 或者 RemoteView 或者 Target 對象,當然最后他們都會變為相應的 Target 對象來進行內部的圖片加載。

接下來我們就來分析一下這被隱藏起來的加載過程。這里以最常用的 into(imageView) 進行分析。

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); }</code></pre>

這個方法有點長,我們一點一點看,首先它檢查了當前是否為主線程,若為主線程就拋出異常。

static void checkMain() {
  if (!isMain()) {
    throw new IllegalStateException("Method call should happen from the main thread.");
  }
}

之后判斷我們給的圖片的資源路徑是否合法,如果不合法會取消生成請求,并直接顯示占位圖

if (!data.hasImage()) {
  picasso.cancelRequest(target);
  if (setPlaceholder) {
    setPlaceholder(target, getPlaceholderDrawable());
  }
  return;
}

若給的圖片資源路徑不為空,會檢查一個 defer 字段的真假,只有當我們設置 fit() 時,defer才為 true ,因為 fit() 方法會讓加載的圖片以適應我們 ImageView ,所以只有當我們的 ImageView laid out后才會去進行圖片的獲取并剪裁以適應。這里就不具體分析了,接下來往下看。

Request request = createRequest(started);

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;

}</code></pre>

接下來,終于生成了我們的 Request 對象,同Http請求的Request對象相同,這個 Request 對象包含了許多我們需要的信息已獲得準確的回應。這里注意,我們的 Request 對象是可以通過 RequestTransformer 經過變換的,默認Picasso中內置的是一個什么也不變的 RequestTransformer 。

默認的 RequestTransformer

RequestTransformer IDENTITY = new RequestTransformer() {
  @Override public Request transformRequest(Request request) {
    return request;
  }
};

生成了 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;
      }
    }

找到的話就取消請求。否則會更具是否需要設置占位設置一下展位圖,然后生成一個 Action 對象并使其加入隊列發出。

if (setPlaceholder) {
  setPlaceholder(target, getPlaceholderDrawable());
}

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

picasso.enqueueAndSubmit(action);</code></pre>

這個 Action 對象包含了我們的目標 Target 和請求數據 Request 以及相應的回調Callback等信息。

到這里我們還是沒有真正的進行加載,最多只在內存中進行了緩存查詢。

Go Action

通過上面的代碼調用,我們了解到Picasso發出了一個 Action ,就像一個導演一樣,說Action的時候就開始拍戲了,我們的Picasso在發出 Action 后就開始正式加載圖片了。

Action 最終會被 Dispatcher 給分發出去。

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

Dispatcher 隨著Picasso的初始化而生成,正如其名,它負責所有Action的分發并將收到的結果也進行分發,分發給Picasso對象。我們來分析一下Dispatcher的工作過程。

Dispatcher 內部是通過 Handler 進行工作的,這里插下嘴,Handler真是Android中超級強大的類,幫助我們輕松的實現線程之間的通信、切換,有許多有名的開源庫都利用了 Handler 來實現功能,比如Google自家的響應式框架 Agera ,內部真是通過 Handler 實現的。所以我們要好好的掌握這個強大的類。

不多說了,我們繼續看代碼

void dispatchSubmit(Action action) {
  handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}

Dispatcher 通過內部的 Handler 發送了一個消息,并將 Action 對象傳遞了出去,那么我們就要找到這個 Handler 對象的具體實現,根據收到的消息類型它做了哪些事情。

private static class DispatcherHandler extends Handler {
  private final Dispatcher dispatcher;

public DispatcherHandler(Looper looper, Dispatcher dispatcher) { super(looper); this.dispatcher = dispatcher; }

@Override public void handleMessage(final Message msg) { switch (msg.what) { case REQUEST_SUBMIT: { Action action = (Action) msg.obj; dispatcher.performSubmit(action); break; } …… } } }</code></pre>

很容易找到了,就是執行了 performSubmit(action) 方法,繼續來看下去,這里還沒有具體的加載動作。

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

又是一段比較長的代碼,但是還是很容易看懂的。主要就是生成了 BitmapHunter 對象,并將其發出。

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

通過查看代碼,我們知道這個 service 對象是一個 ExecutorService ,也就我們常用的線程池,這里Picasso默認使用的是 PicassoExecutorService ,在Dispatcher內部有一個監聽網絡變化廣播的Receiver,Picasso會根據我們不同的網絡變化(Wifi,4G,3G,2G)智能的切換線程池中該線程的數量,以幫助我們更好的節省流量。

既然線程池將 BitmapHunter 對象發出了,說明這個 BitmapHunter 對象一定是 Runable 的子類,從名字上來分析,我們也可以知道它肯定承擔了Bitmap的獲取生成。點進去一看源碼,果然是這樣的。哈哈。

線程池submit了一個 Runable 對象后,一定會調用其 run() 方法,我們就來看看它是不是加載了 Bitmap 吧~

@Override public void run() {
  try {
    updateThreadName(data);

if (picasso.loggingEnabled) {
  log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
}

result = hunt();

if (result == null) {
  dispatcher.dispatchFailed(this);
} else {
  dispatcher.dispatchComplete(this);
}

} catch (Downloader.ResponseException e) { if (!e.localCacheOnly || e.responseCode != 504) { exception = e; } dispatcher.dispatchFailed(this); } catch (NetworkRequestHandler.ContentLengthException e) { exception = e; dispatcher.dispatchRetry(this); } catch (IOException e) { exception = e; dispatcher.dispatchRetry(this); } catch (OutOfMemoryError e) { StringWriter writer = new StringWriter(); stats.createSnapshot().dump(new PrintWriter(writer)); exception = new RuntimeException(writer.toString(), e); dispatcher.dispatchFailed(this); } catch (Exception e) { exception = e; dispatcher.dispatchFailed(this); } finally { Thread.currentThread().setName(Utils.THREAD_IDLE_NAME); } }</code></pre>

代碼雖長,核心只有 hunt() 一句,捕獵開始! BitmapHunter 這個獵人開始狩獵 Bitmap 了。

Bitmap hunt() throws IOException {
  Bitmap bitmap = null;

if (shouldReadFromMemoryCache(memoryPolicy)) { bitmap = cache.get(key); if (bitmap != null) { stats.dispatchCacheHit(); loadedFrom = MEMORY; if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache"); } return bitmap; } }

data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy; RequestHandler.Result result = requestHandler.load(data, networkPolicy); if (result != null) { loadedFrom = result.getLoadedFrom(); exifRotation = result.getExifOrientation();

bitmap = result.getBitmap();

// If there was no Bitmap then we need to decode it from the stream.
if (bitmap == null) {
  InputStream is = result.getStream();
  try {
    bitmap = decodeStream(is, data);
  } finally {
    Utils.closeQuietly(is);
  }
}

}

if (bitmap != null) { if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_DECODED, data.logId()); } stats.dispatchBitmapDecoded(bitmap); if (data.needsTransformation() || exifRotation != 0) { synchronized (DECODE_LOCK) { if (data.needsMatrixTransform() || exifRotation != 0) { bitmap = transformResult(data, bitmap, exifRotation); if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId()); } } if (data.hasCustomTransformations()) { bitmap = applyCustomTransformations(data.transformations, bitmap); if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations"); } } } if (bitmap != null) { stats.dispatchBitmapTransformed(bitmap); } } }

return bitmap; }</code></pre>

這個方法很長,最終返回我們所期望的 Bitmap 對象,說明這一個方法正是真正從各個渠道獲取 Bitmap 對象的方法。我們看到,這里又進行了一次內存緩存的檢查,避免兩次請求相同圖片的重復加載,沒有的話,利用 RequestHandler 對象進行加載。 RequestHandler 是一個抽象類, load() 方法抽象,因為根據不同圖片的加載要執行不同的操作,比如從網絡中獲取,從resource中獲取,從聯系人中獲取,從MediaStore獲取等,這里運用了模板方法的模式,將一些方法將邏輯相同的方法公用,具體的加載細節子類決定,最終都返回 Result 對象。

根據不同的 RequestHandler 實例,我們可能可以直接獲取一個Bitmap對象,也可能只獲取一段流InputStream,比如網絡加載圖片時。如果結果以流的形式提供,那么Picasso會自動幫我們將流解析成Bitmap對象。之后根據我們之前通過 RequestCreator 對 Request 的配置,決定是否對Bitmap對象進行變換,具體實現都大同小異,利用強大的Matrix,最后返回Bitmap。

之后我們再回到 run() 方法,假設這里我們成功獲取到了 Bitmap ,那么是怎么傳遞的呢?這里還是利用了 Dispatcher

if (result == null) {
  dispatcher.dispatchFailed(this);
} else {
  dispatcher.dispatchComplete(this);
}

和之前一樣, Dispatcher 內部通過Handler發送了一個特定的消息,然后執行。

void performComplete(BitmapHunter hunter) {
  if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
    cache.set(hunter.getKey(), hunter.getResult());
  }
  hunterMap.remove(hunter.getKey());
  batch(hunter);
  if (hunter.getPicasso().loggingEnabled) {
    log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
  }
}

上面就是它具體做的事,這里著重看 batch() 方法,相信這個方法一定是通過某種方式將獵人狩獵的Bitmap給上交給國家了。

private void batch(BitmapHunter hunter) {
  if (hunter.isCancelled()) {
    return;
  }
  batch.add(hunter);
  if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
    handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
  }
}

咦?這里還是沒有將 Bitmap 發出,但是通過handler發出了一個消息,恩恩,這都是套路了。這里有個設計很棒的地方,這里發送消息用了延時,為什么呢?為什么不馬上發送呢?Picasso是將圖片一批批的送出去的,每次發送的間隔為200ms,間隔不長也不短,這樣有什么好處呢,好處就是如果我們看到一部分圖片是一起加載出來的,而不是一張一張加載出來的。這個設計好貼心。前面根據網絡情況決定線程池大小的做法也好貼心,Jake大大真是超棒!

找到了這個消息收到后具體執行的方法了

void performBatchComplete() {
  List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
  batch.clear();
  mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
  logBatch(copy);
}

這里就體現了Handler線程通信與切換的強大之處了,這里通過將一批Bitmap對象交給了處理主線程消息的Handler處理,這樣我們獲取到Bitmap并加載到ImageView上就是在主線程了操作的了,我們去看看這個Handler做了什么。這個Handler是在Dispatcher構造時傳進來的,在Picasso類中定義。哈哈,正不虧是掌握一切權與利的對象,最苦最累的活讓別人去做,自己做最風光體面的事。

static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
  @Override public void handleMessage(Message msg) {
    switch (msg.what) {
      case HUNTER_BATCH_COMPLETE: {
        @SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
        //noinspection ForLoopReplaceableByForEach
        for (int i = 0, n = batch.size(); i < n; i++) {
          BitmapHunter hunter = batch.get(i);
          hunter.picasso.complete(hunter);
        }
        break;
      }
        ……
    }
};

不多說了,我們來看看代碼,恩,很清楚的針對每個BitmapHunter執行了 complete() 方法,那就去看看嘍。

void complete(BitmapHunter hunter) {
  Action single = hunter.getAction();
  List<Action> joined = hunter.getActions();

boolean hasMultiple = joined != null && !joined.isEmpty(); boolean shouldDeliver = single != null || hasMultiple;

if (!shouldDeliver) { return; }

Uri uri = hunter.getData().uri; Exception exception = hunter.getException(); Bitmap result = hunter.getResult(); LoadedFrom from = hunter.getLoadedFrom();

if (single != null) { deliverAction(result, from, single); }

if (hasMultiple) { //noinspection ForLoopReplaceableByForEach for (int i = 0, n = joined.size(); i < n; i++) { Action join = joined.get(i); deliverAction(result, from, join); } }

if (listener != null && exception != null) { listener.onImageLoadFailed(this, uri, exception); } }</code></pre>

這個方法從 BitmapHunter 中獲取 Action 對象,若這個 Action 成功完成,會執行Action的 complete() 方法,并將結果傳入,很明顯這是一個回調方法。Action也是一個抽象類,根據我們的 Target 不同會生成不同的Action對象,比如 into(target) 方法會生成 TargetAction 對象, into(imageView) 會生成 ImageViewAction 對象,調用fetch()方法會產生 FetchAction 對象。這里由于我們是以將圖片加載到ImageView中來分析的,所以我們只分析一下 ImageViewAction 的代碼。

每個 Action 的子類要實現兩個回調方法,一個 error() ,在圖片加載失敗時觸發,一個 complete() ,在圖片成功加載時調用。

@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
  if (result == null) {
    throw new AssertionError(
        String.format("Attempted to complete action with no result!\n%s", this));
  }

ImageView target = this.target.get(); if (target == null) { return; }

Context context = picasso.context; boolean indicatorsEnabled = picasso.indicatorsEnabled; PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);

if (callback != null) { callback.onSuccess(); } }

@Override public void error() { ImageView target = this.target.get(); if (target == null) { return; } Drawable placeholder = target.getDrawable(); if (placeholder instanceof AnimationDrawable) { ((AnimationDrawable) placeholder).stop(); } if (errorResId != 0) { target.setImageResource(errorResId); } else if (errorDrawable != null) { target.setImageDrawable(errorDrawable); }

if (callback != null) { callback.onError(); } }</code></pre>

恩,很清楚的看到成功時就正確顯示圖片,失敗時則根據是否設置了錯誤圖來選擇是否加載錯誤圖。這里用到了一個 PicassoDrawable 的類,這個類繼承自 BitmapDrawable 類,通過它,可以很方便的實現淡入淡出效果。

總結

到這里,我們分析完了一次成功的Picasso圖片加載過程,當然我們不可能每次都成功,也會發生各種錯誤,或者我們暫停了加載又繼續加載等,這些Picasso都作出了不同的處理,線程之間通過Handler進行通信。這里就不一一詳述了。相信大家自己會去看的。

不得不說,Picasso真是一個優雅的圖片加載庫,用極少的代碼完成了了不起的事,從API的設計到代碼的編寫,無不體現了作者的深思熟慮和貼心,運用多用設計模式巧妙地提供極其簡單的調用接口,隱藏背后復雜的實現。通過對Picasso的使用與源碼分析,我學到了很多,希望我以后也可以設計出這么優雅實用的庫或框架。

天道酬勤,我要加油!

 

來自:http://www.jianshu.com/p/1b610c29e596

 

 本文由用戶 z_jide 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!