volley系列之流程簡析(二)+絕妙的緩存

上一篇說了Volley的請求流程,可是沒有說請求到response後怎麼處理,這篇文章就來詳細的說一說。讓咱們回憶一下,不論是在CacheDispatcher仍是NetworkDispatcher中只要得到response,就經過這種方式傳遞出去java

mDelivery.postResponse(request, response);複製代碼

讓咱們探進去看看都發生了什麼?算法

@Override
    public void postResponse(Request<?> request, Response<?> response) {
        postResponse(request, response, null);
    }

    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }複製代碼

前一個函數是直接調用第二個函數,方法中前兩句都是作標記,重點看最後一行代碼,首先mResponsePoster是個啥東西呀,緩存

mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };複製代碼

Executor僅僅是一個接口,它只有一個execute方法,你們也看見了,須要一個參數Runnable。也就是說他其實就是一個包裝,而後放進handler執行,那麼這個handler是哪一個handler?bash

public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }複製代碼

他實際上是在RequestQueue的構造函數裏面進行初始化的。想一想也是,得到數據之後通常都要進行UI操做,因此必須得放在主線程中操做。好了,相比你們的好奇心都沒了吧,讓咱們來看主線。剛纔說到他須要一個Runnable,裏面傳的是ResponseDeliveryRunnable,讓咱們跟進去在看,網絡

public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
            mRequest = request;
            mResponse = response;
            mRunnable = runnable;
        }
        public void run() {
            // If this request has canceled, finish it and don't deliver. if (mRequest.isCanceled()) { mRequest.finish("canceled-at-delivery"); return; } // Deliver a normal response or error, depending. if (mResponse.isSuccess()) { mRequest.deliverResponse(mResponse.result); } else { mRequest.deliverError(mResponse.error); } // If this is an intermediate response, add a marker, otherwise we're done
            // and the request can be finished.
            if (mResponse.intermediate) {
                mRequest.addMarker("intermediate-response");
            } else {
                mRequest.finish("done");
            }

            // If we have been provided a post-delivery runnable, run it.
            if (mRunnable != null) {
                mRunnable.run();
            }
       }
    }複製代碼

直接看run方法,前面都是對request進行判斷,若是取消的話就直接結束,而後無論成功或者失敗,都經過request來發送這個response,探進去看看request這個方法:框架

@Override
    protected void deliverResponse(String response) {
        if (mListener != null) {
            mListener.onResponse(response);
        }
    }複製代碼
public StringRequest(int method, String url, Listener<String> listener,
            ErrorListener errorListener) {
        super(method, url, errorListener);
        mListener = listener;
    }複製代碼

兩個結合能夠看出直接把response傳遞給Listener listener,它就是Respnose中定義的接口,是否是有點熟悉,其實就是咱們初始化request定義的兩個監聽器中的其中一個,另外一個同理,就不貼出來了。原來最後將response的成功或者失敗都交給咱們處理,聯繫上邊的知道咱們的處理方法都被放在了主線程的handler,因此能夠放心進行UI操做。這下流程大致都清楚了吧。細心的同窗會發現ResponseDeliveryRunnable中還能夠傳遞一個runnable,這個是怎麼用呢,用在哪呢,其實這兒Volley只有一個地方用到了,就是在CacheDispatcher中,假若有些response的soft-TTL(response存活時間)到了,就會發送一個runnable,讓他從新進行網絡請求獲取response,假如返回的是304(就是不須要更新),就僅僅更新一下他的存活時間,什麼也不作。假如返回的是一個新的response,就會在NetworkDispatcher中從新發送給request進行再一次操做。把代碼貼出來讓大家再回顧一下。ide

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);
            .............
                    final Request<?> finalRequest = request;
                    mDelivery.postResponse(request, response,
                            new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(finalRequest);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }複製代碼

中間有一些省略,看重點就能夠了。
上篇文章說這一篇講一下緩存的精彩之處,可是想一想仍是要把Volley的流程所有要搞明白,因此就。。。下面說說緩存是怎麼精彩的,先說一部分,也是最精彩的部分,至少是我認爲的。
你們一說到緩存,就能想到二級緩存,三級緩存(其實也就是二級),lru算法等。那麼Volley中有沒有呢?網上有人說沒有lru,這兒我是不贊同的。來看看我爲何不贊同,同時但願大家有本身的判斷。
直接看緩存的類,他有一個接口Cache,讓咱們看他的子類,函數

