Picasso源码分析(2):默认的下载器、缓存、线程池和转换器
下载器
当用户没有为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,并没有做额外的变换处理。