Picasso原始碼分析(二):預設的下載器、快取、執行緒池和轉換器

王世暉發表於2016-06-12

Picasso原始碼分析(一):單例模式、建造者模式、面向介面程式設計
Picasso原始碼分析(二):預設的下載器、快取、執行緒池和轉換器
Picasso原始碼分析(三):快照功能實現和HandlerThread的使用
Picasso原始碼分析(四):不變模式、建造者模式和Request的預處理
Picasso原始碼分析(五):into方法追本溯源和責任鏈模式建立BitmapHunter
Picasso原始碼分析(六):BitmapHunter與請求結果的處理

下載器

當使用者沒有為Picasso指定下載器的時候Picasso會通過Utils.createDefaultDownloader(context)方法建立一個預設的下載器

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

可見如果反射發現應用已經整合了okhttp,那麼使用okhttp建立一個下載器,否則使用HttpURLConnection建立下載器。兩種方式建立的下載器需要實現Downloader介面,使用者也可以實現自己的下載器,實現Downloader介面即可。下邊只分析OkHttpDownloader,UrlConnectionDownloader道理類似。

    @Override 
    public Response load(Uri uri, int networkPolicy) throws IOException {
    CacheControl cacheControl = null;
    if (networkPolicy != 0) {
      if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
        cacheControl = CacheControl.FORCE_CACHE;
      } else {
        CacheControl.Builder builder = new CacheControl.Builder();
        if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
          builder.noCache();
        }
        if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
          builder.noStore();
        }
        cacheControl = builder.build();
      }
    }

    Request.Builder builder = new Request.Builder().url(uri.toString());
    if (cacheControl != null) {
      builder.cacheControl(cacheControl);
    }

    com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();
    int responseCode = response.code();
    if (responseCode >= 300) {
      response.body().close();
      throw new ResponseException(responseCode + " " + response.message(), networkPolicy,
          responseCode);
    }

    boolean fromCache = response.cacheResponse() != null;

    ResponseBody responseBody = response.body();
    return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
  }

由於OkHttpDownloader實現了Downloader介面,因此終點關注覆寫DownLoader的兩個方法。
load方法的註釋如下:

  /**
   * Download the specified image url from the internet.
   *
   * @param uri Remote image URL.
   * @param networkPolicy The NetworkPolicy used for this request.
   * @return Response containing either a  Bitmap representation of the request or an
   * InputStream for the image data. null can be returned to indicate a problem
   * loading the bitmap.
   * @throws IOException if the requested URL cannot successfully be loaded.
   */
  Response load(Uri uri, int networkPolicy) throws IOException;

load方法傳入圖片的url和網路策略,返回一個請求到的Bitmap或者InputStream,出錯就返回null。
接下來是一點我覺得有一點小瑕疵,以為出現 了魔數0,方法呼叫者傳入0表示沒有設定快取策略

    if (networkPolicy != 0) {
    ...
    }

這乍一看還真弄不明白0表示什麼意思,牽扯到列舉和數字的轉換

/** Designates the policy to use for network requests. */
public enum NetworkPolicy {
  /** Skips checking the disk cache and forces loading through the network. */
  NO_CACHE(1 << 0),
  /**
   * Skips storing the result into the disk cache.
   * Note: At this time this is only supported if you are using OkHttp.
   */
  NO_STORE(1 << 1),
  /** Forces the request through the disk cache only, skipping network. */
  OFFLINE(1 << 2);
  ...

原來NetworkPolicy是一個列舉類,1表示不快取,強制從網路獲取圖片,2表示不儲存快取,4表示不做網路請求強制讀快取。
所以如果load方法如果傳入的networkPolicy不是0,那麼就解析該networkPolicy對應的NetworkPolicy,根據策略不同做不同的處理。
isOfflineOnly方法通過位運算判斷網路策略是否為離線模式

  public static boolean shouldReadFromDiskCache(int networkPolicy) {
    return (networkPolicy & NetworkPolicy.NO_CACHE.index) == 0;
  }
  public static boolean shouldWriteToDiskCache(int networkPolicy) {
    return (networkPolicy & NetworkPolicy.NO_STORE.index) == 0;
  }
  public static boolean isOfflineOnly(int networkPolicy) {
    return (networkPolicy & NetworkPolicy.OFFLINE.index) != 0;
  }

同理shouldReadFromDiskCache方法判斷是否可以讀寫快取,shouldWriteToDiskCache方法判斷是否可以儲存快取。
如果為離線模式就設定cacheControl為CacheControl.FORCE_CACHE,表示強制okhttp讀取快取。CacheControl為okhttp的快取控制類。
否則的話通過建造者模式構建CacheControl物件

CacheControl.Builder builder = new CacheControl.Builder();

然後設定相應的快取策略,最終通過CacheControl的內部類Builder的build方法建立CacheControl物件。
接著還是通過建造者模式建立okhttp的Request物件,先通過Reqest的內部類Builder構造一個builder,然後給builder設定相應的屬性

