Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 使用okhttp3做Android圖片框架Picasso的下載器和緩存器

使用okhttp3做Android圖片框架Picasso的下載器和緩存器

編輯:關於Android編程

最近項目裡把圖片加載框架從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之後又沒有聯網的話,之前下載好的圖片也不會顯示。

那麼如何指定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(client))
    .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服務器

\\\

數據庫服務器(給假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();
                    JLogUtils.i("AlexImage","我擦居然不緩存");
                }
                if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
                    builder.noStore();
                    JLogUtils.i("AlexImage","我擦居然不緩存2");
                }
                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自動重定向的功能!
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved