Picasso學習筆記

rowandjj發表於2015-11-21

下面是我在學習Picasso過程中做的筆記,思路可能有些凌亂,如果閱讀過程中有發現不準確或者錯誤的地方歡迎交流(微博@楚奕RJ)

內容主要分三部分,第一部分是picasso的基本使用,第二部分是picasso的原始碼閱讀,最後一部分是關於原始碼中一些細節的梳理.

關於圖片載入庫的思考

我們為什麼要用圖片庫呢?
圖片庫通常會對圖片載入邏輯進行封裝、優化,比如多級快取/非同步執行緒排程/圖片壓縮變換,有了這些特性,開發者不再需要關注記憶體佔用、
OOM、網路請求等問題,而只需關注業務本身的邏輯,這對提高生產效率有很大幫助。

我工作半年以來也研究了一些主流的圖片庫,發現雖然每個圖片的架構和實現細節不太一樣,但是通常都會有以下模組:

  1. 請求分發模組。負責封裝請求,對請求進行優先順序排序,並按照型別進行分發。
  2. 快取模組。通常包括一個二級的快取,記憶體快取、磁碟快取。並預置多種快取策略。
  3. 下載模組。負責下載網路圖片。
  4. 監控模組。負責監控快取命中率、記憶體佔用、載入圖片平均耗時等。
  5. 圖片處理模組。負責對圖片進行壓縮、變換等處理。
  6. 本地資源載入模組。負責載入本地資源,如assert、drawable、sdcard等。
  7. 顯示模組。負責將圖片輸出顯示。

圖片載入庫

Android平臺圖片載入庫現狀

目前社群主流的圖片載入庫有Universal Image Loader,Picasso,Volley,Fresco,Glide.

Picasso簡介

A powerful image downloading and caching library for Android,developed by Square

wiki:http://square.github.io/picasso/

Picasso的特性

  1. 絕對是最輕量的圖片載入庫,120kb.
  2. 自帶監控功能,可以檢測cache hit/記憶體大小等等資料
  3. 圖片預載入
  4. 執行緒併發數依網路狀態變化而變化、優先順序排程
  5. 圖片變換
  6. 圖片壓縮、自適應
  7. 易擴充套件

Picasso的使用

  • 載入一張網路圖片到ImageView
Picasso.with(context)
    .load(url)
    .placeholder(R.drawable.user_placeholder)
    .error(R.drawable.user_placeholder_error)
    .into(imageView);//此種策略並不會壓縮圖片
  • 預載入一張圖片
Picasso.with(this).load(URL).fetch();
Picasso.with(this).load(URL).fetch(Callback);

注意哦,如果你以以下面這種方式加上圖形變換preload的話:

Picasso.with(this).load(URL).rotate(20).fetch();

再下面這種方式是取不到preload的圖片的,因為預快取的是經過變換後的圖片,它的cachekey會有rotation標識

Picasso.with(this).load(URL).into(imageView);

當然我說的是preload到記憶體中的那份經過旋轉的圖片,http會快取旋轉前的圖片到磁碟(支援快取的情況下),所以最終還是可以從磁碟快取
拿到圖片的。

  • 替換預設的Picasso
Picasso p = new Picasso.Builder(this).executor().downloader(downloader).memoryCache(cache).build();
Picasso.setSingletonInstance(p);
  • 同步call
 new AsyncTask<Void,Void,Bitmap>(){
        @Override
        protected Bitmap doInBackground(Void... params) {
            try {
                return Picasso.with(PicassoTestActivity.this).load(URL).get();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if(bitmap != null){
                imageView.setImageBitmap(bitmap);
            }
        }
    }.execute();

注意,必須在非同步執行緒呼叫,否則crash,另外,這個結果並不會快取到記憶體裡面,所以慎用。

  • 自適應
Picasso.with(TestImageActivity.this).load(url).fit().into(imageview);

fit方法的意思是,讓圖片的寬高恰好等於imageView的寬高.前提是你的imageView控制元件不能設定成wrap_content,也就是必須
有大小才行。另外,如果使用了fit方法,那麼就不能呼叫resize.

  • 壓縮到指定尺寸
Picasso.with(TestImageActivity.this).load(url).resize(widthPixel,heightPixel).centerInside().into(imageView);
Picasso.with(TestImageActivity.this).load(URL).resizeDimen(R.dimen.width,R.dimen.height).centerCrop().into(iv);

resize後面通常接centerInside或者centerCrop。注意這跟ImageView的scaleTyoe沒有關係,僅僅指的是圖片的縮放方式。
比如如下程式碼,iv不壓縮,iv_2寬高壓縮到40dp並且指定為centerInside.

<ImageView
        android:id="@+id/iv"
        android:background="#000"
        android:layout_width="100dp"
        android:layout_height="100dp"/>
<ImageView
    android:layout_below="@id/iv"
    android:id="@+id/iv_2"
    android:background="#000"
    android:layout_width="100dp"
    android:layout_height="100dp"/>

Picasso.with(TestImageActivity.this).load(URL).into(iv);
Picasso.with(TestImageActivity.this).load(URL).resizeDimen(R.dimen.width/*40dp*/, R.dimen.height/*40dp*/).centerInside().into(iv_2);

最終顯示結果如下:

demo

可以很明顯看到下面的圖模糊許多,這是因為圖片被壓縮了,但是顯示的時候又被ImageView拉伸了(預設scaleType是fitCenter),
要想不顯示拉伸的圖,可以給iv_2增加scaleType="centerInside",效果如下:

demo

  • 圖形變換
Picasso.with(TestImageActivity.this).load(URL).rotate(20).into(iv);
//自定義變換
 Picasso.with(TestImageActivity.this).load(URL).transform(new Transformation() {
                    @Override
                    public Bitmap transform(Bitmap source) {//從原圖中間裁剪一個正方形
                        int size = Math.min(source.getWidth(), source.getHeight());
                        int x = (source.getWidth() - size) / 2;
                        int y = (source.getHeight() - size) / 2;
                        Bitmap result = Bitmap.createBitmap(source, x, y, size, size);
                        if (result != source) {
                            source.recycle();
                        }
                        return result;
                    }

                    @Override
                    public String key() {
                        return "square()";
                    }
                }).into(iv);

各種圖形變換:https://github.com/wasabeef/picasso-transformations

  • 暫停/重啟請求任務

通常在滑動列表的時候需要暫停請求


Picasso.with(context).load(URL).tag(context);


public class SampleScrollListener implements AbsListView.OnScrollListener {
  private final Context context;


  public SampleScrollListener(Context context) {
    this.context = context;
  } 


  @Override 
  public void onScrollStateChanged(AbsListView view, int scrollState) {
    final Picasso picasso = Picasso.with(context);
    if (scrollState == SCROLL_STATE_IDLE || scrollState == SCROLL_STATE_TOUCH_SCROLL) {
      picasso.resumeTag(context);
    } else { 
      picasso.pauseTag(context);
    } 
  } 


  @Override 
  public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                       int totalItemCount) {
    // Do nothing. 
  } 
}

Picasso原始碼分析

其實看原始碼分析什麼的最無聊了,最好的方式是自己跟一遍原始碼,picasso裡面有很多設計很優雅的地方,只有自己看了才會真正有收穫.

整體架構

盜用下Trinea的圖:

整體架構

Picasso中的核心類包括PicassoDispatcherBitmapHunterRequestHandlerRequestActionCache
等.Picasso類是一個負責圖片下載、變換、快取的管理器,當它收到一個圖片下載請求的時候,它會建立Request並提交給Dispatcher,
Dispatcher會尋找對應的處理器RequestHandler,並將請求與該處理器一起提交給執行緒池執行,圖片獲取成功後,最終會交給
PicassoDrawable顯示到Target上。