    Request.Builder builder = new Request.Builder().url(uri.toString());
    if (cacheControl != null) {
      builder.cacheControl(cacheControl);
    }

最後呼叫builder的build方法就獲取到一個Request物件
有了Request,就可以通過okhttp的newCall方法進行非同步網路請求

    com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();

這樣獲取到了okhttp的Response,需要判斷此Response的請求狀態碼是否大於300,200表示請求正常,大於300表示請求出現問題,比如常見的狀態嗎及表達的意思如下:
300表示 伺服器根據請求可執行多種操作
301表示永久重定向
302表示臨時重定向
400表示請求語義或者引數有誤,伺服器不能理解
403表示伺服器理解請求但是拒絕執行
404表示請求的資源在伺服器上沒有找到
5XX表示伺服器出現了問題
接下來需要將okhttp的Response轉化為Downloader的Response

 boolean fromCache = response.cacheResponse() != null;
    ResponseBody responseBody = response.body();
    return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());

OkHttpDownloader覆寫Downloader的shutdown方法比較簡單,關閉okhttp的快取即可

  @Override 
  public void shutdown() {
    com.squareup.okhttp.Cache cache = client.getCache();
    if (cache != null) {
      try {
        cache.close();
      } catch (IOException ignored) {
      }
    }
  }

快取

如果使用者沒有為Picasso設定快取的話,Picasso會預設建立一個LruCache快取

      if (cache == null) {
        cache = new LruCache(context);
      }

而LruCache實際上是一個通過LinkedHashMap實現的記憶體快取,實現了Cache介面

/** A memory cache which uses a least-recently used eviction policy. */
public class LruCache implements Cache {
final LinkedHashMap<String, Bitmap> map;
...

LruCache還有一些其他的統計量屬性,如下,良好的命名規則讓讀者顧名思義

  private final int maxSize;
  private int size;
  private int putCount;
  private int evictionCount;
  private int hitCount;
  private int missCount;

由於java集合框架提供的LinkedHashMap可以按照LRU最近最少使用原則維護列表的訪問順序,因此天然適合做Lru快取。只需要將LinkedHashMap建構函式的第二個引數傳遞為true接可以按照訪問順序而不是插入順序維護列表的元素了。

     /**
     * ...
     * @param accessOrder
     *            true if the ordering should be done based on the last
     *            access (from least-recently accessed to most-recently
     *            accessed), and  false if the ordering should be the
     *            order in which the entries were inserted.
     * ...
     */
    this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);

所以LruCache只需實現很少的程式碼就可以了。以get和set方法為例分析。

  @Override 
  public Bitmap get(String key) {
    if (key == null) {
      throw new NullPointerException("key == null");
    }
    Bitmap mapValue;
    synchronized (this) {
      mapValue = map.get(key);
      if (mapValue != null) {
        hitCount++;
        return mapValue;
      }
      missCount++;
    }
    return null;
  }

LruCache不支援null的鍵,所以需要首先做引數合法性檢查
接著同步鎖定LruCache物件,從LinkedHashMap中獲取對應key的值,獲取成功增加hitCount,返回value,否則增加missCount,返回null。

 @Override 
 public void set(String key, Bitmap bitmap) {
    if (key == null || bitmap == null) {
      throw new NullPointerException("key == null || bitmap == null");
    }
    Bitmap previous;
    synchronized (this) {
      putCount++;
      size += Utils.getBitmapBytes(bitmap);
      previous = map.put(key, bitmap);
      if (previous != null) {
        size -= Utils.getBitmapBytes(previous);
      }
    }
    trimToSize(maxSize);
  }

LruCache不支援null的鍵和null的值,因此set方法首先檢查傳入引數的合法性
接著同樣的同步鎖定LruCache物件,增加putCount和size

      putCount++;
      size += Utils.getBitmapBytes(bitmap);

可見size表示的並不是LinkedHashMap中儲存鍵值對的個數,而是所有bitmap快取佔據的儲存空間的大小
接著獲取舊的快取,如果之前儲存的有對應key的舊的快取,那麼因為快取替換的原因,需要減去舊快取佔據的儲存空間

      if (previous != null) {
        size -= Utils.getBitmapBytes(previous);
      }

不管是新增快取還是替換快取,都改變了儲存空間的大小
所以需要重新調整

  private void trimToSize(int maxSize) {
    while (true) {
      String key;
      Bitmap value;
      synchronized (this) {
        if (size < 0 || (map.isEmpty() && size != 0)) {
          throw new IllegalStateException(
              getClass().getName() + ".sizeOf() is reporting inconsistent results!");
        }

        if (size <= maxSize || map.isEmpty()) {
          break;
        }

        Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        map.remove(key);
        size -= Utils.getBitmapBytes(value);
        evictionCount++;
      }
    }
  }

