picasso詳解及其源碼簡析

1、前言

圖片庫做爲開發中一個重要的基礎組件,其重要性不言而喻。目前來講,比較流行的圖片庫分別是 Android-Universal-Image-LoaderPicassoGlideFresco。它們的通常特色是:android

  • 使用上比較簡單,基本上均可以用一句鏈式代碼就直接完成了圖片的下載到展現。
  • 多級緩存,至少有 2 級緩存,磁盤緩存以及內存緩存。加上高效的圖片處理,極大的減小了OOM 的發生。
  • 多種數據源,網絡,磁盤,resource,assets 等。
  • 自動化程度高,圖片下載、緩存管理,圖片解碼、圖片處理甚至是 ListView/RecyclerView 的 Adapter 中圖片自動取消、展現等,都不用咱們操心了。

這篇文章要分析的是 square/picasso。沒錯,又是 square,誰中你那麼優秀呢?不去管它的舊版本如何,這裏直接分析其最新版本 2.71828 吧。git

2、picasso 簡介

1.關於 picasso

picasso,偉大的畫家畢加索,這裏想必也是在表達致敬之意。square 在 github 項目首頁中只用了一句話解釋這個庫。github

一個適用於Android的強大的圖像下載和緩存庫編程

image.png

除了 github 項目首頁的介紹外,picasso 還另外提供了一個 github page。在這裏,它進一步介紹了其具有的幾個功能:設計模式

  • 處理 ImageView 在適配器中的回收以及取消下載。
  • 使用最少的內存來處理複雜的圖像轉換。
  • 自動管理磁盤和內存緩存。

同時該頁面也給出了一些使用例子,如在 Adapter 中使用,圖像變換以及自定義變換、設置 placeholders 等。感興趣的同窗能夠自行前往看一看,不過要注意 Picasso 的實例再也不從 Picasso.with(context) 開始了,也再也不是該頁面所展現的 Picasso.get(),而封裝了一個 PicassoProvider,而後經過它的靜態方法 get() 來獲取。緩存

2.集成

gradle 中集成,so easy,每一個整 android 的小夥伴都懂的。bash

implementation 'com.squareup.picasso:picasso:2.71828'網絡

3、源碼分析

前面簡要介紹了下 picasso,下面便開始進入正題源碼分析。不變的原則,從最簡單的主路徑開始分析。分析以前先來看一看其框架圖,是否是似曾相識?併發

Picasso.jpg

1.demo

picasso 工程裏有提供 sample,做爲典型的例子,這裏選擇了從 SampleListDetailAdapter 拿出一段用於 ListView 的 Adapter 中加載列表圖片的代碼來做爲用於主路徑分析的 demo。app

PicassoProvider.get()
        .load(url)
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .resizeDimen(R.dimen.list_detail_image_size, R.dimen.list_detail_image_size)
        .centerInside()
        .tag(context)
        .into(holder.image);
複製代碼

鏈式調用的一大特色就在於無論多複雜的邏輯,看起來好像都是順序執行的同樣。嗯呵,你可能想到了 RxJava。下面就來逐個逐個分析上面的每個方法的調用。

2.主路徑分析

2.1 構造 Picasso 實例,初始化運行環境

  • PicassoProvider及其 get() 方法
public final class PicassoProvider {
  @SuppressLint("StaticFieldLeak")
  private static volatile Picasso instance;

  @Initializer
  public static Picasso get() {
    // 單例實現
    if (instance == null) {
      synchronized (PicassoProvider.class) {
        if (instance == null) {
          // 獲取 context
          Context autoContext = PicassoContentProvider.context;
          if (autoContext == null) {
            throw new NullPointerException("context == null");
          }
          // 經過 Picasso.Builder 來構建 Picasso 實例
          instance = new Picasso.Builder(autoContext).build();
        }
      }
    }
    return instance;
  }

  private PicassoProvider() {
    throw new AssertionError("No instances.");
  }
}
複製代碼

PicassoProvider 經過單例設計模式的方式,在 get() 方法中爲咱們提供了 Picasso 的實例。也就是說,在應用中,一個進程內只有一個 Picasso 實例。

構建 Picasso 實例是須要 Context 的,這裏是從 PicassoContentProvider 處獲取。所以,進一步看看 PicassoContentProvider 又是如何提供 Context。

  • PicassoContentProvider 及其 context 的初始化
public final class PicassoContentProvider extends ContentProvider {
  @SuppressLint("StaticFieldLeak")
  @Nullable static Context context;

  @Override public boolean onCreate() {
    context = getContext();
    return true;
  }
  ......
}
複製代碼

PicassoContentProvider 繼承了 ContentProvider,這是一個很是微妙的實現。其利用了 ContentProvider 會在 Application 初始化的時候同時被初始化的原理,從而初始化 context。不得不服!!!這就避免了開發者還須要對其進行全局初始化的步驟,典型是在應用的 Application#onCreate() 進行初始化。

  • Picasso.Builder 及其 build() 方法

先來看 Picasso 的構造方法,其主要目的是獲取 ApplicationCotnext,從而避免引發內存泄漏。

public Builder(@NonNull Context context) {
      checkNotNull(context, "context == null");
      this.context = context.getApplicationContext();
    }
複製代碼

再來看 build() 方法。

public Picasso build() {
      Context context = this.context;
      // 初始化磁盤 Cache,也即初始化 OkHttp 的 Cache
      okhttp3.Cache unsharedCache = null;
      if (callFactory == null) {
        File cacheDir = createDefaultCacheDir(context);
        long maxSize = calculateDiskCacheSize(cacheDir);
        unsharedCache = new okhttp3.Cache(cacheDir, maxSize);
        callFactory = new OkHttpClient.Builder()
            .cache(unsharedCache)
            .build();
      }
      // 初始化內存 Cache
      if (cache == null) {
        cache = new PlatformLruCache(Utils.calculateMemoryCacheSize(context));
      }
      // 初始化線程池
      if (service == null) {
        service = new PicassoExecutorService(new PicassoThreadFactory());
      }
      // 初始化內存 Cache 統計器
      Stats stats = new Stats(cache);
      // 初始化分發器
      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, cache, stats);
      // new 出 Picasso 實例
      return new Picasso(context, dispatcher, callFactory, unsharedCache, cache, listener,
          requestTransformers, requestHandlers, stats, defaultBitmapConfig, indicatorsEnabled,
          loggingEnabled);
    }
複製代碼

磁盤 Cache: 對了,磁盤在手機上通常指的就是閃存 Flash 或者外置的 SD Card 了。Picasso 沒有單獨實現一個磁盤緩存,而是利用了 OkHttp 的緩存管理,使得緩存的有效期與 Http 協議同步。這樣作的好處是保證了圖片的更新的及時性,而不像其餘磁盤緩存經過硬設一個緩存過時時間好比 7 天來判斷其是否過時。固然,它可能也會增長多一些網絡請求。另外,默認建立的磁盤 Cache 的路徑爲 App 的內部 data 目錄下 cache/picasso-cache,而大小通常爲磁盤空間總大小的 2%,可是也不能超過 50 M。

內存 Cache: PlatformLruCache,其內部經過 LruCache 來實現的內存 Cahce。而其大小被限制在應用可得到的最大 heap 內存的 15%。

/** A memory cache which uses a least-recently used eviction policy. */
final class PlatformLruCache {
  final LruCache<String, BitmapAndSize> cache;
  ......
}
複製代碼

線程池: PicassoExecutorService,其繼承自ThreadPoolExecutor,在其構造函數中設置了其核心線程數以及最大線程數爲 3 個,這就限制了其最多隻能有 3 個線程能同時工做。

class PicassoExecutorService extends ThreadPoolExecutor {
  private static final int DEFAULT_THREAD_COUNT = 3;

  PicassoExecutorService(ThreadFactory threadFactory) {
    super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
        new PriorityBlockingQueue<Runnable>(), threadFactory);
  }
  ......
}
複製代碼

Stats: 內存 Cache 統計器,統計 Cache 的各個指標,好比命中,未命中等。

分發器Dispatcher: 似曾相識的概念,在 OkHttp 中有見到過。這裏也是同樣,主要管理任務的分發與調度。包括提交,取消,重試以及網絡變化的監聽策略。

Picasso及其構造函數: Picasso 的構造會使得其持有 Dispatcher,Cache,PlatformLruCache,RequestTransformer 隊列,RequestHandler隊列等,這些引用都會保存在 Picasso 內部。除此以外,其自身也會初始化一些 RequestHandlers 用於處理已知的各類資源類型。當咱們傳遞不一樣的資源類型的請求時,其便會選擇相應的 RequestHandler 來進行處理。