它將一張圖片的載入過程分為八步,依次為:

建立->入隊->執行->解碼->變換->批處理->完成->分發->顯示(可選)

也可以從日誌中看到這個過程:

11-05 10:39:00.942 2952-2952/com.taobao.paimainews D/Picasso: Main        created      [R0] Request{http://ww3.sinaimg.cn/mw600/006g34NHgw1exj5c4hmfvj30hs0qoqff.jpg resize(90,300) centerInside rotation(30.0) ARGB_8888}
11-05 10:39:00.981 2952-3109/com.taobao.paimainews D/Picasso: Dispatcher  enqueued     [R0]+40ms
11-05 10:39:00.993 2952-3193/com.taobao.paimainews D/Picasso: Hunter      executing    [R0]+50ms
11-05 10:39:01.038 2952-3193/com.taobao.paimainews D/Picasso: Hunter      decoded      [R0]+97ms
11-05 10:39:01.041 2952-3193/com.taobao.paimainews D/Picasso: Hunter      transformed  [R0]+100ms
11-05 10:39:01.042 2952-3109/com.taobao.paimainews D/Picasso: Dispatcher  batched      [R0]+101ms for completion
11-05 10:39:01.279 2952-2952/com.taobao.paimainews D/Picasso: Main        completed    [R0]+338ms from DISK
11-05 10:39:01.280 2952-3109/com.taobao.paimainews D/Picasso: Dispatcher  delivered    [R0]+338ms

下面是Picasso的類圖:

類圖

程式碼分析

版本:2.5.2

Picasso類是整個圖片載入器的入口,負責初始化各個模組,配置相關引數等等。
Picasso.with()方法用於建立全域性唯一的Picasso例項,為了確保唯一,使用了單例模式

Picasso#with()

 static volatile Picasso singleton = null;

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

with方法內部通過Builder模式建立Picasso例項,這樣做的好處是簡潔清晰,通常在構造器引數很多的時候使用。
build方法會最終建立Picasso例項:

Picasso#Builder#build()

 public Picasso build() {
      Context context = this.context;

      if (downloader == null) {
        downloader = Utils.createDefaultDownloader(context);
      }
      if (cache == null) {
        cache = new LruCache(context);
      }
      if (service == null) {
        service = new PicassoExecutorService();
      }
      if (transformer == null) {
        transformer = RequestTransformer.IDENTITY;
      }

      Stats stats = new Stats(cache);

      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

      return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
          defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
    }

此方法做了如下基本配置:

  1. 使用預設的快取策略,記憶體快取基於LruCache,磁碟快取基於http快取,HttpResponseCache
  2. 建立預設的下載器
  3. 建立預設的執行緒池(3個worker執行緒)
  4. 建立預設的Transformer,這個Transformer什麼事情也不幹,只負責轉發請求
  5. 建立預設的監控器(Stats),用於統計快取命中率、下載時長等等
  6. 建立預設的處理器集合,即RequestHandlers.它們分別會處理不同的載入請求

處理器集合的初始化在Picasso的構造器中:
Picasso構造器

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

從命名就可以看出來,可以從網路、file、assert、contactsphoto等地方載入圖片.

另,Picasso支援增加自己的處理器.

load()方法用於從不同地方載入圖片,比如網路、resource、File等,該方法內部邏輯很簡單,只是建立了一個RequestCreator

Picasso#load()

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

RequestCreator從名字就可以知道這是一個封裝請求的類,請求在Picasso中被抽象成RequestRequestCreator類提供了
諸如placeholdertagerrormemoryPolicynetworkPolicy等方法.
由於可配置項太多,所以Request也使用了Builder模式:
RequestCreator構造器

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

那麼可想而知into方法一定會去將Request建立,並丟到執行緒池或者分發器中執行。into方法有多種過載,因為Picasso不僅僅可以
將圖片載入到ImageView上,還可以載入到Target或者RemoteView上.
這裡選取imageView作為分析物件,該方法程式碼如下:

RequestCreator#into()

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()) {//檢查是否設定uri或者resID
      //如果沒有設定當然取消請求
      picasso.cancelRequest(target);
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }

    if (deferred) {//是否呼叫了fit(),如果是,代表需要將image調整為ImageView的大小
      if (data.hasSize()) {//不能與resize一起用
        throw new IllegalStateException("Fit cannot be used with resize.");
      }
      //既然要適應ImageView,肯定需要拿到ImageView大小
      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 request = createRequest(started);
    String requestKey = createKey(request);

    if (shouldReadFromMemoryCache(memoryPolicy)) {//是否需要在快取裡面先查詢
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {//cache hit
        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,將任務交給dispatcher
    if (setPlaceholder) {
      setPlaceholder(target, getPlaceholderDrawable());
    }

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

    picasso.enqueueAndSubmit(action);
  }

邏輯註釋寫的很清楚了,into方法會先從快取裡面查詢圖片,如果找不到的話,則會建立Action即一個載入任務,交給Dispatcher執行。
那我們就來看看picasso.enqueueAndSubmit方法做了什麼.
在這之前,先來看下Action是什麼鬼,為什麼有了Request還要Action.

先看Request有哪些屬性:

  int id;
  long started;
  int networkPolicy;
  public final Uri uri;
  public final int resourceId;
  public final String stableKey;
  public final List<Transformation> transformations;
  public final int targetWidth;
  public final int targetHeight;
  public final boolean centerCrop;
  public final boolean centerInside;
  public final boolean onlyScaleDown;
  public final float rotationDegrees;
  public final float rotationPivotX;
  public final float rotationPivotY;
  public final boolean hasRotationPivot;
  public final Bitmap.Config config;
  public final Priority priority;

再看Action的屬性:

  final Picasso picasso;
  final Request request;
  final WeakReference<T> target;
  final boolean noFade;
  final int memoryPolicy;
  final int networkPolicy;
  final int errorResId;
  final Drawable errorDrawable;
  final String key;
  final Object tag;
  boolean willReplay;
  boolean cancelled;

Request關注的是請求本身,比如請求的源、id、開始時間、圖片變換配置、優先順序等等,而Action則代表的是一個載入任務,所以不僅需要
Request物件的引用,還需要Picasso例項,是否重試載入等等

Action有個需要關注的點,那就是WeakReference<T> target,它持有的是Target(比如ImageView..)的弱引用,這樣可以保證載入時間很長的情況下
也不會影響到Target的回收了.

好的,那回到剛才的思路,我們開始分析picasso.enqueueAndSubmit方法:

picasso#enqueueAndSubmit()

  final Map<Object, Action> targetToAction;
   ...
  this.targetToAction = new WeakHashMap<Object, Action>();
   ...
  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);
  }

