最近项目里把图片加载框架从xUtils换到了Picasso,一些下载和缓存的策略也因此发生变化,Picasso的缓存没有xUtils自动化那么高,使用起来遇到了一些困难,但是好在Picasso的源码非常清晰易读,很快就从源码上对Picasso的缓存策略有的大概的了解。
首先要明确一下几个概念,这里是以Picasso2.5.2+okhttp3为基础:
1、Picasso默认只有LRU缓存,也就是内存里面的缓存,Picasso默认不会将下载好的图片存储到磁盘上。(如果不信请往下看)
2、Picasso默认不会使用okhttp3作为自己的图片下载和缓存框架。(虽然官网上说如果项目中已经有了okhttp,会自动使用okhttp下载和缓存)
3、Picasso默认使用的下载工具是HttpURLConnection。
4、如果想实现Picasso的磁盘缓存并使用okhttp3作为下载器,需要手动编写代码,并且所有的磁盘缓存都交给okhttp3进行处理,Picasso没有和缓存有关的代码。说白了,Picasso的磁盘缓存主要靠Http框架完成。
好了这里总结出一个重要结论:Picasso负责内存中的LRU图片缓存,Http框架负责磁盘缓存。如果没有手动的指定Http框架的缓存,那么重启app之后又没有联网的话,之前下载好的图片也不会显示。
(注:如果想看Picasso+Okhttp3的实现效果,可以去下载我github上的一个demo:https://github.com/AlexZhuo/AlxPicassoProgress
这个项目中使用Picasoo2+Okhttp3实现了下载进度显示功能,Flash缓存功能等。)
那么如何指定okhttp3作为图片下载和缓存的框架,Picasso声称的自动支持okhttp为什么是不准确的呢,请看代码,先从Picasso的初始化代码看起
一般来说,普通的Picasso的用法是下面的一行代码
Picasso.with(context).load(url).error(默认图片).tag(context).into(imageView, callback);
其中with()方法就是进行一个static实例的初始化,让我们来看一下
public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}
一个普通的单例模式,而且线程安全,不过这不是重点,重点是Build.build()方法,让我们来看一下它到底是怎么初始化的
public Picasso build() {
Context context = this.context;
if (downloader == null) {//如果没有手动指定下载器
downloader = Utils.createDefaultDownloader(context);
}
if (cache == null) {//如果没有手动指定缓存策略
cache = new LruCache(context);//LRU内存缓存
}
if (service == null) {//如果没有手动指定线程池,那就用Picasso自己的默认线程池
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);
}
}
你看如果没有指定下载器,那么就会执行Utils.createDefaultDownloader()来获取一个下载器,那么Picasoo是如何获取这个下载器的呢?来看一下这个方法
static Downloader createDefaultDownloader(Context context) {
try {
Class.forName("com.squareup.okhttp.OkHttpClient");
return OkHttpLoaderCreator.create(context);
} catch (ClassNotFoundException ignored) {
}
return new UrlConnectionDownloader(context);
}
好了,看到这里就真相大白了,Picasso通过反射来检查com.squareup.okhttp.OkHttpClient这个类是否存在,也就是检测你的项目中有没有部署okhttp,如果有的话,就使用okHttp作为下载器,否则就使用HttpURLConnection
可问题是,okhttp3的类名已经不是这个了,而是换成了okhttp3.OkHttpClient,那么这个反射方法必然会失败,虽然你部署了okhttp3,但是Picasoo是不会用他的。
那么问题又来了,Picasso只给了两个downloader,旧版的okhttp和HttpURLConnection,那么如何让Picasso支持okhttp3呢?答案也很简单,自己写一个okhttp3的downloader就好了,因为Picasso已经给出了换downloader的api,换起来非常方便。并且在修改Downloader的时候,还可以自定义okhttp3的缓存策略和下载策略,做一些很有意思的自定义下载方法。
用户可以照着旧版的okHttpDownLoader自己写,也可以去网上下载一个现成的,这里我推荐一个github项目
https://github.com/JakeWharton/picasso2-okhttp3-downloader
这个项目里面只有一个java文件OkHttp3Downloader.java,把这个文件拷贝到自己的项目中去,然后这样用:
public static Picasso picasso = new Picasso.Builder(context)
.downloader(new OkHttp3Downloader(context))
.build()
picasso.load(url).placeholder(R.drawable.qraved_bg_default).error(R.drawable.qraved_bg_default).tag(context).into(target, null);
Picasso的LRU缓存工作分析
前面说过,Picasso只负责内存中LRU缓存的读写,那么Picasso是怎样控制的呢?
每次在Picasso第一次加载某张图片的时候,会执行downloader的load()方法,现在我把okhttp3Downloader的该方法实现贴出来:
@Override public Response load(Uri uri, int networkPolicy) throws IOException {
CacheControl cacheControl = null;
if (networkPolicy != 0) {
if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
cacheControl = CacheControl.FORCE_CACHE;//不要轻易设置成force_catche,可能会下载不到图片
} 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) {//这个对象为null并不影响okhttp3的缓存效果
builder.cacheControl(cacheControl);
}
okhttp3.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;//这里的fromCache为true则说明该图片是从okhttp3的磁盘缓存中读出来的,一般重启app加载同一个url的图片会这样,反之就是从网上现下的
ResponseBody responseBody = response.body();//这里body就是jpg文件的字节流
return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
}
下载成功之后,会在后面的方法中将下载好的jpg解析成bitmap并将bitmap存储到LRU缓存中,如果这个bitmap的缓存不被清理掉,那么下次要加载同一个url的图片的时候,就会通过Picasso的dispatcher分发器直接读取LRU缓存中bitmap,也就不需要掉load()方法从网上再下载一次了,这种现象在上下滚动listView的时候十分常见,下面贴出BitmapHunter.java中分发读取缓存的代码:
Bitmap hunt() throws IOException {
Bitmap bitmap = null;
if (shouldReadFromMemoryCache(memoryPolicy)) {
bitmap = cache.get(key);
if (bitmap != null) {//如果从LRU缓存中得到了相应的bitmap
stats.dispatchCacheHit();
loadedFrom = MEMORY;
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
}
return bitmap;
}
}
//如果没用从LRU中读到bitmap,那么就联网下载(可能会经过okhttp3的磁盘缓存)
data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
RequestHandler.Result result = requestHandler.load(data, networkPolicy);//同步访问网络
if (result != null) {
loadedFrom = result.getLoadedFrom();
exifRotation = result.getExifOrientation();
bitmap = result.getBitmap();
// If there was no Bitmap then we need to decode it from the stream.
if (bitmap == null) {
InputStream is = result.getStream();
try {
bitmap = decodeStream(is, data);
} finally {
Utils.closeQuietly(is);
}
}
}
if (bitmap != null) {
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId());
}
stats.dispatchBitmapDecoded(bitmap);
if (data.needsTransformation() || exifRotation != 0) {
synchronized (DECODE_LOCK) {
if (data.needsMatrixTransform() || exifRotation != 0) {
bitmap = transformResult(data, bitmap, exifRotation);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
}
}
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(data.transformations, bitmap);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
}
}
}
if (bitmap != null) {
stats.dispatchBitmapTransformed(bitmap);
}
}
}
return bitmap;
}
第一次获取图片:搜索本地LRU缓存发现没有-->>分发器调用okhttp3下载图片-->>okhttp3自动将图片缓存为文件-->>picasoo解码jpg文件为bitmap-->>Picasoo显示图片-->>Picasso将自己的图片缓存到LRU内存中
第二次加载同一个URL:搜索本地LRU缓存发现存在-->>分发器获得bitmap-->>显示bitmap
第二次加载同一个URL但LRU缓存失效: 搜索本地LRU缓存发现已经失效-->>分发器调用okhttp-->>okhttp扫描磁盘缓存发现存在-->>okhttp读取磁盘缓存将输出流交给Picasso-->>Picasso解码并显示
注意:观察okhttp是不是从磁盘中读取的缓存,可以打印downloader的load()方法的 boolean fromCache = response.cacheResponse() != null;这个布尔值
注意:picasso对象一定要是单例模式,不然LRU缓存会失效
okhttp3的缓存玩法
Picasso偷懒之处在于它不做sd卡的磁盘图片缓存,所以每次重启app上次加载好的图片会丢失,Picasso依赖Http加载框架为它做磁盘缓存。
在使用okhttp3下载器的时候发现,okhttp3已经自动帮我们实现了大文件下载的缓存,这样就实现了我们关闭app重启之后,原来下载好的图片即使不联网也能显示,但是Picasso默认使用的URLConnectionDownloader不支持这一点。但是okhttp3普通的GET方法却不会自动的缓存,但是如果okhttp3没有实现图片下载的缓存,或者想实现在没有联网的情况下让okhttp3直接抓取上一次的缓存内容应该怎么办呢?
注:查看okhttp3是否从缓存加载的图片方法是打印downloader的load()方法的 boolean fromCache = response.cacheResponse() != null;
现在需要我们对okhttp3的缓存控制有一定了解,并修改上面github上的那个okhttp3的下载器的一个构造函数如下:
private static OkHttpClient defaultOkHttpClient(File cacheDir, long maxSize) {
Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Pragma")//去掉一个header
.header("Cache-Control", String.format("max-age=%d", 480))//添加本地缓存过期时间,单位是秒
.build();
}
};
return new OkHttpClient.Builder()
.cache(new okhttp3.Cache(cacheDir, maxSize))
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.build();
}
这样就手动配置了okhttp3磁盘缓存的过期时间,也就是在过期时间到达之前,okhttp3会把已经下载好的jpg文件存在sd卡中,等待下次相同url的调用。同样可以根据这个配置,可以解决一些棘手的情况如服务器端的图像已经改变了,但是客户端由于缓存的原因没变的问题
下面是okhttp3缓存位置和大小的配置代码
private static final String PICASSO_CACHE = "picasso-cache";
private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
private static File createDefaultCacheDir(Context context) {
File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE);
if (!cache.exists()) {
//noinspection ResultOfMethodCallIgnored
cache.mkdirs();
}
return cache;
}
private static long calculateDiskCacheSize(File dir) {
long size = MIN_DISK_CACHE_SIZE;
try {
StatFs statFs = new StatFs(dir.getAbsolutePath());
long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();
// Target 2% of the total space.
size = available / 50;
} catch (IllegalArgumentException ignored) {
}
// Bound inside min/max size for disk cache.
return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
}
Picasso下载器的高级玩法——自动重定向:
项目里面遇到这样一个需求,下载一张照片需要经过三个服务器,一个是提供假url的数据库服务器,一个是将假url翻译成真url的解密服务器,一个是存放有图片的CDN服务器
数据库服务器(给假url) 解密服务器 CDN服务器
现在需要实现这样一个流程,从数据库服务器通过json获得一个假url,然后把假url发给解密服务器获得一个真url,然后用真url去CDN服务器下载照片那么问题来了,Picasso的LRU缓存是以url为key,bitmap为value的,如果我用真url去下载,而用假url做key,那么Picasso的缓存就没用啦,这样用户体验就完蛋了,那么能不能我直接将假url给Picasso,让后Picasso自动的去获取真url并自动下载呢(并以假url做key)?通过自定义下载器可以非常容易的实现这一点,方法还是修改下载器的load()方法,让他在一个load()中请求两次网络,一次是解密服务器获取String字符串url,第二次是拿着真的url去CDN服务器下载图片,代码如下
@Override public Response load(Uri uri, int networkPolicy) throws IOException {
JLogUtils.i("AlexImage","假的uri是->"+uri+" 缓存策略是"+networkPolicy);
CacheControl cacheControl = null;
if (networkPolicy != 0) {
if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
JLogUtils.i("AlexImage","准备强制缓存");
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 php_builder = new Request.Builder().url(uri.toString());
if (cacheControl != null) {
php_builder.cacheControl(cacheControl);
}
long startTime = System.currentTimeMillis();
okhttp3.Response php_response = client.newCall(php_builder.build()).execute();//执行第一次http请求连接解密服务器
int php_responseCode = php_response.code();
JLogUtils.i("AlexImage","解密服务器响应码是"+php_responseCode);
if (php_responseCode >= 300) {
php_response.body().close();
throw new ResponseException(php_responseCode + " " + php_response.message(), networkPolicy, php_responseCode);
}
boolean fromPhpCache = php_response.cacheResponse() != null;
JLogUtils.i("AlexImage","解密服务器的响应是不是从缓存拿的呀?"+fromPhpCache);
JLogUtils.i("AlexImage","全部的header是"+php_response.headers());
if(php_response.header("Content-Type").equals("text/plain")){//如果php发来的是cdn的图片url,这里通过header进行区分
JLogUtils.i("AlexImage","现在是从php取得的url字符串而不是jpg");
String cdnUrl = php_response.body().string();
JLogUtils.i("AlexImage","php服务器响应时间"+(System.currentTimeMillis() - startTime));
JLogUtils.i("AlexImage","CDN的imageUrl是->"+cdnUrl);
Request.Builder cdn_builder = new Request.Builder().url(cdnUrl);
if (cacheControl != null) {
cdn_builder.cacheControl(cacheControl);
}
long cdnStartTime = System.currentTimeMillis();
okhttp3.Response cdn_response = client.newCall(cdn_builder.build()).execute();//执行第二次http请求连接CDN服务器
int cdn_responseCode = cdn_response.code();
JLogUtils.i("AlexImage","cdn的响应码是"+cdn_responseCode);
if (cdn_responseCode >= 300) {
cdn_response.body().close();
throw new ResponseException(cdn_responseCode + " " + cdn_response.message(), networkPolicy,
cdn_responseCode);
}
JLogUtils.i("AlexImage","cdn响应时间"+(System.currentTimeMillis() - cdnStartTime));
boolean fromCache = cdn_response.cacheResponse() != null;
ResponseBody cdn_responseBody = cdn_response.body();
JLogUtils.i("AlexImage","cdn的图片是不是从缓存拿的呀?fromCache = "+fromCache);
return new Response(cdn_responseBody.byteStream(), fromCache, cdn_responseBody.contentLength());
}else {//如果php发来的不是图片的URL,那就直接用php发来的图片
JLogUtils.i("AlexImage","准备直接用PHP的图片!!!");
boolean fromCache = php_response.cacheResponse() != null;
ResponseBody responseBody = php_response.body();
return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
}
}