另外,這裏還注意到,Picasso 實現了 LifecycleObserver 接口。這意味着咱們能夠將其註冊到 LifecycleOwner 的 LifecycleRegistry 中,從而實現生命週期的聯動。

public class Picasso implements LifecycleObserver {
  ......
  Picasso(Context context, Dispatcher dispatcher, Call.Factory callFactory,
      @Nullable okhttp3.Cache closeableCache, PlatformLruCache cache, @Nullable Listener listener,
      List<RequestTransformer> requestTransformers, List<RequestHandler> extraRequestHandlers,
      Stats stats, @Nullable Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled,
      boolean loggingEnabled) {
    this.context = context;
    this.dispatcher = dispatcher;
    this.callFactory = callFactory;
    this.closeableCache = closeableCache;
    this.cache = cache;
    this.listener = listener;
    this.requestTransformers = Collections.unmodifiableList(new ArrayList<>(requestTransformers));
    this.defaultBitmapConfig = defaultBitmapConfig;

    // Adjust this and Builder(Picasso) as internal handlers are added or removed.
    int builtInHandlers = 8;
    int extraCount = extraRequestHandlers.size();
    List<RequestHandler> allRequestHandlers = new ArrayList<>(builtInHandlers + extraCount);

    ......
    allRequestHandlers.add(ResourceDrawableRequestHandler.create(context));
    allRequestHandlers.add(new ResourceRequestHandler(context));
    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(callFactory, stats));
    requestHandlers = Collections.unmodifiableList(allRequestHandlers);

    this.stats = stats;
    this.targetToAction = new LinkedHashMap<>();
    this.targetToDeferredRequestCreator = new LinkedHashMap<>();
    this.indicatorsEnabled = indicatorsEnabled;
    this.loggingEnabled = loggingEnabled;
  }
  ......
}
複製代碼

到這裏,Picasso 的運行環境已經基本初始化好了,接下來就是等待用戶的請求發送了。

2.2 參數設置,從 RequestCreator 到 Request

前面經過 PicassoProvider.get() 構建了 Picasso 實例,同時也初始化了 Picasso 的運行環境,接下來就是一系列參數的設置。經過 Picasso 的 load() 方法即可獲得 Request 的建立器 RequestCreator,咱們將須要的參數都送進 RequestCreator 以便能最終構建出咱們所須要的那個 Request。

  • Picasso 的 load() 方法,構造 RequestCreator 實例

load() 重載了 4 個不一樣的方法,分別對應了加載不一樣的數據源,4 個方法以下。

public RequestCreator load(@Nullable Uri uri) {
    return new RequestCreator(this, uri, 0);
}
public RequestCreator load(@Nullable String path) {
    .......
    return load(Uri.parse(path));
}
public RequestCreator load(@Nullable File file) {
   ......
    return load(Uri.fromFile(file));
}
public RequestCreator load(@DrawableRes int resourceId) {
   ......
    return new RequestCreator(this, null, resourceId);
}
複製代碼

能夠看出前 3 個其實都是同一個,形參都將轉成 Uri。而從上面的代碼也能夠看出,最終都是 new 出一個 RequestCreator 實例。而且這裏 Uri 與 resourceId 是互斥的,必定只是其中的一個。再來看看 RequestCraetor 的構造方法。

RequestCreator(Picasso picasso, @Nullable Uri uri, int resourceId) {
    ......
    this.picasso = picasso;
    this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}
複製代碼

這裏最主要就是構造了 Request.Builder 實例,並存儲在 data 屬性中。其主要是用於在設置好參數後,進一步構造出 Request 實例。

  • RequestCreator.placeholder() 以及 error() 方法
public RequestCreator placeholder(@DrawableRes int placeholderResId) {
   ......
    this.placeholderResId = placeholderResId;
    return this;
}
public RequestCreator error(@DrawableRes int errorResId) {
    ......
    this.errorResId = errorResId;
    return this;
}
複製代碼

就是簡單的記錄下 placeholderResId 以及 errorResId。

  • RequestCreator.resizeDimen() 和 centerInside() 方法

resizeDeimen() 其實主是將參數 dimen 所表達的數值轉成實際大小,再調用 resize() 方法,因此這裏直接看 resize() 方法便可。