它會先從action任務上拿到對應target,也就是imageView,然後從weakHashMap中通過這個imageView索引到對應的action,如果
發現這個action跟傳進來的action不一樣的話,那就取消掉之前的載入任務。最後將當前載入任務提交.

跟進submit發現最終呼叫的是DispatcherdispatchSubmit(action)方法.這個Dispatcher即任務分發器,它是在
Picasso例項建立的時候初始化的.。

那我們在看dispatchSubmit方法之前,必然得了解下Dispatcher.

Picasso.Builder.build()

  Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

每一個Dispatcher都需要關聯執行緒池(service)、下載器(downloader)、主執行緒的Handler(HANDLER)、快取(cache)、
監控器(stats).

這裡先看執行緒池,Picasso預設的執行緒池叫PicassoExecutorService,它繼承自ThreadPoolExecutor,預設執行緒數量為
3.但是PicassoExecutorService的特性是可以根據網路情況調整執行緒數量,wifi下是4個執行緒,而2g網只有一個執行緒。具體是
通過在Dispatcher中註冊了監聽網路變化的廣播接收者。

另外,PicassoExecutorService中還有一個很重要的方法叫submit,它會去執行一個runnable.

好的,我們回到Dispatcher,這裡還需要關注的是Dispatcher中有個內部類叫DispatcherHandler,注意哦,
這個handler是Dispatcher自己的,而不是構造器傳進來的。而且,這個handler繫結的是子執行緒的Looper,為什麼?請看:

