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

上一篇說了Volley的請求流程授段,但是沒有說請求到response后怎么處理,這篇文章就來詳細的說一說。讓我們回憶一下甫贯,不管是在CacheDispatcher還是NetworkDispatcher中只要獲得response马靠,就通過這種方式傳遞出去

mDelivery.postResponse(request, response);

讓我們探進去看看都發(fā)生了什么奄抽?

@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));
    }

前一個函數(shù)是直接調用第二個函數(shù),方法中前兩句都是做標記甩鳄,重點看最后一行代碼逞度,首先mResponsePoster是個啥東西呀,

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

Executor僅僅是一個接口妙啃,它只有一個execute方法档泽,大家也看見了,需要一個參數(shù)Runnable揖赴。也就是說他其實就是一個包裝馆匿,然后放進handler執(zhí)行,那么這個handler是哪個handler燥滑?

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

他其實是在RequestQueue的構造函數(shù)里面進行初始化的渐北。想想也是,獲得數(shù)據(jù)以后一般都要進行UI操作突倍,所以必須得放在主線程中操作腔稀。好了,相比大家的好奇心都沒了吧羽历,讓我們來看主線焊虏。剛才說到他需要一個Runnable,里面?zhèn)鞯氖荝esponseDeliveryRunnable秕磷,讓我們跟進去在看诵闭,

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來發(fā)送這個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<String> listener褥琐,它就是Respnose中定義的接口锌俱,是不是有點熟悉,其實就是我們初始化request定義的兩個監(jiān)聽器中的其中一個敌呈,另一個同理贸宏,就不貼出來了。原來最后將response的成功或者失敗都交給我們處理磕洪,聯(lián)系上邊的知道我們的處理方法都被放在了主線程的handler吭练,所以可以放心進行UI操作。這下流程大體都清楚了吧析显。細心的同學會發(fā)現(xiàn)ResponseDeliveryRunnable中還可以傳遞一個runnable鲫咽,這個是怎么用呢,用在哪呢谷异,其實這兒Volley只有一個地方用到了分尸,就是在CacheDispatcher中,假如有些response的soft-TTL(response存活時間)到了晰绎,就會發(fā)送一個runnable寓落,讓他重新進行網絡請求獲取response括丁,假如返回的是304(就是不需要更新)荞下,就僅僅更新一下他的存活時間,什么也不做史飞。假如返回的是一個新的response尖昏,就會在NetworkDispatcher中重新發(fā)送給request進行再一次操作。把代碼貼出來讓你們再回顧一下构资。

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中有沒有呢元暴?網上有人說沒有l(wèi)ru篷扩,這兒我是不贊同的。來看看我為什么不贊同茉盏,同時希望你們有自己的判斷瞻惋。
直接看緩存的類,他有一個接口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的一些說明信息,并沒有真正的數(shù)據(jù)迅箩。還有一個mRootDirectory溉愁,這里面才是存放真正的數(shù)據(jù),默認大小為5M饲趋。

先看get方法拐揭,先從mEntries獲取一個CacheHeader,如果為空就直接返回奕塑,不為空就從文件中取出相應的數(shù)據(jù)堂污,最后轉化成CacheEntry返回。完了爵川,再來看put方法敷鸦,首先判斷空間是否裝下傳過來的Entry,先假設能裝的下,然后就直接寫入磁盤扒披,也就是file中值依。同時也寫入map中,就是這個方法putEntry(key, e);然后再說它是怎么判斷的碟案,直接看代碼吧

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刪除。然后再看容量是否滿足所需要的领迈。不滿足再不斷的循環(huán)彻磁,直到滿足為止。這兒有一個關鍵狸捅,首先它利用LinkedHashMap的內置LRU算法衷蜓,然后僅僅是將緩存頭部信息添加到內存,也就是Map中尘喝,然后將數(shù)據(jù)放在磁盤里磁浇。當添加或者刪除的時候,都會先從Map中查詢朽褪,這樣大大減少磁盤操作置吓,同時磁盤是有容量的,當添加時候容量不夠了鞍匾,會先從Map中刪除交洗,同時將磁盤中也刪除,這樣它兩就是聯(lián)動啊橡淑,同時擁有了LRU算法和容量,真特么精彩咆爽。
好了梁棠,這篇文章也就完了。具體Volley有沒有實現(xiàn)lru斗埂,大家自行判斷符糊。Volley的流程也說完了,接下來的文章會探討它的一些代碼技巧呛凶、框架結構男娄、打log 的方式等。要是文章有什么錯誤或者不穩(wěn)妥的地方,還望大家指出來模闲,一起討論提高建瘫。歡迎閱讀!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末尸折,一起剝皮案震驚了整個濱河市啰脚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌实夹,老刑警劉巖橄浓,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異亮航,居然都是意外死亡荸实,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門缴淋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泪勒,“玉大人,你說我怎么就攤上這事宴猾≡泊妫” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵仇哆,是天一觀的道長沦辙。 經常有香客問我,道長讹剔,這世上最難降的妖魔是什么油讯? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮延欠,結果婚禮上陌兑,老公的妹妹穿的比我還像新娘。我一直安慰自己由捎,他們只是感情好兔综,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狞玛,像睡著了一般软驰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上心肪,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天锭亏,我揣著相機與錄音,去河邊找鬼硬鞍。 笑死慧瘤,一個胖子當著我的面吹牛戴已,可吹牛的內容都是我干的。 我是一名探鬼主播锅减,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼糖儡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了上煤?” 一聲冷哼從身側響起休玩,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎劫狠,沒想到半個月后拴疤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡独泞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年呐矾,在試婚紗的時候發(fā)現(xiàn)自己被綠了蓉坎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片那槽。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡很钓,死狀恐怖剖煌,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情崩泡,我是刑警寧澤壤靶,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布丁逝,位于F島的核電站羽资,受9級特大地震影響淘菩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜屠升,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一潮改、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腹暖,春花似錦汇在、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至以蕴,卻和暖如春糙麦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丛肮。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留魄缚,地道東北人宝与。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓焚廊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親习劫。 傳聞我的和親對象是個殘疾皇子咆瘟,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內容