public RequestCreator resize(int targetWidth, int targetHeight) {
    data.resize(targetWidth, targetHeight);
    return this;
 }
  public RequestCreator centerCrop() {
    data.centerCrop(Gravity.CENTER);
    return this;
 }
複製代碼

這裏的 data 是 Request.Builder 實例,因此這裏是將參數送進了 RequestBuilder 中去了。和上面的 2 個方法比較一下,爲何參數會被存儲在不一樣的地方?

  • ReqeustCreator.tag()方法
public RequestCreator tag(@NonNull Object tag) {
    data.tag(tag);
    return this;
  }
複製代碼

tag 用來標記一個 Request ,能夠是任意一個對象,如 String,Context,Activity,Fragment等。咱們能夠經過 tag 來 pause,resume 以及 cancel 具有相同 tag 的 Request。但要注意的是 tag 與 Request 的生命週期是同樣長的,必定程度上可能會存在內存泄漏的風險。

關於 RequestCreator 的參數還有不少能夠設置的,這裏就不一一詳細進行講解了。僅經過類圖將其列舉出來以下。

RequestCreator

2.3 提交請求 into(ImageView)

  • into() 相關說明,關於 target,關於 fetch()

其實 into() 也是有不少重載的,好比into(BitmapTarget),into(RemoteViews, int viewId, int notificationId,Notification notification) 等。也就是說它還能夠將圖片顯示到 Notification 中以及 BitmapTarget 接口。關於 BitmapTarget 這個接口是什麼意思呢?這裏看一看其在註釋中舉的一個例子就能夠明白了。

public class ProfileView extends FrameLayout implements BitmapTarget {
    Override 
    public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
        setBackgroundDrawable(new BitmapDrawable(bitmap));
      }
    Override 
    public void onBitmapFailed(Exception e, Drawable errorDrawable) {
     setBackgroundDrawable(errorDrawable);
      }
    Override 
    public void onPrepareLoad(Drawable placeHolderDrawable) {
       setBackgroundDrawable(placeHolderDrawable);
     }
  }
複製代碼

也就是說咱們只實現這個接口,而後在 onBitmapLoaded() 的回調中本身選擇應該如何顯示該圖片。

而與 into() 相同級別的還有另外一個方法 fetch(),這個方法主要是隻下載圖片而不展現到 target 上去。這也是一個極爲有用的方法,典型的場景就是預加載。

關於 into() 以外的東西就瞭解這麼多,下面將重要放到 into(ImageView) 上來。into(ImageView) 其只是進一步調用了 into(ImageView,Callback),所以直接來看後面這個重載方法。

  • into(ImageView)提交請求

方法有點長,但其實很簡單,不要畏難,耐着性子看完,增長了比較詳細的註釋了。

public void into(@NonNull ImageView target, @Nullable Callback callback) {
    long started = System.nanoTime();
    // 檢查是否爲主線程,Picasso 要求請求必須從主線程發起。
    checkMain();
    // target 合法性檢查
    if (target == null) {
      throw new IllegalArgumentException("Target must not be null.");
    }
    // 若是沒有設置 url 或者  resId,則取消當前 ImageView 的 Request,並直接返回
    if (!data.hasImage()) {
      picasso.cancelRequest(target);
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }
    // 若是調用了 fit() ,也就是使得圖片大小爲適應 ImageView 的大小
    if (deferred) {
      if (data.hasSize()) {
        // fit() 與 resize() 不能同時調用,不然會拋出異常
        throw new IllegalStateException("Fit cannot be used with resize.");
      }
      int width = target.getWidth();
      int height = target.getHeight();
      if (width == 0 || height == 0) {
        // ImageView 沒有具體的高度或者還沒被渲染出來的狀況下
        if (setPlaceholder) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;
      }
      data.resize(width, height);
    }
    // 構建 Request
    Request request = createRequest(started);

    if (shouldReadFromMemoryCache(request.memoryPolicy)) {
      // 容許從內存緩存讀取則優先從內存緩存讀取
      Bitmap bitmap = picasso.quickMemoryCacheCheck(request.key);
      if (bitmap != null) {
        // 緩存中存在有須要的圖片,則取消並返回結果
        picasso.cancelRequest(target);
        RequestHandler.Result result = new RequestHandler.Result(bitmap, MEMORY);
        setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled);
        if (picasso.loggingEnabled) {
          log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
        }
        if (callback != null) {
          callback.onSuccess();
        }
        return;
      }
    }
   // 設置 PlasceHolder
    if (setPlaceholder) {
      setPlaceholder(target, getPlaceholderDrawable());
    }
   // 建立 ImageViewAction 並提交
    Action action = new ImageViewAction(picasso, target, request, errorDrawable, errorResId, noFade,
        callback);
    picasso.enqueueAndSubmit(action);
  }