Dispatcher#構造器

    this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);

dispatcherThread則是一個HandlerThread:

Dispatcher內部類

 static class DispatcherThread extends HandlerThread {
    DispatcherThread() {
      super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
    }
  }

也就是說,這個handler的訊息處理是在子執行緒進行的!這樣就可以避免阻塞主執行緒的訊息佇列啦!

好的,再回到剛才的問題,來看下dispatchSubmit方法(不知道大家有沒有看暈。。。。):

Dispatcher#dispatchSubmit

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

不用看都知道會發訊息給handler。而handler收到這個訊息之後呼叫了這個方法:

 dispatcher.performSubmit(action);

果斷跟進去:

Dispatcher#performSubmit

 void performSubmit(Action action, boolean dismissFailed) {//注意哦,這裡已經不在主執行緒了,而是在dispatcher執行緒(HandlerThread)
    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
    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());
    }
  }

首先建立了一個BitmapHunter,它繼承自Runnable,可以被執行緒池呼叫。然後判斷執行緒池有沒有關閉,如果沒有的話,
就會將這個bitmapHunter丟到執行緒池裡面,即呼叫剛才說的submit方法。

我們先看下forRequest方法裡面幹了什麼:

BitmapHunter#forRequest

 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);
      }
    }
    //沒有人能處理這個請求,那麼交給ERRORING_HANDLER,它會直接拋異常
    return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
  }

還記得大明湖畔的Picasso麼?在它的構造器中建立了若干RequestHandler,用於處理不同的載入請求,在這裡,它會遍歷
這些requestHandler,看誰可以處理當前請求,如果發現了,那就建立BitmapHandler,並把這個requestHandler傳進去,

執行緒池在收到BitmapHunter之後,會呼叫其run方法,那麼我們就來看下:

BitmapHunter#run

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

核心邏輯是由hunt方法完成的,下面一堆catch語句分別捕捉不同的異常然後上報給dispatcher進行處理。
而hunt方法裡面肯定會呼叫RequestHandler的load方法:

BitmapHunter#hunt

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

    //快取沒有命中的話,再呼叫requestHandler.load
    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
      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;
  }

這裡假設是一個網路請求,那麼最終NetworkRequestHandler會處理請求:

NetworkRequestHandler#load

@Override public Result load(Request request, int networkPolicy) throws IOException {

    //這個downloader也是Dispatcher建立的時候傳進來的
    Response response = downloader.load(request.uri, request.networkPolicy);
    if (response == null) {
      return null;
    }
    //判斷是從快取還是網路拿的
    Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK;
    //從響應中拿到bitmap
    Bitmap bitmap = response.getBitmap();
    if (bitmap != null) {
      return new Result(bitmap, loadedFrom);
    }
    //如果是從網路返回的,那麼拿到的是流物件
    InputStream is = response.getInputStream();
    if (is == null) {
      return null;
    }
    // Sometimes response content length is zero when requests are being replayed. Haven't found
    // root cause to this but retrying the request seems safe to do so.
    if (loadedFrom == DISK && response.getContentLength() == 0) {
      Utils.closeQuietly(is);
      throw new ContentLengthException("Received response with 0 content-length header.");
    }
    if (loadedFrom == NETWORK && response.getContentLength() > 0) {
      stats.dispatchDownloadFinished(response.getContentLength());
    }
    //將結果封裝返回
    return new Result(is, loadedFrom);
  }

