Android第三方圖片加載庫Picasso源碼解析

qoes4901 8年前發布 | 25K 次閱讀 Android Picasso Android開發 移動開發

Picasso作為一個非常棒的android第三方圖片加載庫,在Github上獲得了高達7000多的star。

Picasso.with(context).load(“image url”).into(imageView);

這么簡單的一行代碼就完成了在Android中加載圖片的功能,這其中,Picasso還幫我們自動完成了一些android中處理圖片的問題:例如在adapter中ImageView的回收和取消下載,使用最小的內存來完成圖片的過渡,自動的內存和磁盤緩存等,的確是非常簡單的。

但作為開發人員,簡單的會用是不夠的,畢竟我們在實際項目中可能會遇到各種奇葩問題,這個時候就有必要深入到內部一探究竟了。下面就從源碼的角度來講解一下Picasso的工作原理。我們可以直接在github上 clone下整個Picasso項目或者如果你使用的是studio的gradle依賴管理,那么也可以直接在項目的Extension Library中查看Picasso的源碼。

我們先看下上面一行代碼中用到的三個函數,首先是with函數,它的實現是這個樣子的:

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

可以看到Picasso使用的是單例模式,并且使用Builder模式創建了一個Picasso的實例,具體如何創建的我們這里先不管,繼續往下看。
有了這個的實例之后,直接調用了它的load函數,Picasso重載了幾個不同參數的load函數,用以從不同的地點來加載圖片:

public RequestCreator load(Uri uri) {
    ...
}
public RequestCreator load(String path) {
    ...
}
public RequestCreator load(int resourceId) {
    ...
}

通過load函數,我們最終得到了一個RequestCreator對象,通過這個對象我們就可以定制一些對圖片的特殊處理了,這里我們同樣不去深究這個RequestCreator內部是如果對圖片進行處理的。
最后我們調用了into函數,將加載到的圖片賦給一個ImageView控件。我們前面的操作可以說都是為into函數做準備,實際工作的開始,就是在into里面完成的。我們跟進into方法看一下,當然了,我們現在還是主要關注流程,不必過于深究細節,這個方法稍微有點長,但是為了方便起見,我們把它的代碼貼在這里:

public void into(Target target) {
    long started = System.nanoTime();
    checkMain();

    if (target == null) {
        throw new IllegalArgumentException("Target must not be null.");
    }
    if (deferred) {
        throw new IllegalStateException("Fit cannot be used with a Target.");
    }

    if (!data.hasImage()) {
        picasso.cancelRequest(target);
        target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);
        return;
    }

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

    if (shouldReadFromMemoryCache(memoryPolicy)) {
        Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
        if (bitmap != null) {
            picasso.cancelRequest(target);
            target.onBitmapLoaded(bitmap, MEMORY);
            return;
        }
    }

    target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);

    Action action =
        new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable,
            requestKey, tag, errorResId);
    picasso.enqueueAndSubmit(action);
}

這個方法稍微有些長,但是邏輯還是比較清晰的,我們總結一下:

  1. into會檢查當前是否是在主線程上執行。
  2. 如果我們沒有提供一個圖片資源并且有設置placeholder,那么就會把我們設置的placeholder顯示出來,并中斷執行。
  3. defered屬性我們一般情況下不需要關注,只有當我們調用了RequestCreator的fit方法時defered才為true,但我們幾乎不會這樣做。
  4. 接下來就是創建了一個Request對象,我們在前面做得一些設置都會被封裝到這個Request對象里面。
  5. 檢查我們要顯示的圖片是否可以直接在緩存中獲取,如果有就直接顯示出來好了。
  6. 緩存沒命中,那就只能費點事把源圖片down下來了。這個過程是異步的,并且通過一個Action來完成請求前后的銜接工作。

至此,Picasso在主線程中的工作就結束了。通過上面的分析,我們看到Picasso的思想還是很清晰的:首先通過Picasso創建了一個RequestCreator對象,通過這個對象我們可以針對不同的場景來設置一些屬性,之后創建出Request對象,最后通過Action來確定異步請求并對請求結果做處理。

接下來,我們就深入到內部看下具體的執行。首先看下Picasso構造函數的聲明:

Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
      RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,
      Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) {
    ...
}

這么多的參數,也基本上涵蓋了Picasso內部所有重要的組件。同時我們也看到這個Picasso是不允許外部進行實例化的。在這里,Picasso使用了單例和建造者模式來完成Picasso的實例化,在Builder中實例化了Picasso所需要的這些組件,這里使用Builder模式的另一個好處就是可以讓我們根據自己的需求來個性化定制組件。那這些組件到底都是做什么的呢,下面我們就一一探個究竟。

既然Picasso是通過Builder來實例化的,那我們就從build函數入手,看一下都做了哪些工作。同樣,為了閱讀方便,我們把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);
}

build結束后,就為Picasso創建了如下幾個對象:
Downloader、Cache、ExecutorService、RequestTransformer、Stats和Dispatcher,從名稱上我們基本上已經能夠猜測出來每個組件都是干嘛的。這些對象傳遞給Picasso的構造函數后,在構造函數的內部又創建了了不同的RequestHandler,用以對不用的圖片資源進行加載。說了這么多是不是有點暈,別急,下面我們會對每個組件做介紹,一點點將Picasso的執行過程串起來。