public class DiskBasedCache implements Cache {

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

    /** Total amount of space currently used by the cache in bytes. */
    private long mTotalSize = 0;

    /** The root directory to use for the cache. */
    private final File mRootDirectory;

    /** Default maximum disk usage in bytes. */
    private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;

    /** High water mark percentage for the cache */
    private static final float HYSTERESIS_FACTOR = 0.9f;

    public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
        mRootDirectory = rootDirectory;
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    /**
     * Constructs an instance of the DiskBasedCache at the specified directory using
     * the default maximum cache size of 5MB.
     * @param rootDirectory The root directory of the cache.
     */
    public DiskBasedCache(File rootDirectory) {
        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

    /**
     * Returns the cache entry with the specified key if it exists, null otherwise.
     */
    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }

        File file = getFileForKey(key);
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
            CacheHeader.readHeader(cis); // eat header
            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
            return entry.toCacheEntry(data);
        } catch (IOException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        }  catch (NegativeArraySizeException e) {
            VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
            remove(key);
            return null;
        } finally {
            if (cis != null) {
                try {
                    cis.close();
                } catch (IOException ioe) {
                    return null;
                }
            }
        }
    }

    /**
     * Puts the entry with the specified key into the cache.
     */
    @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }複製代碼

這兒我只放了一些重點的代碼,看緩存固然是要看他的get和put方法。我先說一個java的集合--LinkedHashMap,它保證了插入的順序和讀取的順序是一致的,還內置了LRU算法,這是關鍵。好了,來看代碼:他首先有一個LinkedHashMap的成員變量mEntries,以request的url爲key,CacheHeader爲value存放在該變量中。而CacheHeader是一個輕量級的類,裏面的成員變量和方法並很少。看名字就知道,該類僅僅是存放response的head,裏面只是response的一些說明信息,並無真正的數據。還有一個mRootDirectory,這裏面纔是存放真正的數據,默認大小爲5M。oop

先看get方法,先從mEntries獲取一個CacheHeader,若是爲空就直接返回,不爲空就從文件中取出相應的數據,最後轉化成CacheEntry返回。完了,再來看put方法,首先判斷空間是否裝下傳過來的Entry,先假設能裝的下,而後就直接寫入磁盤,也就是file中。同時也寫入map中,就是這個方法putEntry(key, e);而後再說它是怎麼判斷的,直接看代碼吧post

private void pruneIfNeeded(int neededSpace) {
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
            return;
        }
        if (VolleyLog.DEBUG) {
            VolleyLog.v("Pruning old cache entries.");
        }

        long before = mTotalSize;
        int prunedFiles = 0;
        long startTime = SystemClock.elapsedRealtime();

        Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, CacheHeader> entry = iterator.next();
            CacheHeader e = entry.getValue();
            boolean deleted = getFileForKey(e.key).delete();
            if (deleted) {
                mTotalSize -= e.size;
            } else {
               VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
                       e.key, getFilenameForKey(e.key));
            }
            iterator.remove();
            prunedFiles++;

            if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
                break;
            }
        }

        if (VolleyLog.DEBUG) {
            VolleyLog.v("pruned %d files, %d bytes, %d ms",
                    prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
        }
    }複製代碼

首先看當前的大小和須要的容量的和是否比最大容量小,小的話就直接返回,假如不夠的話,從mEntries中獲取他的迭代器,而後不斷獲取CacheHeader ,而後再從CacheHeader 取得key,再從file中刪除對應的緩存,而後也從mEntries刪除。而後再看容量是否知足所須要的。不知足再不斷的循環,直到知足爲止。這兒有一個關鍵,首先它利用LinkedHashMap的內置LRU算法,而後僅僅是將緩存頭部信息添加到內存,也就是Map中,而後將數據放在磁盤裏。當添加或者刪除的時候,都會先從Map中查詢,這樣大大減小磁盤操做,同時磁盤是有容量的,當添加時候容量不夠了,會先從Map中刪除,同時將磁盤中也刪除,這樣它兩就是聯動啊,同時擁有了LRU算法和容量,真特麼精彩。好了,這篇文章也就完了。具體Volley有沒有實現lru,你們自行判斷。Volley的流程也說完了,接下來的文章會探討它的一些代碼技巧、框架結構、打log 的方式等。要是文章有什麼錯誤或者不穩妥的地方,還望你們指出來,一塊兒討論提升。歡迎閱讀!

相關文章
相關標籤/搜索