Picasso源码分析(二):默认的下载器、缓存、线程池和转换器

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,并没有做额外的变换处理。

猜你喜欢

转载自blog.csdn.net/shihui512/article/details/51648893