Volley 圖片加載相關源碼解析

jopen 8年前發布 | 7K 次閱讀 Android開發 移動開發

轉載請標明出處:
http://blog.csdn.net/lmj623565791/article/details/47721631
本文出自:【張鴻洋的博客】

一 概述

最近在完善圖片加載方面的代碼,于是就看看Volley的圖片加載相關源碼,取取經,順便寫篇博文作為筆記記錄下。

在使用Volley作為圖片加載庫的時候,肯定需要做以下幾件事:

  • Application中初始化Volley請求隊列
  • 初始化ImageLoader,需要設置ImageCache
  • 需要的時候,調用 getInstance().getImageLoader().get(url, new ImageLoader.ImageListener())

二 源碼分析

(一) 初始化Volley請求隊列

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,對于HttpStack在API大于等于9的時候選擇HttpUrlConnetcion,反之則選擇HttpClient,這里我們并不關注Http相關代碼。

接下來初始化了RequestQueue,然后調用了start()方法。

接下來看RequestQueue的構造:

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個參數:mCache、mNetwork、mDispatchers、mDelivery。第一個是硬盤緩存;第二個主要用于Http相關操作;第三個用于轉發請求的;第四個參數用于把結果轉發到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,確保轉發器退出,其實就是內部的幾個線程退出,這里大家如果有興趣可以看眼源碼,參考下Volley中是怎么處理線程退出的(幾個線程都是while(true){//doSomething})。

接下來初始化CacheDispatcher,然后調用start();初始化NetworkDispatcher,然后調用start();

上面的轉發器呢,都是線程,可以看到,這里開了幾個線程在幫助我們工作,具體的源碼,我們一會在看。

好了,到這里,就完成了Volley的初始化的相關代碼,那么接下來看初始化ImageLoader相關源碼。


(二) 初始化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;
    }

很簡單,就是根據我們初始化的RequestQueue和LruCache初始化了一個ImageLoader。


(三) 加載圖片

我們在加載圖片時,調用的是:

 # 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線程調用;

然后根據傳入的參數計算cacheKey,獲取cache;

=>如果cache存在,直接將返回結果封裝為一個ImageContainer(cachedBitmap, requestUrl),然后直接回調imageListener.onResponse(container, true);我們就可以設置圖片了。

=>如果cache不存在,初始化一個ImageContainer(沒有bitmap),然后直接回調,imageListener.onResponse(imageContainer, true);,這里為了讓大家在回調中判斷,然后設置默認圖片(所以,大家在自己實現listener的時候,別忘了判斷resp.getBitmap()!=null);

接下來檢查該url是否早已加入了請求對了,如果早已加入呢,則將剛初始化的ImageContainer加入BatchedImageRequest,返回結束。

如果是一個新的請求,則通過makeImageRequest創建一個新的請求,然后將這個請求分別加入mRequestQueue和mInFlightRequests,注意mInFlightRequests中會初始化一個BatchedImageRequest,存儲相同的請求隊列。

這里注意mRequestQueue是個對象,并不是隊列數據結構,所以我們要看下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;
        }
    }

這里首先將請求加入mCurrentRequests,這個mCurrentRequests保存了所有需要處理的Request,主要為了提供cancel的入口。

如果該請求不應該被緩存則直接加入mNetworkQueue,然后返回。

然后判斷該請求是否有相同的請求正在被處理,如果有則加入mWaitingRequests;如果沒有,則
加入mWaitingRequests.put(cacheKey, null)和mCacheQueue.add(request)。

ok,到這里我們就分析完成了直觀的代碼,但是你可能會覺得,那么到底是在哪里觸發的網絡請求,加載圖片呢?

那么,首先你應該知道,我們需要加載圖片的時候,會makeImageRequest然后將這個請求加入到各種隊列,主要包含mCurrentRequestsmCacheQueue

然后,還記得我們初始化RequestQueue的時候,啟動了幾個轉發線程嗎?CacheDispatcherNetworkDispatcher

其實,網絡請求就是在這幾個線程中真正去加載的,我們分別看一下;


(四)CacheDispatcher

看一眼構造方法;

#CacheDispatcher
 public CacheDispatcher(
            BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
            Cache cache, ResponseDelivery delivery) {
        mCacheQueue = cacheQueue;
        mNetworkQueue = networkQueue;
        mCache = cache;
        mDelivery = delivery;
    }

這是一個線程,那么主要的代碼肯定在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,首先要明確這個緩存指的是硬盤緩存(目錄為context.getCacheDir()/volley),內存緩存在ImageLoader那里已經判斷過了。

可以看到這里是個無限循環,不斷的從mCacheQueue去取出請求,如果請求已經被取消就直接結束;

接下來從緩存中獲取:

=>如果沒有取到,則加入mNetworkQueue

=>如果緩存過期,則加入mNetworkQueue

否則,就是取到了可用的緩存了;調用request.parseNetworkResponse解析從緩存中取出的data和responseHeaders;接下來判斷TTL(主要還是判斷是否過期),如果沒有過期則直接通過mDelivery.postResponse轉發,然后回調到UI線程;如果ttl不合法,回調完成后,還會將該請求加入mNetworkQueue。

好了,這里其實就是如果拿到合法的緩存,則直接轉發到UI線程;反之,則加入到NetworkQueue.

接下來我們看NetworkDispatcher。


(五)NetworkDispatcher

與CacheDispatcher類似,依然是個線程,核心代碼依然在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);
            }
        }
    }

看代碼前,我們首先想一下邏輯,正常情況下我們會取出請求,讓network去請求處理我們的請求,處理完成以后呢:加入緩存,然后轉發。

那么看下是不是:

首先取出請求;然后通過mNetwork.performRequest(request)處理我們的請求,拿到NetworkResponse;接下來,使用request去解析我們的NetworkResponse。

注:因為我們NetworkResponse返回都是服務器返回的一些數據,而不同的請求對應的返回結果肯定不同,比如我們的ImageRequest,解析出來是個bitmap,所以我們的解析代碼是在Request里面;這里大家想想,如果我們自定義Request,該方法是不是會重寫呢?詳情請戳Android Volley 之自定義Request

拿到Response以后,判斷是否應該緩存,如果需要,則緩存。

最后mDelivery.postResponse(request, response);轉發;

ok,和我們的預期差不多。

這樣的話,我們的Volley去加載圖片的核心邏輯就分析完成了,簡單總結下:

  • 首先初始化RequestQueue,主要就是開啟幾個Dispatcher線程,線程會不斷讀取請求(使用的阻塞隊列,沒有消息則阻塞)
  • 當我們發出請求以后,會根據url,ImageView屬性等,構造出一個cacheKey,然后首先從LruCache中獲取(這個緩存我們自己構建的,凡是實現ImageCache接口的都合法);如果沒有取到,則判斷是否存在硬盤緩存,這一步是從getCacheDir里面獲取(默認5M);如果沒有取到,則從網絡請求;

不過,可以發現的是Volley的圖片加載,并沒有LIFO這種策略;貌似對于圖片的下載,也是完整的加到內存,然后壓縮,這么看,對于巨圖、大文件這樣的就廢了;

看起來還是蠻簡單的,不過看完以后,對于如何更好的時候該庫以及如何去設計圖片加載庫還是有很大的幫助的;

如果有興趣,大家還可以在看源碼分析的同時,想想某些細節的實現,比如:

  • Dispatcher都是一些無限循環的線程,可以去看看Volley如何保證其關閉的。
  • 對于圖片壓縮的代碼,可以在ImageRequest的parseNetworkResponse里面去看看,是如何壓縮的。
  • so on…

最后貼個大概的流程圖,方便記憶:

Volley


歡迎關注我的微博:
http://weibo.com/u/3165018720


群號:463081660,歡迎入群

微信公眾號:hongyangAndroid
(歡迎關注,第一時間推送博文信息)

來自: http://blog.csdn.net//lmj623565791/article/details/47721631

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!