深入剖析Android的Volley庫中的圖片加載功能
一、基本使用要點(diǎn)回顧
Volley框架在請(qǐng)求網(wǎng)絡(luò)圖片方面也做了很多工作,提供了好幾種方法.本文介紹使用ImageLoader來進(jìn)行網(wǎng)絡(luò)圖片的加載.
ImageLoader的內(nèi)部使用ImageRequest來實(shí)現(xiàn),它的構(gòu)造器可以傳入一個(gè)ImageCache緩存形參,實(shí)現(xiàn)了圖片緩存的功能,同時(shí)還可以過濾重復(fù)鏈接,避免重復(fù)發(fā)送請(qǐng)求。
下面是ImageLoader加載圖片的實(shí)現(xiàn)方法:
public void displayImg(View view){
ImageView imageView = (ImageView)this.findViewById(R.id.image_view);
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
ImageListener listener = ImageLoader.getImageListener(imageView,R.drawable.default_image, R.drawable.default_image);
imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener);
//指定圖片允許的最大寬度和高度
//imageLoader.get("http://developer.android.com/images/home/aw_dac.png",listener, 200, 200);
}
使用ImageLoader.getImageListener()方法創(chuàng)建一個(gè)ImageListener實(shí)例后,在imageLoader.get()方法中加入此監(jiān)聽器和圖片的url,即可加載網(wǎng)絡(luò)圖片.
下面是使用LruCache實(shí)現(xiàn)的緩存類
public class BitmapCache implements ImageCache {
private LruCache<String, Bitmap> cache;
public BitmapCache() {
cache = new LruCache<String, Bitmap>(8 * 1024 * 1024) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return cache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
cache.put(url, bitmap);
}
}
最后,別忘記在AndroidManifest.xml文件中加入訪問網(wǎng)絡(luò)的權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>
二、源碼分析
(一) 初始化Volley請(qǐng)求隊(duì)列
mReqQueue = Volley.newRequestQueue(mCtx);
主要就是這一行了:
#Volley
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
public static RequestQueue newRequestQueue(Context context, HttpStack stack)
{
return newRequestQueue(context, stack, -1);
}
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue;
if (maxDiskCacheBytes <= -1)
{
// No maximum size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
}
else
{
// Disk cache size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
}
queue.start();
return queue;
}
這里主要就是初始化HttpStack,對(duì)于HttpStack在API大于等于9的時(shí)候選擇HttpUrlConnetcion,反之則選擇HttpClient,這里我們并不關(guān)注Http相關(guān)代碼。
接下來初始化了RequestQueue,然后調(diào)用了start()方法。
接下來看RequestQueue的構(gòu)造:
public RequestQueue(Cache cache, Network network) {
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
初始化主要就是4個(gè)參數(shù):mCache、mNetwork、mDispatchers、mDelivery。第一個(gè)是硬盤緩存;第二個(gè)主要用于Http相關(guān)操作;第三個(gè)用于轉(zhuǎn)發(fā)請(qǐng)求的;第四個(gè)參數(shù)用于把結(jié)果轉(zhuǎn)發(fā)到UI線程(ps:你可以看到new Handler(Looper.getMainLooper()))。
接下來看start方法
#RequestQueue
/**
* Starts the dispatchers in this queue.
*/
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
首先是stop,確保轉(zhuǎn)發(fā)器退出,其實(shí)就是內(nèi)部的幾個(gè)線程退出,這里大家如果有興趣可以看眼源碼,參考下Volley中是怎么處理線程退出的(幾個(gè)線程都是while(true){//doSomething})。
接下來初始化CacheDispatcher,然后調(diào)用start();初始化NetworkDispatcher,然后調(diào)用start();
上面的轉(zhuǎn)發(fā)器呢,都是線程,可以看到,這里開了幾個(gè)線程在幫助我們工作,具體的源碼,我們一會(huì)在看。
好了,到這里,就完成了Volley的初始化的相關(guān)代碼,那么接下來看初始化ImageLoader相關(guān)源碼。
(二) 初始化ImageLoader
#VolleyHelper
mImageLoader = new ImageLoader(mReqQueue, new ImageCache()
{
private final LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>(
(int) (Runtime.getRuntime().maxMemory() / 10))
{
@Override
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
}
};
@Override
public void putBitmap(String url, Bitmap bitmap)
{
mLruCache.put(url, bitmap);
}
@Override
public Bitmap getBitmap(String url)
{
return mLruCache.get(url);
}
});
#ImageLoader
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
很簡單,就是根據(jù)我們初始化的RequestQueue和LruCache初始化了一個(gè)ImageLoader。
(三) 加載圖片
我們?cè)诩虞d圖片時(shí),調(diào)用的是:
# VolleyHelper getInstance().getImageLoader().get(url, new ImageLoader.ImageListener());
接下來看get方法:
#ImageLoader
public ImageContainer get(String requestUrl, final ImageListener listener) {
return get(requestUrl, listener, 0, 0);
}
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
}
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true);
// Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// The request is not already in flight. Send the new request to the network and
// track it.
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
可以看到get方法,首先通過throwIfNotOnMainThread()方法限制必須在UI線程調(diào)用;
然后根據(jù)傳入的參數(shù)計(jì)算cacheKey,獲取cache;
=>如果cache存在,直接將返回結(jié)果封裝為一個(gè)ImageContainer(cachedBitmap, requestUrl),然后直接回調(diào)imageListener.onResponse(container, true);我們就可以設(shè)置圖片了。
=>如果cache不存在,初始化一個(gè)ImageContainer(沒有bitmap),然后直接回調(diào),imageListener.onResponse(imageContainer, true);,這里為了讓大家在回調(diào)中判斷,然后設(shè)置默認(rèn)圖片(所以,大家在自己實(shí)現(xiàn)listener的時(shí)候,別忘了判斷resp.getBitmap()!=null);
接下來檢查該url是否早已加入了請(qǐng)求對(duì)了,如果早已加入呢,則將剛初始化的ImageContainer加入BatchedImageRequest,返回結(jié)束。
如果是一個(gè)新的請(qǐng)求,則通過makeImageRequest創(chuàng)建一個(gè)新的請(qǐng)求,然后將這個(gè)請(qǐng)求分別加入mRequestQueue和mInFlightRequests,注意mInFlightRequests中會(huì)初始化一個(gè)BatchedImageRequest,存儲(chǔ)相同的請(qǐng)求隊(duì)列。
這里注意mRequestQueue是個(gè)對(duì)象,并不是隊(duì)列數(shù)據(jù)結(jié)構(gòu),所以我們要看下add方法
#RequestQueue
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the same cache key in flight.
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
if (mWaitingRequests.containsKey(cacheKey)) {
// There is already a request in flight. Queue up.
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
} else {
// Insert 'null' queue for this cacheKey, indicating there is now a request in
// flight.
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
這里首先將請(qǐng)求加入mCurrentRequests,這個(gè)mCurrentRequests保存了所有需要處理的Request,主要為了提供cancel的入口。
如果該請(qǐng)求不應(yīng)該被緩存則直接加入mNetworkQueue,然后返回。
然后判斷該請(qǐng)求是否有相同的請(qǐng)求正在被處理,如果有則加入mWaitingRequests;如果沒有,則
加入mWaitingRequests.put(cacheKey, null)和mCacheQueue.add(request)。
ok,到這里我們就分析完成了直觀的代碼,但是你可能會(huì)覺得,那么到底是在哪里觸發(fā)的網(wǎng)絡(luò)請(qǐng)求,加載圖片呢?
那么,首先你應(yīng)該知道,我們需要加載圖片的時(shí)候,會(huì)makeImageRequest然后將這個(gè)請(qǐng)求加入到各種隊(duì)列,主要包含mCurrentRequests、mCacheQueue。
然后,還記得我們初始化RequestQueue的時(shí)候,啟動(dòng)了幾個(gè)轉(zhuǎn)發(fā)線程嗎?CacheDispatcher和NetworkDispatcher。
其實(shí),網(wǎng)絡(luò)請(qǐng)求就是在這幾個(gè)線程中真正去加載的,我們分別看一下;
(四)CacheDispatcher
看一眼構(gòu)造方法;
#CacheDispatcher
public CacheDispatcher(
BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
Cache cache, ResponseDelivery delivery) {
mCacheQueue = cacheQueue;
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
}
這是一個(gè)線程,那么主要的代碼肯定在run里面。
#CacheDispatcher
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Make a blocking call to initialize the cache.
mCache.initialize();
while (true) {
try {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request<?> request = mCacheQueue.take();
request.addMarker("cache-queue-take");
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve this item from cache.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}
// If it is completely expired, just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parse its data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
// Soft-expired cache hit. We can deliver the cached response,
// but we need to also send the request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
}
}
ok,首先要明確這個(gè)緩存指的是硬盤緩存(目錄為context.getCacheDir()/volley),內(nèi)存緩存在ImageLoader那里已經(jīng)判斷過了。
可以看到這里是個(gè)無限循環(huán),不斷的從mCacheQueue去取出請(qǐng)求,如果請(qǐng)求已經(jīng)被取消就直接結(jié)束;
接下來從緩存中獲?。?/p>
=>如果沒有取到,則加入mNetworkQueue
=>如果緩存過期,則加入mNetworkQueue
否則,就是取到了可用的緩存了;調(diào)用request.parseNetworkResponse解析從緩存中取出的data和responseHeaders;接下來判斷TTL(主要還是判斷是否過期),如果沒有過期則直接通過mDelivery.postResponse轉(zhuǎn)發(fā),然后回調(diào)到UI線程;如果ttl不合法,回調(diào)完成后,還會(huì)將該請(qǐng)求加入mNetworkQueue。
好了,這里其實(shí)就是如果拿到合法的緩存,則直接轉(zhuǎn)發(fā)到UI線程;反之,則加入到NetworkQueue.
接下來我們看NetworkDispatcher。
(五)NetworkDispatcher
與CacheDispatcher類似,依然是個(gè)線程,核心代碼依然在run中;
# NetworkDispatcher
//new NetworkDispatcher(mNetworkQueue, mNetwork,mCache, mDelivery)
public NetworkDispatcher(BlockingQueue<Request<?>> queue,
Network network, Cache cache,
ResponseDelivery delivery) {
mQueue = queue;
mNetwork = network;
mCache = cache;
mDelivery = delivery;
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
Request<?> request;
try {
// Take a request from the queue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
// Perform the network request.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// Parse the response here on the worker thread.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// Write to cache if applicable.
// TODO: Only update cache metadata instead of entire record for 304s.
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// Post the response back.
request.markDelivered();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}
看代碼前,我們首先想一下邏輯,正常情況下我們會(huì)取出請(qǐng)求,讓network去請(qǐng)求處理我們的請(qǐng)求,處理完成以后呢:加入緩存,然后轉(zhuǎn)發(fā)。
那么看下是不是:
首先取出請(qǐng)求;然后通過mNetwork.performRequest(request)處理我們的請(qǐng)求,拿到NetworkResponse;接下來,使用request去解析我們的NetworkResponse。
拿到Response以后,判斷是否應(yīng)該緩存,如果需要,則緩存。
最后mDelivery.postResponse(request, response);轉(zhuǎn)發(fā);
ok,和我們的預(yù)期差不多。
這樣的話,我們的Volley去加載圖片的核心邏輯就分析完成了,簡單總結(jié)下:
首先初始化RequestQueue,主要就是開啟幾個(gè)Dispatcher線程,線程會(huì)不斷讀取請(qǐng)求(使用的阻塞隊(duì)列,沒有消息則阻塞)
當(dāng)我們發(fā)出請(qǐng)求以后,會(huì)根據(jù)url,ImageView屬性等,構(gòu)造出一個(gè)cacheKey,然后首先從LruCache中獲?。ㄟ@個(gè)緩存我們自己構(gòu)建的,凡是實(shí)現(xiàn)ImageCache接口的都合法);如果沒有取到,則判斷是否存在硬盤緩存,這一步是從getCacheDir里面獲?。J(rèn)5M);如果沒有取到,則從網(wǎng)絡(luò)請(qǐng)求;
不過,可以發(fā)現(xiàn)的是Volley的圖片加載,并沒有LIFO這種策略;貌似對(duì)于圖片的下載,也是完整的加到內(nèi)存,然后壓縮,這么看,對(duì)于巨圖、大文件這樣的就廢了;
看起來還是蠻簡單的,不過看完以后,對(duì)于如何更好的時(shí)候該庫以及如何去設(shè)計(jì)圖片加載庫還是有很大的幫助的;
如果有興趣,大家還可以在看源碼分析的同時(shí),想想某些細(xì)節(jié)的實(shí)現(xiàn),比如:
Dispatcher都是一些無限循環(huán)的線程,可以去看看Volley如何保證其關(guān)閉的。
對(duì)于圖片壓縮的代碼,可以在ImageRequest的parseNetworkResponse里面去看看,是如何壓縮的。
so on…
最后貼個(gè)大概的流程圖,方便記憶:

