Android Volley圖片加載功能詳解
Gituhb項(xiàng)目
Volley源碼中文注釋項(xiàng)目我已經(jīng)上傳到github,歡迎大家fork和start.
為什么寫這篇博客
本來文章是維護(hù)在github上的,但是我在分析ImageLoader源碼過程中與到了一個(gè)問題,希望大家能幫助解答.
Volley獲取網(wǎng)絡(luò)圖片
本來想分析Universal Image Loader的源碼,但是發(fā)現(xiàn)Volley已經(jīng)實(shí)現(xiàn)了網(wǎng)絡(luò)圖片的加載功能.其實(shí),網(wǎng)絡(luò)圖片的加載也是分幾個(gè)步驟:
1. 獲取網(wǎng)絡(luò)圖片的url.
2. 判斷該url對(duì)應(yīng)的圖片是否有本地緩存.
3. 有本地緩存,直接使用本地緩存圖片,通過異步回調(diào)給ImageView進(jìn)行設(shè)置.
4. 無本地緩存,就先從網(wǎng)絡(luò)拉取,保存在本地后,再通過異步回調(diào)給ImageView進(jìn)行設(shè)置.
我們通過Volley源碼,看一下Volley是否是按照這個(gè)步驟實(shí)現(xiàn)網(wǎng)絡(luò)圖片加載的.
ImageRequest.java
按照Volley的架構(gòu),我們首先需要構(gòu)造一個(gè)網(wǎng)絡(luò)圖片請(qǐng)求,Volley幫我們封裝了ImageRequest類,我們來看一下它的具體實(shí)現(xiàn):
/** 網(wǎng)絡(luò)圖片請(qǐng)求類. */ @SuppressWarnings("unused") public class ImageRequest extends Request<Bitmap> { /** 默認(rèn)圖片獲取的超時(shí)時(shí)間(單位:毫秒) */ public static final int DEFAULT_IMAGE_REQUEST_MS = 1000; /** 默認(rèn)圖片獲取的重試次數(shù). */ public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; private final Response.Listener<Bitmap> mListener; private final Bitmap.Config mDecodeConfig; private final int mMaxWidth; private final int mMaxHeight; private ImageView.ScaleType mScaleType; /** Bitmap解析同步鎖,保證同一時(shí)間只有一個(gè)Bitmap被load到內(nèi)存進(jìn)行解析,防止OOM. */ private static final Object sDecodeLock = new Object(); /** * 構(gòu)造一個(gè)網(wǎng)絡(luò)圖片請(qǐng)求. * @param url 圖片的url地址. * @param listener 請(qǐng)求成功用戶設(shè)置的回調(diào)接口. * @param maxWidth 圖片的最大寬度. * @param maxHeight 圖片的最大高度. * @param scaleType 圖片縮放類型. * @param decodeConfig 解析bitmap的配置. * @param errorListener 請(qǐng)求失敗用戶設(shè)置的回調(diào)接口. */ public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, ImageView.ScaleType scaleType, Bitmap.Config decodeConfig, Response.ErrorListener errorListener) { super(Method.GET, url, errorListener); mListener = listener; mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; mScaleType = scaleType; } /** 設(shè)置網(wǎng)絡(luò)圖片請(qǐng)求的優(yōu)先級(jí). */ @Override public Priority getPriority() { return Priority.LOW; } @Override protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { synchronized (sDecodeLock) { try { return doParse(response); } catch (OutOfMemoryError e) { return Response.error(new VolleyError(e)); } } } private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); Bitmap bitmap; if (mMaxWidth == 0 && mMaxHeight == 0) { decodeOptions.inPreferredConfig = mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { // 獲取網(wǎng)絡(luò)圖片的真實(shí)尺寸. decodeOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); int actualWidth = decodeOptions.outWidth; int actualHeight = decodeOptions.outHeight; int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); decodeOptions.inJustDecodeBounds = false; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desireHeight)) { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true); tempBitmap.recycle(); } else { bitmap = tempBitmap; } } if (bitmap == null) { return Response.error(new VolleyError(response)); } else { return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); } } static int findBestSampleSize( int actualWidth, int actualHeight, int desiredWidth, int desireHeight) { double wr = (double) actualWidth / desiredWidth; double hr = (double) actualHeight / desireHeight; double ratio = Math.min(wr, hr); float n = 1.0f; while ((n * 2) <= ratio) { n *= 2; } return (int) n; } /** 根據(jù)ImageView的ScaleType設(shè)置圖片的大小. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ImageView.ScaleType scaleType) { // 如果沒有設(shè)置ImageView的最大值,則直接返回網(wǎng)絡(luò)圖片的真實(shí)大小. if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } // 如果ImageView的ScaleType為FIX_XY,則將其設(shè)置為圖片最值. if (scaleType == ImageView.ScaleType.FIT_XY) { if (maxPrimary == 0) { return actualPrimary; } return maxPrimary; } if (maxPrimary == 0) { double ratio = (double)maxSecondary / (double)actualSecondary; return (int)(actualPrimary * ratio); } if (maxSecondary == 0) { return maxPrimary; } double ratio = (double) actualSecondary / (double) actualPrimary; int resized = maxPrimary; if (scaleType == ImageView.ScaleType.CENTER_CROP) { if ((resized * ratio) < maxSecondary) { resized = (int)(maxSecondary / ratio); } return resized; } if ((resized * ratio) > maxSecondary) { resized = (int)(maxSecondary / ratio); } return resized; } @Override protected void deliverResponse(Bitmap response) { mListener.onResponse(response); } }
因?yàn)閂olley本身框架已經(jīng)實(shí)現(xiàn)了對(duì)網(wǎng)絡(luò)請(qǐng)求的本地緩存,所以ImageRequest做的主要事情就是解析字節(jié)流為Bitmap,再解析過程中,通過靜態(tài)變量保證每次只解析一個(gè)Bitmap防止OOM,使用ScaleType和用戶設(shè)置的MaxWidth和MaxHeight來設(shè)置圖片大小.
總體來說,ImageRequest的實(shí)現(xiàn)非常簡(jiǎn)單,這里不做過多的講解.ImageRequest的缺陷在于:
1.需要用戶進(jìn)行過多的設(shè)置,包括圖片的大小的最大值.
2.沒有圖片的內(nèi)存緩存,因?yàn)閂olley的緩存是基于Disk的緩存,有對(duì)象反序列化的過程.
ImageLoader.java
鑒于以上兩個(gè)缺點(diǎn),Volley又提供了一個(gè)更牛逼的ImageLoader類.其中,最關(guān)鍵的就是增加了內(nèi)存緩存.
再講解ImageLoader的源碼之前,需要先介紹一下ImageLoader的使用方法.和之前的Request請(qǐng)求不同,ImageLoader并不是new出來直接扔給RequestQueue進(jìn)行調(diào)度,它的使用方法大體分為4步:
•創(chuàng)建一個(gè)RequestQueue對(duì)象.
RequestQueue queue = Volley.newRequestQueue(context);
•創(chuàng)建一個(gè)ImageLoader對(duì)象.
ImageLoader構(gòu)造函數(shù)接收兩個(gè)參數(shù),第一個(gè)是RequestQueue對(duì)象,第二個(gè)是ImageCache對(duì)象(也就是內(nèi)存緩存類,我們先不給出具體實(shí)現(xiàn),講解完ImageLoader源碼之后,我會(huì)提供一個(gè)利用LRU算法的ImageCache實(shí)現(xiàn)類)
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() { @Override public void putBitmap(String url, Bitmap bitmap) {} @Override public Bitmap getBitmap(String url) { return null; } });
•獲取一個(gè)ImageListener對(duì)象.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
•調(diào)用ImageLoader的get方法加載網(wǎng)絡(luò)圖片.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
有了ImageLoader的使用方法,我們結(jié)合使用方法來看一下ImageLoader的源碼:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * 關(guān)聯(lián)用來調(diào)用ImageLoader的RequestQueue. */ private final RequestQueue mRequestQueue; /** 圖片內(nèi)存緩存接口實(shí)現(xiàn)類. */ private final ImageCache mCache; /** 存儲(chǔ)同一時(shí)間執(zhí)行的相同CacheKey的BatchedImageRequest集合. */ private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<String, BatchedImageRequest>(); private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>(); /** 獲取主線程的Handler. */ private final Handler mHandler = new Handler(Looper.getMainLooper()); private Runnable mRunnable; /** 定義圖片K1緩存接口,即將圖片的內(nèi)存緩存工作交給用戶來實(shí)現(xiàn). */ public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); } /** 構(gòu)造一個(gè)ImageLoader. */ public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; } /** 構(gòu)造網(wǎng)絡(luò)圖片請(qǐng)求成功和失敗的回調(diào)接口. */ public static ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId) { return new ImageListener() { @Override public void onResponse(ImageContainer response, boolean isImmediate) { if (response.getBitmap() != null) { view.setImageBitmap(response.getBitmap()); } else if (defaultImageResId != 0) { view.setImageResource(defaultImageResId); } } @Override public void onErrorResponse(VolleyError error) { if (errorImageResId != 0) { view.setImageResource(errorImageResId); } } }; } public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { // 判斷當(dāng)前方法是否在UI線程中執(zhí)行.如果不是,則拋出異常. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // 從L1級(jí)緩存中根據(jù)key獲取對(duì)應(yīng)的Bitmap. Bitmap cacheBitmap = mCache.getBitmap(cacheKey); if (cacheBitmap != null) { // L1緩存命中,通過緩存命中的Bitmap構(gòu)造ImageContainer,并調(diào)用imageListener的響應(yīng)成功接口. ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null); // 注意:因?yàn)槟壳笆窃赨I線程中,因此這里是調(diào)用onResponse方法,并非回調(diào). imageListener.onResponse(container, true); return container; } ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // L1緩存命中失敗,則先需要對(duì)ImageView設(shè)置默認(rèn)圖片.然后通過子線程拉取網(wǎng)絡(luò)圖片,進(jìn)行顯示. imageListener.onResponse(imageContainer, true); // 檢查cacheKey對(duì)應(yīng)的ImageRequest請(qǐng)求是否正在運(yùn)行. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // 相同的ImageRequest正在運(yùn)行,不需要同時(shí)運(yùn)行相同的ImageRequest. // 只需要將其對(duì)應(yīng)的ImageContainer加入到BatchedImageRequest的mContainers集合中. // 當(dāng)正在執(zhí)行的ImageRequest結(jié)束后,會(huì)查看當(dāng)前有多少正在阻塞的ImageRequest, // 然后對(duì)其mContainers集合進(jìn)行回調(diào). request.addContainer(imageContainer); return imageContainer; } // L1緩存沒命中,還是需要構(gòu)造ImageRequest,通過RequestQueue的調(diào)度來獲取網(wǎng)絡(luò)圖片 // 獲取方法可能是:L2緩存(ps:Disk緩存)或者HTTP網(wǎng)絡(luò)請(qǐng)求. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; } /** 構(gòu)造L1緩存的key值. */ private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) { return new StringBuilder(url.length() + 12).append("#W").append(maxWidth) .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url) .toString(); } public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); } private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { throwIfNotOnMainThread(); String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); return mCache.getBitmap(cacheKey) != null; } /** 當(dāng)L1緩存沒有命中時(shí),構(gòu)造ImageRequest,通過ImageRequest和RequestQueue獲取圖片. */ protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType, final String cacheKey) { return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); } /** 圖片請(qǐng)求失敗回調(diào).運(yùn)行在UI線程中. */ private void onGetImageError(String cacheKey, VolleyError error) { BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.setError(error); batchResponse(cacheKey, request); } } /** 圖片請(qǐng)求成功回調(diào).運(yùn)行在UI線程中. */ protected void onGetImageSuccess(String cacheKey, Bitmap response) { // 增加L1緩存的鍵值對(duì). mCache.putBitmap(cacheKey, response); // 同一時(shí)間內(nèi)最初的ImageRequest執(zhí)行成功后,回調(diào)這段時(shí)間阻塞的相同ImageRequest對(duì)應(yīng)的成功回調(diào)接口. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; // 將阻塞的ImageRequest進(jìn)行結(jié)果分發(fā). batchResponse(cacheKey, request); } } private void batchResponse(String cacheKey, BatchedImageRequest request) { mBatchedResponses.put(cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : mBatchedResponses.values()) { for (ImageContainer container : bir.mContainers) { if (container.mListener == null) { continue; } if (bir.getError() == null) { container.mBitmap = bir.mResponseBitmap; container.mListener.onResponse(container, false); } else { container.mListener.onErrorResponse(bir.getError()); } } } mBatchedResponses.clear(); mRunnable = null; } }; // Post the runnable mHandler.postDelayed(mRunnable, 100); } } private void throwIfNotOnMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ImageLoader must be invoked from the main thread."); } } /** 抽象出請(qǐng)求成功和失敗的回調(diào)接口.默認(rèn)可以使用Volley提供的ImageListener. */ public interface ImageListener extends Response.ErrorListener { void onResponse(ImageContainer response, boolean isImmediate); } /** 網(wǎng)絡(luò)圖片請(qǐng)求的承載對(duì)象. */ public class ImageContainer { /** ImageView需要加載的Bitmap. */ private Bitmap mBitmap; /** L1緩存的key */ private final String mCacheKey; /** ImageRequest請(qǐng)求的url. */ private final String mRequestUrl; /** 圖片請(qǐng)求成功或失敗的回調(diào)接口類. */ private final ImageListener mListener; public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) { mBitmap = bitmap; mRequestUrl = requestUrl; mCacheKey = cacheKey; mListener = listener; } public void cancelRequest() { if (mListener == null) { return; } BatchedImageRequest request = mInFlightRequests.get(mCacheKey); if (request != null) { boolean canceled = request.removeContainerAndCancelIfNecessary(this); if (canceled) { mInFlightRequests.remove(mCacheKey); } } else { request = mBatchedResponses.get(mCacheKey); if (request != null) { request.removeContainerAndCancelIfNecessary(this); if (request.mContainers.size() == 0) { mBatchedResponses.remove(mCacheKey); } } } } public Bitmap getBitmap() { return mBitmap; } public String getRequestUrl() { return mRequestUrl; } } /** * CacheKey相同的ImageRequest請(qǐng)求抽象類. * 判定兩個(gè)ImageRequest相同包括: * 1. url相同. * 2. maxWidth和maxHeight相同. * 3. 顯示的scaleType相同. * 同一時(shí)間可能有多個(gè)相同CacheKey的ImageRequest請(qǐng)求,由于需要返回的Bitmap都一樣,所以用BatchedImageRequest * 來實(shí)現(xiàn)該功能.同一時(shí)間相同CacheKey的ImageRequest只能有一個(gè). * 為什么不使用RequestQueue的mWaitingRequestQueue來實(shí)現(xiàn)該功能? * 答:是因?yàn)閮H靠URL是沒法判斷兩個(gè)ImageRequest相等的. */ private class BatchedImageRequest { /** 對(duì)應(yīng)的ImageRequest請(qǐng)求. */ private final Request<?> mRequest; /** 請(qǐng)求結(jié)果的Bitmap對(duì)象. */ private Bitmap mResponseBitmap; /** ImageRequest的錯(cuò)誤. */ private VolleyError mError; /** 所有相同ImageRequest請(qǐng)求結(jié)果的封裝集合. */ private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>(); public BatchedImageRequest(Request<?> request, ImageContainer container) { mRequest = request; mContainers.add(container); } public VolleyError getError() { return mError; } public void setError(VolleyError error) { mError = error; } public void addContainer(ImageContainer container) { mContainers.add(container); } public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { mContainers.remove(container); if (mContainers.size() == 0) { mRequest.cancel(); return true; } return false; } } }
重大疑問
個(gè)人對(duì)Imageloader的源碼有兩個(gè)重大疑問?
•batchResponse方法的實(shí)現(xiàn).
我很奇怪,為什么ImageLoader類里面要有一個(gè)HashMap來保存BatchedImageRequest集合呢?
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
畢竟batchResponse是在特定的ImageRequest執(zhí)行成功的回調(diào)中被調(diào)用的,調(diào)用代碼如下:
protected void onGetImageSuccess(String cacheKey, Bitmap response) { // 增加L1緩存的鍵值對(duì). mCache.putBitmap(cacheKey, response); // 同一時(shí)間內(nèi)最初的ImageRequest執(zhí)行成功后,回調(diào)這段時(shí)間阻塞的相同ImageRequest對(duì)應(yīng)的成功回調(diào)接口. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; // 將阻塞的ImageRequest進(jìn)行結(jié)果分發(fā). batchResponse(cacheKey, request); } }
從上述代碼可以看出,ImageRequest請(qǐng)求成功后,已經(jīng)從mInFlightRequests中獲取了對(duì)應(yīng)的BatchedImageRequest對(duì)象.而同一時(shí)間被阻塞的相同的ImageRequest對(duì)應(yīng)的ImageContainer都在BatchedImageRequest的mContainers集合中.
那我認(rèn)為,batchResponse方法只需要遍歷對(duì)應(yīng)BatchedImageRequest的mContainers集合即可.
但是,ImageLoader源碼中,我認(rèn)為多余的構(gòu)造了一個(gè)HashMap對(duì)象mBatchedResponses來保存BatchedImageRequest集合,然后在batchResponse方法中又對(duì)集合進(jìn)行兩層for循環(huán)各種遍歷,實(shí)在是非常詭異,求指導(dǎo).
詭異代碼如下:
private void batchResponse(String cacheKey, BatchedImageRequest request) { mBatchedResponses.put(cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : mBatchedResponses.values()) { for (ImageContainer container : bir.mContainers) { if (container.mListener == null) { continue; } if (bir.getError() == null) { container.mBitmap = bir.mResponseBitmap; container.mListener.onResponse(container, false); } else { container.mListener.onErrorResponse(bir.getError()); } } } mBatchedResponses.clear(); mRunnable = null; } }; // Post the runnable mHandler.postDelayed(mRunnable, 100); } }
我認(rèn)為的代碼實(shí)現(xiàn)應(yīng)該是:
private void batchResponse(String cacheKey, BatchedImageRequest request) { if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (ImageContainer container : request.mContainers) { if (container.mListener == null) { continue; } if (request.getError() == null) { container.mBitmap = request.mResponseBitmap; container.mListener.onResponse(container, false); } else { container.mListener.onErrorResponse(request.getError()); } } mRunnable = null; } }; // Post the runnable mHandler.postDelayed(mRunnable, 100); } }
•使用ImageLoader默認(rèn)提供的ImageListener,我認(rèn)為存在一個(gè)缺陷,即圖片閃現(xiàn)問題.當(dāng)為L(zhǎng)istView的item設(shè)置圖片時(shí),需要增加TAG判斷.因?yàn)閷?duì)應(yīng)的ImageView可能已經(jīng)被回收利用了.
自定義L1緩存類
首先說明一下,所謂的L1和L2緩存分別指的是內(nèi)存緩存和硬盤緩存.
實(shí)現(xiàn)L1緩存,我們可以使用Android提供的Lru緩存類,示例代碼如下:
import android.graphics.Bitmap; import android.support.v4.util.LruCache; /** Lru算法的L1緩存實(shí)現(xiàn)類. */ @SuppressWarnings("unused") public class ImageLruCache implements ImageLoader.ImageCache { private LruCache<String, Bitmap> mLruCache; public ImageLruCache() { this((int) Runtime.getRuntime().maxMemory() / 8); } public ImageLruCache(final int cacheSize) { createLruCache(cacheSize); } private void createLruCache(final int cacheSize) { mLruCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; } @Override public Bitmap getBitmap(String url) { return mLruCache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { mLruCache.put(url, bitmap); } }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android 中Volley二次封裝并實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求緩存
- Android Volley框架全面解析
- Android Volley框架使用方法詳解
- Android中Volley框架進(jìn)行請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)的使用
- Android開發(fā)中使用Volley庫發(fā)送HTTP請(qǐng)求的實(shí)例教程
- Android的HTTP操作庫Volley的基本使用教程
- Android的HTTP類庫Volley入門學(xué)習(xí)教程
- Android Volley框架使用源碼分享
- Android 開發(fā)中Volley詳解及實(shí)例
- Android中volley封裝實(shí)踐記錄
相關(guān)文章
Android使用TabLayout+Fragment實(shí)現(xiàn)頂部選項(xiàng)卡
本文通過實(shí)例代碼給大家介紹了Android使用TabLayout+Fragment實(shí)現(xiàn)頂部選項(xiàng)卡功能,包括TabLyout的使用,感興趣的朋友參考下本文吧2017-05-05android的消息處理機(jī)制(圖文+源碼分析)—Looper/Handler/Message
這篇文章寫的非常好,深入淺出;android的消息處理機(jī)制(圖+源碼分析)—Looper,Handler,Message是一位大三學(xué)生自己剖析的心得,感興趣的朋友可以了解下哦,希望對(duì)你有所幫助2013-01-01Android利用ViewPager實(shí)現(xiàn)帶小圓球的圖片滑動(dòng)
這篇文章主要為大家詳細(xì)介紹了Android利用ViewPager實(shí)現(xiàn)帶小圓球的圖片滑動(dòng),并且只有第一次安裝app時(shí)才出現(xiàn)歡迎界面具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11Android Handler 原理分析及實(shí)例代碼
這篇文章主要介紹了Android Handler 原理分析及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02Android tabLayout+recyclerView實(shí)現(xiàn)錨點(diǎn)定位的示例
這篇文章主要介紹了Android tabLayout+recyclerView實(shí)現(xiàn)錨點(diǎn)定位的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08Android中ProgressBar用法簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android中ProgressBar用法,以簡(jiǎn)單實(shí)例形式分析了Android中ProgressBar進(jìn)度條控件的功能與布局相關(guān)技巧,需要的朋友可以參考下2016-01-01Android View進(jìn)行手勢(shì)識(shí)別詳解
本文主要介紹 Android View進(jìn)行手勢(shì)識(shí)別,這里整理了相關(guān)資料和簡(jiǎn)單示例,有興趣的小伙伴可以參考下2016-08-08