編輯:關於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緩存工作分析
前面說過,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()); } }
主要講解Android Studio中生成aar文件以及本地方式使用aar文件的方法。 在Android Studio中對一個自己庫進行生成操作時將會同時生成*.jar與
最近移植了很多C++平台的庫,很多都是後台開發的庫,因為NDK開發,以後很可能會使用,提前預研一下。libcurl這個庫很有名,用的人比較多,下載源碼,直接就可以編譯使用
本文主要介紹Android ViewGroup/View的繪制流程,及常用的自定義ViewGroup的方法。在此基礎上介紹動態控制View的位置的三種方法,並給出最佳的
現在的移動支付越來越便捷,為了防止被他人隨意使用,很多人都開始使用鎖屏功能。但是傳統的鎖屏功能都是使用的單一密碼,這樣被他人破解的可能性又大大的增加。那麼有