調整大小的思路也比較簡單,只要size大於了maxSize,就不停的根據LRU原則刪除最近最少使用的快取。

        Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        map.remove(key);
        size -= Utils.getBitmapBytes(value);
        evictionCount++;

直到size不大於maxSize或者LinkedHashMap物件空了就不需要繼續刪除快取了。

        if (size <= maxSize || map.isEmpty()) {
          break;
        }

執行緒池

Picasso提供了一個預設的執行緒池

      if (service == null) {
        service = new PicassoExecutorService();
      }

PicassoExecutorService繼承自ThreadPoolExecutor,定製了一個執行緒池

class PicassoExecutorService extends ThreadPoolExecutor {
  private static final int DEFAULT_THREAD_COUNT = 3;
  PicassoExecutorService() {
    super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
        new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
  }
  ....

核心執行緒數和最大執行緒數都是3,超時設定為0,所以超時單位無意義,設定為毫秒,阻塞佇列設定為一個優先順序佇列,傳入自定義的一個執行緒工廠。ThreadPoolExecutor建構函式引數比較多,每個引數的意義如下注釋所示。

    /**
     * Creates a new ThreadPoolExecutor with the given initial
     * parameters and default thread factory and rejected execution handler.
     * It may be more convenient to use one of the  Executors factory
     * methods instead of this general purpose constructor.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless  allowCoreThreadTimeOut is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the  keepAliveTime argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the  Runnable
     *        tasks submitted by the execute method.
     * @throws IllegalArgumentException if one of the following holds:
     *          corePoolSize < 0
     *          keepAliveTime < 0
     *          maximumPoolSize <= 0
     *          maximumPoolSize < corePoolSize
     * @throws NullPointerException if  workQueue is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

PicassoExecutorService設計最好的地方在於可以根據不同的網路情況設定不同的執行緒數,這也是解決弱網路的一個思路,網路好的情況下建立較多的請求執行緒,提高併發度,網路差的時候設定較少的請求執行緒節約資源。

  void adjustThreadCount(NetworkInfo info) {
    if (info == null || !info.isConnectedOrConnecting()) {
      setThreadCount(DEFAULT_THREAD_COUNT);
      return;
    }
    switch (info.getType()) {
      case ConnectivityManager.TYPE_WIFI:
      case ConnectivityManager.TYPE_WIMAX:
      case ConnectivityManager.TYPE_ETHERNET:
        setThreadCount(4);
        break;
      case ConnectivityManager.TYPE_MOBILE:
        switch (info.getSubtype()) {
          case TelephonyManager.NETWORK_TYPE_LTE:  // 4G
          case TelephonyManager.NETWORK_TYPE_HSPAP:
          case TelephonyManager.NETWORK_TYPE_EHRPD:
            setThreadCount(3);
            break;
          case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
          case TelephonyManager.NETWORK_TYPE_CDMA:
          case TelephonyManager.NETWORK_TYPE_EVDO_0:
          case TelephonyManager.NETWORK_TYPE_EVDO_A:
          case TelephonyManager.NETWORK_TYPE_EVDO_B:
            setThreadCount(2);
            break;
          case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
          case TelephonyManager.NETWORK_TYPE_EDGE:
            setThreadCount(1);
            break;
          default:
            setThreadCount(DEFAULT_THREAD_COUNT);
        }
        break;
      default:
        setThreadCount(DEFAULT_THREAD_COUNT);
    }
  }

可以看到在wifi網路連線的情況下設定併發執行緒數為4,4G網路併發執行緒數為3,3G網路併發執行緒數為2,2G網路併發執行緒數為1。
setThreadCount方法實際上呼叫了ThreadPoolService的setCorePoolSize方法和setMaxinumPoolSize方法

  private void setThreadCount(int threadCount) {
    setCorePoolSize(threadCount);
    setMaximumPoolSize(threadCount);
  }

轉換器

Picasso提供的預設的轉換器實際上什麼也沒有做,因此需要改變圖片大小等操作需要專門處理,先看看預設的實現。

      if (transformer == null) {
        transformer = RequestTransformer.IDENTITY;
      }

轉換器就是一個請求被提交前對請求進行的轉換處理,可以在提交請求之前對該請求進行一些變換處理操作。

  /**
   * A transformer that is called immediately before every request is submitted. This can be used to
   * modify any information about a request.
   */
  public interface RequestTransformer {
    /**
     * Transform a request before it is submitted to be processed.
     * @return The original request or a new request to replace it. Must not be null.
     */
    Request transformRequest(Request request);
    /** A  RequestTransformer which returns the original request. */
    RequestTransformer IDENTITY = new RequestTransformer() {
      @Override public Request transformRequest(Request request) {
        return request;
      }
    };
  }

可見IDENTITY直接返回了原來的request,並沒有做額外的變換處理。

相關文章