現在我們關注下這個downloader的前世今生,如果使用者沒有自定義的話,那將使用預設downloader:

Picasso#Builder#build()

 downloader = Utils.createDefaultDownloader(context);

Utils#createDefaultDownloader

static Downloader createDefaultDownloader(Context context) {
    try {
      Class.forName("com.squareup.okhttp.OkHttpClient");
      return OkHttpLoaderCreator.create(context);
    } catch (ClassNotFoundException ignored) {
    }
    return new UrlConnectionDownloader(context);
  }

首先反射下,看有沒有依賴okhttp,如果依賴的話,那就使用OkHttpClient嘍,否則就使用預設的HttpUrlConnection了。
注:其實從4.4開始,okhttp已經作為HttpUrlConnection的實現引擎了。

可以從picasso的pom檔案裡面看到,okhttp是optional的:

 <dependency>
      <groupId>com.squareup.okhttp</groupId>
      <artifactId>okhttp</artifactId>
      <optional>true</optional>
    </dependency>

UrlConnectionDownloader為例,看下它的load方法:

  @Override public Response load(Uri uri, int networkPolicy) throws IOException {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
      installCacheIfNeeded(context);
    }

    HttpURLConnection connection = openConnection(uri);
    connection.setUseCaches(true);

    if (networkPolicy != 0) {
      String headerValue;

      if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
        headerValue = FORCE_CACHE;
      } else {
        StringBuilder builder = CACHE_HEADER_BUILDER.get();
        builder.setLength(0);

        if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
          builder.append("no-cache");
        }
        if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
          if (builder.length() > 0) {
            builder.append(',');
          }
          builder.append("no-store");
        }

        headerValue = builder.toString();
      }

      connection.setRequestProperty("Cache-Control", headerValue);
    }

    int responseCode = connection.getResponseCode();
    if (responseCode >= 300) {
      connection.disconnect();
      throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
          networkPolicy, responseCode);
    }

    long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
    boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));

    return new Response(connection.getInputStream(), fromCache, contentLength);
  }

注意哦,Disk Cache功能是在這裡做掉的,它基於Http語義來判斷是否快取.
另,返回的是inputStream流,而不是Bitmap物件.

好的,現在我們回到BitmapHunter#run(),它在拿到結果後會將結果交給dispatcher

BitmapHunter#run()

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

我們看dispatcher.dispatchComplete(this),它會把訊息發給自己內部的handler,也就是剛才說的Looper在子執行緒
的handler
,handler將做如下處理:

  BitmapHunter hunter = (BitmapHunter) msg.obj;
  dispatcher.performComplete(hunter);

注意哦,BitmapHunter會持有網路請求回來的Bitmap引用.來看下performComplete:

Dispatcher#performComplete

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方法,從名字就可以知道,這個方法會把結果暫存,
然後批量處理(等待200ms),這樣做也是為了防止短時間大量任務阻塞訊息佇列。到時間後,就會執行performBatchComplete,
此方法會將這個批次的所有結果一次性發給主執行緒的Handler,也就是Picasso中定義的Handler:

Dispatcher#performBatchComplete

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

主執行緒收到訊息後會進行處理:

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

對batch中每個BitmapHunter呼叫complete方法,而complete方法會呼叫deliverAction方法,最終其實呼叫的是具體
action的complete方法,如果是ImageView的話,那就是ImageViewActioncomplete方法:

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

注意看這一句,ImageView target = this.target.get(),因為targetImageView的弱引用,在下載過程中,
ImageView可能已經被銷燬了,所以這裡要做下判斷。

如果沒有被回收,那麼圖片最終通過PicassoDrawable.setBitmap()方法被設定到ImageView上.
這個PicassoDrawable提供了fade動畫.

好了,分析基本完畢,下面是整個流程的時序圖.

時序圖