- Android Glide圖片加載(加載監(jiān)聽、加載動(dòng)畫)
- Android開發(fā)中ImageLoder進(jìn)行圖片加載和緩存
- Android圖片加載緩存框架Glide
- Android圖片加載利器之Picasso基本用法
- Android 常見的圖片加載框架詳細(xì)介紹
- Android圖片加載框架Glide的基本用法介紹
- Android中RecyclerView 滑動(dòng)時(shí)圖片加載的優(yōu)化
- Android程序開發(fā)ListView+Json+異步網(wǎng)絡(luò)圖片加載+滾動(dòng)翻頁的例子(圖片能緩存,圖片不錯(cuò)亂)
- android異步加載圖片并緩存到本地實(shí)現(xiàn)方法
- Android加載大分辨率圖片到手機(jī)內(nèi)存中的實(shí)例方法
- Android ListView實(shí)現(xiàn)ImageLoader圖片加載的方法
相關(guān)文章
android手機(jī)獲取唯一標(biāo)識(shí)的方法
這篇文章主要 為大家詳細(xì)介紹了android手機(jī)獲取唯一標(biāo)識(shí)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
Kotlin基礎(chǔ)通關(guān)之字符串與數(shù)字類型
這篇文章主要介紹了Kotlin基礎(chǔ)知識(shí)中的字符串與數(shù)字類型,編程中的入門知識(shí),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Android 3D滑動(dòng)菜單完全解析 Android實(shí)現(xiàn)推拉門式的立體特效
這篇文章主要為大家詳細(xì)介紹了Android 3D滑動(dòng)菜單,Android實(shí)現(xiàn)推拉門式的立體特效,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11
Android判斷后臺(tái)服務(wù)是否開啟的兩種方法實(shí)例詳解
這篇文章主要介紹了Android判斷后臺(tái)服務(wù)是否開啟的方法的相關(guān)資料,這里提供了兩種方法及實(shí)例,需要的朋友可以參考下2017-07-07
分享一個(gè)輕量級(jí)圖片加載類 ImageLoader
這篇文章給大家分享一個(gè)輕量級(jí)圖片加載類 ImageLoader,需要的朋友可以參考下2016-08-08