複製代碼

方法裏的代碼都加了詳細的註釋,而且總結流程圖以下。

Into.jpg

流程圖上看也確實步驟比較多,但其實關鍵步驟就 2 個。其一,先判斷是否內存緩存是否已經存儲圖片了,若是有就用內存緩存的圖片。其二,若是沒有就構造一個 ImageViewAction 來提交到 Picasso 的請求隊列。說到緩存,這裏有一個重要的知識點,關於緩存的 key,它是在 Request 被 build 出來後一塊兒被建立的,其在 Request 的 createKey() 方法中。

private String createKey(StringBuilder builder) {
    Request data = this;
    // key 的主要部分
    if (data.stableKey != null) {
      builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);
      builder.append(data.stableKey);
    } else if (data.uri != null) {
      String path = data.uri.toString();
      builder.ensureCapacity(path.length() + KEY_PADDING);
      builder.append(path);
    } else {
      builder.ensureCapacity(KEY_PADDING);
      builder.append(data.resourceId);
    }
   // key 的分隔線
    builder.append(KEY_SEPARATOR);
    // 參數
    if (data.rotationDegrees != 0) {
      builder.append("rotation:").append(data.rotationDegrees);
      if (data.hasRotationPivot) {
        builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
      }
      builder.append(KEY_SEPARATOR);
    }
    if (data.hasSize()) {
      builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
      builder.append(KEY_SEPARATOR);
    }
    if (data.centerCrop) {
      builder.append("centerCrop:").append(data.centerCropGravity).append(KEY_SEPARATOR);
    } else if (data.centerInside) {
      builder.append("centerInside").append(KEY_SEPARATOR);
    }

    //noinspection ForLoopReplaceableByForEach
    for (int i = 0, count = data.transformations.size(); i < count; i++) {
      builder.append(data.transformations.get(i).key());
      builder.append(KEY_SEPARATOR);
    }
    return builder.toString();
  }
複製代碼

代碼看起來也有點長,但簡單理解下,cache key 主要由主體部分 + 參數部分構成。主體部分通常是指 url 或者 resId,而參數部分主要就是其是否有旋轉,resize 等。那這樣一來就獲得一個結論,就是同一個圖片在內存中可能由於要顯示的參數不同會有不一樣的緩存。這樣作的好處天然是提升了顯示速度,而壞處天然就是佔用的內存會較多。

2.4 入隊請求,分發請求Dispatcher,執行請求 BitmapHunter

  • 入隊到 Picasso

在 into(ImageView) 中,若是沒有命中 Cache,最後一步就是封裝 ImageViewAction,並提交給 picasso,由其進一步處理。

Action action = new ImageViewAction(picasso, target, request, errorDrawable, errorResId, noFade,
        callback);
picasso.enqueueAndSubmit(action);
複製代碼

先來看一看 enqueueAndSubmit

void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    if (targetToAction.get(target) != action) {
      // This will also check we are on the main thread.
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    submit(action);
  }
複製代碼

Picasso 中用 targetToAction 來保存了 target 與 action 之間的關係。這裏首先從裏面找 target 是否已經關聯了其餘的 action,若是已經關聯了則先要將其取消。這裏的場景就是特別適合 Adapter 了。在 Adapter 中因爲 ImageView 是會被重用的,那麼就必定會存在一個 target 對應不一樣的 action 的狀況。

  • 取消以前可能存在的 Request

這裏先來看一下任務的取消 cancelExistingRequest() 的實現,而後再去看 submit()。

void cancelExistingRequest(Object target) {
    checkMain();
    Action action = targetToAction.remove(target);
    if (action != null) {
      action.cancel();
      // 從 dispatcher 中取消
      dispatcher.dispatchCancel(action);
    }
    if (target instanceof ImageView) {
      ImageView targetImageView = (ImageView) target;
     // 若是有延遲的 request,也要將其取消,這裏假設沒有吧。
      DeferredRequestCreator deferredRequestCreator =
          targetToDeferredRequestCreator.remove(targetImageView);
      if (deferredRequestCreator != null) {
        deferredRequestCreator.cancel();
      }
    }
  }
