使用okhttp3做Android图片框架Picasso的下载器和缓存器

最近项目里把图片加载框架从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内存缓存会失效,那时候你滚动一个listView,滚下去再滚上来,原本下载好的图片会重新下一遍的

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服务器

使用okhttp3做Android图片框架Picasso的下载器和缓存器_第1张图片使用okhttp3做Android图片框架Picasso的下载器和缓存器_第2张图片使用okhttp3做Android图片框架Picasso的下载器和缓存器_第3张图片

数据库服务器(给假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());
        }
    }

这样通过自定义Picasso的下载器就完成了一个复杂的url自动重定向的功能!


你可能感兴趣的:(使用okhttp3做Android图片框架Picasso的下载器和缓存器)