簡單總結下,當我們執行Picasso.with(context).load(url).into(imageview)時,首先會構造Picasso例項,然後會
根據url建立請求,然後請求會被交給Dispatcher,Dispatcher將在子執行緒對請求任務進行排程,將請求任務交給執行緒池
執行,執行完畢後,將結果傳給主執行緒的handler,最後在主執行緒中將圖片設定到ImageView上.

其他需要關注的點

  • 關於快取策略

    Picasso的快取是記憶體快取+磁碟快取,記憶體快取基於LruCache類,可配置替換。磁碟快取依賴於http快取,不可配置。
    先看記憶體快取.記憶體快取比較簡單,是通過LinkedHashMap實現.
    讀快取時機:生成了請求Request物件,準備建立Action載入任務之前,會先去快取裡面查詢下.

    RequestCreator#into

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

    寫快取時機:圖片從網路或者其他地方載入成功後,即在BitmapHunter的run方法執行結束的時候.
    Dispatcher#performComplete

    
     if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
          cache.set(hunter.getKey(), hunter.getResult());
        }
    

    注意哦,快取的是經過壓縮之後的圖片(如果你使用了fit或者resize方法的話),
    再看磁碟快取。
    如果你是使用UrlConnectionDownloader的話,那很不幸,快取只在Api>14上生效,因為快取依賴於HttpResponseCache.
    如果你依賴了okhttp,那麼快取策略始終是有效的。另外需要說明的是,既然是http快取,那麼快取的可用性依賴於http響應是
    否允許快取,也就是說得看響應中是否攜帶Cache-ControlExpires等欄位.對於這塊不瞭解的話,可以參考我的這篇文章:
    HttpCache in android
    還有一點,快取的路徑是 應用cache目錄/picasso-cache 資料夾.具體程式碼參考Utils.createDefaultCacheDir方法

  • 關於預載入

    首先要注意的是Callback是一個強引用,如果你使用帶Callback的過載形式的話,只有當Request結束的時候才會釋放
    引用,在此期間你的Activity/Fragment等元件引用不會被釋放.因此你需要注意記憶體洩露的情形.

    怎麼實現?很簡單拉,調fetch的時候建立了FetchAction,然後其他流程上面描述的一樣,最終在Dispatcher.performComplete
    的時候將結果寫入記憶體快取,結果回傳到主執行緒的時候,呼叫了FetchActioncomplete方法,這裡面不對Bitmap
    任何處理就行拉:

    FetchAction#complete

    ```
     @Override void complete(Bitmap result, Picasso.LoadedFrom from) {
           if (callback != null) {
             callback.onSuccess();
           }
         }
    ```
    
  • 關於圖形變換

    圖形變換在Picasso中被抽象成Transformation介面,具體的變換操作由transform方法實現.Request維護一個
    圖形變換的列表List<Transformation>,當圖片載入成功後,BitmapHunter中將會遍歷這個變換集合,依次進行變換,
    最後返回變換後的bitmap.恩,其實是一個回撥的思想,將操作封裝到介面中交給系統,系統在某個特定時機呼叫你的介面。

    具體程式碼:

    BitmapHunter#applyCustomTransformations

       ```
       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;
                   }
                   ....
                   result = newResult;
                 }
                 return result;
               } 
    
       ```
    
  • 關於CleanupThread

    Picasso類中有一個內部執行緒叫CleanupThread,這是一個daemon執行緒,它的工作是找到那些Target(比如說ImageView)已經被回收
    但是所對應的Request請求還在繼續的任務(Action),找到之後,會取消對應的請求,避免資源浪費.

    看下程式碼:

    Picasso#CleanupThread

     private static class CleanupThread extends Thread {
        private final ReferenceQueue<Object> referenceQueue;
        private final Handler handler;
    
        CleanupThread(ReferenceQueue<Object> referenceQueue, Handler handler) {//關聯主執行緒的handler,refreenceQueue
          this.referenceQueue = referenceQueue;
          this.handler = handler;
          setDaemon(true);
          setName(THREAD_PREFIX + "refQueue");
        }
    
        @Override public void run() {
          Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
          while (true) {
            try {
              // Prior to Android 5.0, even when there is no local variable, the result from
              // remove() & obtainMessage() is kept as a stack local variable.
              // We're forcing this reference to be cleared and replaced by looping every second
              // when there is nothing to do.
              // This behavior has been tested and reproduced with heap dumps.
              RequestWeakReference<?> remove =
                  (RequestWeakReference<?>) referenceQueue.remove(THREAD_LEAK_CLEANING_MS);
              Message message = handler.obtainMessage();
              if (remove != null) {
                message.what = REQUEST_GCED;
                message.obj = remove.action;
                handler.sendMessage(message);
              } else {
                message.recycle();
              }
            } catch (InterruptedException e) {
              break;
            } catch (final Exception e) {
              handler.post(new Runnable() {
                @Override public void run() {
                  throw new RuntimeException(e);
                }
              });
              break;
            }
          }
        }
    
        void shutdown() {
          interrupt();
        }
      }

    可以看到它會不斷輪詢ReferenceQueue,找到這樣的reference,就交給handler,handler會從reference中拿到action,
    並取消請求.

     case REQUEST_GCED: {
              Action action = (Action) msg.obj;
              if (action.getPicasso().loggingEnabled) {
                log(OWNER_MAIN, VERB_CANCELED, action.request.logId(), "target got garbage collected");
              }
              action.picasso.cancelExistingRequest(action.getTarget());
              break;
            }

    那麼這個ReferenceQueue又是如何關聯Action的呢?這個可以從Action的構造器中拿到答案:

     this.target =//RequestWeakReference是WeakReference的子類
            target == null ? null : new RequestWeakReference<T>(this/*即Action本身*/, target, picasso.referenceQueue);

    可以看到兩點:

    1. 每個Action都會關聯Picasso中唯一的referenceQueue例項;
    2. 每個RequestWeakReference都會同時關聯TargetAction.
  • resume/pause

    1. pause

      流程如下。

    pause

    可能會有疑問的地方在於Dispatcher#performPauseTag中遍歷所有的hunter,都會調一次cancel,這似乎會取消所有
    的請求。但其實不是這樣的,可以看下BitmapHunter#cancel方法的程式碼:

       boolean cancel() {
         return action == null
             && (actions == null || actions.isEmpty())
             && future != null
             && future.cancel(false);
       }

    注意到它會判斷action是否為空,如果不為空就不會取消了。而在Dispatcher#performPauseTag中會把tag匹配的
    action與對應的BitmapHunter解綁(detach),讓BitmapHunter的action為空.所以這並不影響其他任務的執行。

    1. resume

      流程如下。

    resume

    其實就是遍歷pausedActions,挨個重新交給dispatcher分發。

    作者的提交記錄: https://github.com/square/picasso/pull/665/files#diff-f11286bbae6959a7a5dd74bf99276f1aR229

  • 圖片壓縮

    圖片壓縮的原理通常都是利用BitmapFactory#Options類,先將injustDecodeBounds設定為true,對Bitmap進行一次
    解碼,拿到outWidth/outHeight,即實際寬高,然後根據期望壓縮到的寬和高算出inSampleSize,最後將injustDecodeBounds設定為false,
    再對Bitmap進行一次解碼即可。另一種壓縮的方法是設定圖片的顯示效果,比如ARGB_8888等等.Picasso綜合了利用這兩種方案.

    圖片壓縮

    詳細程式碼參考BitmapHunter#decodeStreamRequestHandler#createBitmapOptionsRequestHandler#calculateInSampleSize
    這三個方法,有個需要注意的地方,只有當設定圖片的寬高時(呼叫了fit或者resize)才會計算smpleSize進行壓縮。