複製代碼

因此主要是將 Dispatcher 中的 action 取消。Dispatcher 中主要用了一個 HandlerThread 來管理,而且全部關於請求的提交,取消等都是經過消息來執行的。以下所示。

static final int REQUEST_SUBMIT = 1;
  static final int REQUEST_CANCEL = 2;
  static final int HUNTER_COMPLETE = 4;
  static final int HUNTER_RETRY = 5;
  static final int HUNTER_DECODE_FAILED = 6;
  static final int NETWORK_STATE_CHANGE = 9;
  static final int AIRPLANE_MODE_CHANGE = 10;
  static final int TAG_PAUSE = 11;
  static final int TAG_RESUME = 12;
  static final int REQUEST_BATCH_RESUME = 13;
複製代碼

消息如何執行,這裏就不詳細說明了。Dispatcher 中的 action 取消明顯就是經過 REQUEST_CANCEL 來執行,而 REQUEST_CANCEL 對應的就是 performCance()。

void performCancel(Action action) {
   // 取消對應的 hunter
    String key = action.request.key;
    BitmapHunter hunter = hunterMap.get(key);
    if (hunter != null) {
      hunter.detach(action);
      if (hunter.cancel()) {
        hunterMap.remove(key);
        if (action.picasso.loggingEnabled) {
          log(OWNER_DISPATCHER, VERB_CANCELED, action.request.logId());
        }
      }
    }
    // 若是在 pause 隊列中,則移除
    if (pausedTags.contains(action.getTag())) {
      pausedActions.remove(action.getTarget());
      if (action.picasso.loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_CANCELED, action.request.logId(),
            "because paused request got canceled");
      }
    }
    // 失敗隊列中包含了,也移除
    Action remove = failedActions.remove(action.getTarget());
    if (remove != null && remove.picasso.loggingEnabled) {
      log(OWNER_DISPATCHER, VERB_CANCELED, remove.request.logId(), "from replaying");
    }
  }
複製代碼

hunter 是什麼呢?hunter 的中文意思是獵人或者搜尋者的意思。這裏就是負責執行實際請求的類 BitmapHunter,它實現了 Runnable 接口,是 Dispatcher 中 ExecuteService 線程池的實際調度單位。這裏先看一下它的 cancel(),後面還會講它的 submit()。

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

future 是 Future<?> 類型,它是 ExecuteService.submit() 的返回值,這裏咱們能夠經過其 cancel() 方法來取消 BitmapHunter 的執行。

  • 提交 Request

回到 Picasso 的 enqueueAndSubmit() 中來,下一步就是 submit(action)。submit() 將 action 經過 Dispatcher.dispatchSubmit()提交到 Dispatcher ,Dispatcher 將其轉化成消息 REQUEST_SUBMIT 發送到消息隊列中,而 REQUEST_SUBMIT 所對應的處理方法是 performSubmit(),所以從 performSubmit() 的代碼開始分析 Request 的提交。

void performSubmit(Action action, boolean dismissFailed) {
    ......
    // 已經提交了
    BitmapHunter hunter = hunterMap.get(action.request.key);
    if (hunter != null) {
      hunter.attach(action);
      return;
    }
    // 線程池已經關閉
    if (service.isShutdown()) {
      ......
      return;
    }
   // 建立一個新的 BitmapHunter
    hunter = forRequest(action.picasso, this, cache, stats, action);
   // 將 hunter 提交給線程池
    hunter.future = service.submit(hunter);
   // 已經提交的 hunter 保存在 hunterMap 中
    hunterMap.put(action.request.key, hunter);
    ......
  }
複製代碼

代碼中關鍵的邏輯是建立 hunter 並提交給線程 ExecuteService。這裏須要關注一下 forRequest 的實現,其裏面有一個關鍵步驟就是爲 Request 選擇一下合適的 Handler。

static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher,
      PlatformLruCache cache, Stats stats, Action action) {
    Request request = action.request;
    List<RequestHandler> requestHandlers = picasso.getRequestHandlers();
    // 選擇合適的 Handler 並建立 BitmapHunter
    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);
      }
    }
    // 一個都沒有
    return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
  }
複製代碼