我們先來看下Downloader:它是一個接口,規定了一些通用的方法,這也就意味著,我們可以提供自己的下載器,只要實現這個接口即可,Picasso的擴展能力還是很不錯的。這里Picasso默認使用OkHttpClient來作為下載器,同樣也是squareup公司開源的一個網絡庫。

Cache:Picasso的緩存,這里實例化的是LruCache,其內部使用的是LinkedHashMap

ExecutorService:這里Picasso實現了自己的PicassoExecutorService,它繼承了ThreadPoolExecutor,也就是Picasso自己維護了一個線程池,用于異步加載圖片。

RequestTransformer:主要是對RequestCreator創建的Request進行轉換,默認對Request對象不做處理。

Stats:這個類只要是維護圖片的一些狀態

Dispatcher:從名字上就可以判斷出來,這個類在這里起到了一個調度器的作用,圖片要不要開始下載以及下載后Bitmap的返回都是通過這個調度器來執行的,后面我們會對它進行更加詳細的講解。

對Picasso幾個核心類有了大致了解后,我們再來看它到底是如何執行一個異步請求,又是如何將執行結果返回的。
通過上面的分析我們知道,RequestCreator在into方法的最后會創建一個Action實例,然后調用Picasso的enqueueAndSubmit方法,而最終是調用了Dispatcher的dispatchSubmit方法,也就是我們前面說的,Dispatcher起到了調度器的作用。在Dispatcher內部,Dispatcher定義了DispatcherThread和DispatcherHandler兩個內部類,并在Dispatcher的構造函數中對他們經行了實例化,所有的調度也都是通過handler異步的執行的:

Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
      Downloader downloader, Cache cache, Stats stats) {
    this.dispatcherThread = new DispatcherThread();
    this.dispatcherThread.start();
    ...
    this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
    ...
}

因此,我們看到的dispatchSubmit方法就如下所示:

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

在handler中最終調用了performSubmit方法來觸發一個圖片的加載,那么我們來看一下這個函數:

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

方法并不發雜,主要是獲得BitmapHunter實例,由這個實例來執行實際的下載操作。BitmapHunter本身是Runnable的一個實現,而這個實例最終是交由Picasso線程池進行運行的。

那么這個BitmapHunter加載圖片完成或失敗后是怎么通知UI的呢?我們前面提到Dispatcher在Picasso中起到了一個調度器的作用,當圖片加載完畢后自然也是通過這個調度器來更新UI:

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

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

        result = hunt();
        // 通過Dispatcher來處理結果
        if (result == null) {
            dispatcher.dispatchFailed(this);
        } else {
            dispatcher.dispatchComplete(this);
        }
    } catch() { 
        //各種異常處理
    }

假如我們的圖片成功下載下來了,接下來就看看這個圖片是如何被渲染到ImageView中的。

void dispatchComplete(BitmapHunter hunter) {
    handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
}

同樣通過handler來發送一個message,我們再看消息處理函數

@Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        ...
        case HUNTER_COMPLETE: {
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performComplete(hunter);
          break;
        }
        ...
      }
}

我們最終執行了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");
    }
}

這個時候,Picasso并不是立即將圖片顯示出來,而是用到了一個批處理,其實就是把操作先暫存在一個list中,等空閑的時候再拿出來處理,這樣做得好處也是盡量減少主線程的執行時間,一方面防止ANR,另一方面快速返回,響應頁面的其他渲染操作,防止卡頓用戶界面。

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

handMessage中對應的處理方法為:

@Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        ...
        case HUNTER_DELAY_NEXT_BATCH: {
          dispatcher.performBatchComplete();
          break;
        }
        ...
      }
}

因此,通過這個batch我們實際看到,最后還是調用了Dispatcher的方法來處理,但由于這個處理并非是在主線程(參考前面Dispatcher構造函數中Handler的實例化),因此我們還需要通過一個主線程的Handler來處理這個請求

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

這個mainThreadHandler是在Dispatcher實例化時由外部傳遞進來的,我們在前面的分析中看到,Picasso在通過Builder創建時會對Dispatcher進行實例化,在那個地方將主線程的handler傳了進來,我們回到Picasso這個類,看到其有一個靜態成員變量HANDLER,這樣我們也就清楚了。

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

到這里,我們的圖片馬上就要顯示出來了。我們前面提到,Picasso中一個Action提供了請求前后的銜接工作,對于我們現在的情況,Picasso使用了ImageViewAction來進行處理,也就是在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();
    }
}

OK,到此,一個圖片從開始下載到成功渲染到ImageView中整個流程就講完了。其中,Dispatcher那塊的調度有點繞,如果不是很理解,可以先熟悉下android中handler的用法,handler本身也是android提供給我們方便進行線程間通信的,就像Picasso的實現一樣,善用handler可以幫助我們實現更加流暢的用戶UI操作體驗。


文/_xiaoz(簡書)

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