假設這裏給的是 url 地址,且是以 http / https 開頭的,那被選中的就是 NetworkRequestHandler。

  • 執行 Request

當一個 Runnable 被提交到線程池 ExecuteService 中去以後,接下來就是等待其被度到,也就是執行其方法 run()。

@Override public void run() {
    try {
      ......
      result = hunt();
      ......
    } catch (NetworkRequestHandler.ResponseException e) {
     ......
    } finally {
      Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
    }
  }
複製代碼

精簡一下代碼,關鍵就是調用 hunt()。

Result hunt() throws IOException {
    // 1.先判斷緩存中是否存在,若是存在則直接返回
    if (shouldReadFromMemoryCache(data.memoryPolicy)) {
      Bitmap bitmap = cache.get(key);
      if (bitmap != null) {
        ......
        return new Result(bitmap, MEMORY);
      }
    }
    ......
    final AtomicReference<Result> resultReference = new AtomicReference<>();
    final AtomicReference<Throwable> exceptionReference = new AtomicReference<>();
    final CountDownLatch latch = new CountDownLatch(1);
    try {
      // 2. 經過 NetworkRequestHandler 加載圖片,並等待其返回。
      requestHandler.load(picasso, data, new RequestHandler.Callback() {
        @Override public void onSuccess(@Nullable Result result) {
          resultReference.set(result);
          latch.countDown();
        }

        @Override public void onError(@NonNull Throwable t) {
          exceptionReference.set(t);
          latch.countDown();
        }
      });

      latch.await();
    } catch (InterruptedException ie) {
      ......
    }
    ......
    Bitmap bitmap = result.getBitmap();
    if (bitmap != null) {
      ......
      // 3.執行全部的 Transformation,獲取最終的 result
      List<Transformation> transformations = new ArrayList<>(data.transformations.size() + 1);
      if (data.needsMatrixTransform() || result.getExifRotation() != 0) {
        transformations.add(new MatrixTransformation(data));
      }
      transformations.addAll(data.transformations);
      result = applyTransformations(picasso, data, transformations, result);
     .....
    }
    return result;
  }
複製代碼

關鍵也就是註釋中 3 個步驟: (1) 先判斷緩存中是否存在,若是存在則直接返回。這個在 into() 中也有判斷。 (2) 經過 NetworkRequestHandler 加載圖片,並等待其返回。關於 NetworkRequestHandler 的 load() 方法的實現,其實就是 OkHttp 的請求過程,這裏就沒有必要展開了。同時這裏使用了 CountDownLatch 類來使得當前線程能夠等待異步線程執行完成。這裏簡要了解一下其原理,即當 CountDownLatch.await() 使得當線程進入 awaiting 狀態後,只有經過 countDown() 使得其計數變成 0 後纔會喚醒當前的線程。具體原理圖以下。

CountDownLatch工做原理-圖片來自網絡

另外還使用了 AtomicReference,相似的還有 AtomicInteger 等,主要做用即是在併發編程中保證數據操做的原子性。這些都是併發編程的內容,這裏只做瞭解便可。

(3) 執行全部的 Transformation,獲取最終的 result。這裏的 Transformation 就是經過 RequestCreator 的 transform() 方法所添加的。Transformation 是一個接口,通常咱們可在其方法 transform() 中進行自定義處理,處理完後再將新的圖片返回。

到這裏,請求的提交到執行就分完成了。至此,整個從 Picasso 的初始化到構建 Request 設置參數,再到提交 request 以及 執行 request 都分析完畢了。

4、總結

Picasso 整體來講,其源碼難度較小,分析起來也比較輕鬆。其中最重要的兩個細節點在於:

  • 每個 Target 同一時刻只對應一個 Action,當同一個 Target 有新的請求時,當前 Action 會被取消掉。這就解決了 Adapter 中 ImageView 的重用問題,使得其不會因重用而致使圖片顯示錯亂。
  • 圖片在緩存中的 key,key 不只包括了 url 地址/資源 id,同時還包括了其形態參數,好比大小,旋轉等。這就使得同一圖片可能會存在多個緩存文件。

下面再以 Picasso 的框架圖來完成這篇文章的所有總結。

Picasso.jpg

最後,感謝你能讀到並讀完此文章。受限於做者水平有限,若是存在錯誤或者疑問都歡迎留言討論。若是個人分享可以幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。

相關文章
相關標籤/搜索