Volley源碼解析

注:本文轉(zhuǎn)自http://codekk.com/open-source-project-analysis/detail/Android/grumoon/Volley%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90

1. 功能介紹

1.1. Volley

Volley 是 Google 推出的 Android 異步網(wǎng)絡(luò)請(qǐng)求框架和圖片加載框架。在 Google I/O 2013 大會(huì)上發(fā)布云芦。

名字由來(lái):a burst or emission of many things or a large amount at once

發(fā)布演講時(shí)候的配圖

從名字由來(lái)和配圖中無(wú)數(shù)急促的火箭可以看出 Volley 的特點(diǎn):特別適合數(shù)據(jù)量小,通信頻繁的網(wǎng)絡(luò)操作挠进。(個(gè)人認(rèn)為 Android 應(yīng)用中絕大多數(shù)的網(wǎng)絡(luò)操作都屬于這種類(lèi)型)颤难。

1.2 Volley 的主要特點(diǎn)

(1). 擴(kuò)展性強(qiáng)成肘。Volley 中大多是基于接口的設(shè)計(jì)豺裆,可配置性強(qiáng)吃既。

(2). 一定程度符合 Http 規(guī)范考榨,包括返回 ResponseCode(2xx、3xx态秧、4xx董虱、5xx)的處理扼鞋,請(qǐng)求頭的處理申鱼,緩存機(jī)制的支持等。并支持重試及優(yōu)先級(jí)定義云头。

(3). 默認(rèn) Android2.3 及以上基于 HttpURLConnection捐友,2.3 以下基于 HttpClient 實(shí)現(xiàn),這兩者的區(qū)別及優(yōu)劣在4.2.1 Volley中具體介紹溃槐。

(4). 提供簡(jiǎn)便的圖片加載工具匣砖。

2. 總體設(shè)計(jì)

2.1. 總體設(shè)計(jì)圖

上面是 Volley 的總體設(shè)計(jì)圖,主要是通過(guò)兩種Diapatch Thread不斷從RequestQueue中取出請(qǐng)求,根據(jù)是否已緩存調(diào)用Cache或Network這兩類(lèi)數(shù)據(jù)獲取接口之一猴鲫,從內(nèi)存緩存或是服務(wù)器取得請(qǐng)求的數(shù)據(jù)对人,然后交由ResponseDelivery去做結(jié)果分發(fā)及回調(diào)處理。

2.2. Volley 中的概念

簡(jiǎn)單介紹一些概念拂共,在詳細(xì)設(shè)計(jì)中會(huì)仔細(xì)介紹牺弄。

Volley 的調(diào)用比較簡(jiǎn)單,通過(guò) newRequestQueue(…) 函數(shù)新建并啟動(dòng)一個(gè)請(qǐng)求隊(duì)列RequestQueue后宜狐,只需要往這個(gè)RequestQueue不斷 add Request 即可势告。

Volley:Volley 對(duì)外暴露的 API,通過(guò) newRequestQueue(…) 函數(shù)新建并啟動(dòng)一個(gè)請(qǐng)求隊(duì)列RequestQueue抚恒。

Request:表示一個(gè)請(qǐng)求的抽象類(lèi)咱台。StringRequest、JsonRequest俭驮、ImageRequest都是它的子類(lèi)回溺,表示某種類(lèi)型的請(qǐng)求。

RequestQueue:表示請(qǐng)求隊(duì)列混萝,里面包含一個(gè)CacheDispatcher(用于處理走緩存請(qǐng)求的調(diào)度線程)馅而、NetworkDispatcher數(shù)組(用于處理走網(wǎng)絡(luò)請(qǐng)求的調(diào)度線程),一個(gè)ResponseDelivery(返回結(jié)果分發(fā)接口)譬圣,通過(guò) start() 函數(shù)啟動(dòng)時(shí)會(huì)啟動(dòng)CacheDispatcher和NetworkDispatchers瓮恭。

CacheDispatcher:一個(gè)線程,用于調(diào)度處理走緩存的請(qǐng)求厘熟。啟動(dòng)后會(huì)不斷從緩存請(qǐng)求隊(duì)列中取請(qǐng)求處理屯蹦,隊(duì)列為空則等待,請(qǐng)求處理結(jié)束則將結(jié)果傳遞給ResponseDelivery去執(zhí)行后續(xù)處理绳姨。當(dāng)結(jié)果未緩存過(guò)登澜、緩存失效或緩存需要刷新的情況下,該請(qǐng)求都需要重新進(jìn)入NetworkDispatcher去調(diào)度處理飘庄。

NetworkDispatcher:一個(gè)線程脑蠕,用于調(diào)度處理走網(wǎng)絡(luò)的請(qǐng)求。啟動(dòng)后會(huì)不斷從網(wǎng)絡(luò)請(qǐng)求隊(duì)列中取請(qǐng)求處理跪削,隊(duì)列為空則等待谴仙,請(qǐng)求處理結(jié)束則將結(jié)果傳遞給ResponseDelivery去執(zhí)行后續(xù)處理,并判斷結(jié)果是否要進(jìn)行緩存碾盐。

ResponseDelivery:返回結(jié)果分發(fā)接口晃跺,目前只有基于ExecutorDelivery的在入?yún)?handler 對(duì)應(yīng)線程內(nèi)進(jìn)行分發(fā)。

HttpStack:處理 Http 請(qǐng)求毫玖,返回請(qǐng)求結(jié)果掀虎。目前 Volley 中有基于 HttpURLConnection 的HurlStack和 基于 Apache HttpClient 的HttpClientStack凌盯。

Network:調(diào)用HttpStack處理請(qǐng)求,并將結(jié)果轉(zhuǎn)換為可被ResponseDelivery處理的NetworkResponse烹玉。

Cache:緩存請(qǐng)求結(jié)果驰怎,Volley 默認(rèn)使用的是基于 sdcard 的DiskBasedCache。NetworkDispatcher得到請(qǐng)求結(jié)果后判斷是否需要存儲(chǔ)在 Cache二打,CacheDispatcher會(huì)從 Cache 中取緩存結(jié)果砸西。

3. 流程圖

Volley 請(qǐng)求流程圖

上圖是 Volley 請(qǐng)求時(shí)的流程圖,在 Volley 的發(fā)布演講中給出址儒,我在這里將其用中文重新畫(huà)出芹枷。

4. 詳細(xì)設(shè)計(jì)

4.1 類(lèi)關(guān)系圖

這是 Volley 框架的主要類(lèi)關(guān)系圖

圖中紅色圈內(nèi)的部分,組成了 Volley 框架的核心莲趣,圍繞 RequestQueue 類(lèi)鸳慈,將各個(gè)功能點(diǎn)以組合的方式結(jié)合在了一起。各個(gè)功能點(diǎn)也都是以接口或者抽象類(lèi)的形式提供喧伞。

紅色圈外面的部分走芋,在 Volley 源碼中放在了toolbox包中,作為 Volley 為各個(gè)功能點(diǎn)提供的默認(rèn)的具體實(shí)現(xiàn)潘鲫。

通過(guò)類(lèi)圖我們看出翁逞, Volley 有著非常好的拓展性。通過(guò)各個(gè)功能點(diǎn)的接口溉仑,我們可以給出自定義的挖函,更符合我們需求的具體實(shí)現(xiàn)。

多用組合浊竟,少用繼承怨喘;針對(duì)接口編程,不針對(duì)具體實(shí)現(xiàn)編程振定。

優(yōu)秀框架的設(shè)計(jì)必怜,令人叫絕,受益良多后频。

4.2 核心類(lèi)功能介紹

4.2.1 Volley.java

這個(gè)和 Volley 框架同名的類(lèi)梳庆,其實(shí)是個(gè)工具類(lèi),作用是構(gòu)建一個(gè)可用于添加網(wǎng)絡(luò)請(qǐng)求的RequestQueue對(duì)象卑惜。

(1). 主要函數(shù)

Volley.java 有兩個(gè)重載的靜態(tài)方法膏执。

public static RequestQueue newRequestQueue(Context context)

public static RequestQueue newRequestQueue(Context context, HttpStack stack)

第一個(gè)方法的實(shí)現(xiàn)調(diào)用了第二個(gè)方法,傳 HttpStack 參數(shù)為 null残揉。

第二個(gè)方法中胧后,如果 HttpStatck 參數(shù)為 null,則如果系統(tǒng)在 Gingerbread 及之后(即 API Level >= 9)抱环,采用基于 HttpURLConnection 的 HurlStack壳快,如果小于 9,采用基于 HttpClient 的 HttpClientStack镇草。

if (stack == null) {

if (Build.VERSION.SDK_INT >= 9) {

stack = new HurlStack();

} else {

stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));

}

}

得到了 HttpStack,然后通過(guò)它構(gòu)造一個(gè)代表網(wǎng)絡(luò)(Network)的具體實(shí)現(xiàn)BasicNetwork眶痰。

接著構(gòu)造一個(gè)代表緩存(Cache)的基于 Disk 的具體實(shí)現(xiàn)DiskBasedCache。

最后將網(wǎng)絡(luò)(Network)對(duì)象和緩存(Cache)對(duì)象傳入構(gòu)建一個(gè) RequestQueue梯啤,啟動(dòng)這個(gè) RequestQueue竖伯,并返回。

Network network = new BasicNetwork(stack);

RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);

queue.start();

return queue;

我們平時(shí)大多采用Volly.newRequestQueue(context)的默認(rèn)實(shí)現(xiàn)因宇,構(gòu)建RequestQueue七婴。

通過(guò)源碼可以看出,我們可以拋開(kāi) Volley 工具類(lèi)構(gòu)建自定義的RequestQueue察滑,采用自定義的HttpStatck打厘,采用自定義的Network實(shí)現(xiàn),采用自定義的Cache實(shí)現(xiàn)等來(lái)構(gòu)建RequestQueue贺辰。

優(yōu)秀框架的高可拓展性的魅力來(lái)源于此啊

(2). HttpURLConnection 和 AndroidHttpClient(HttpClient 的封裝)如何選擇及原因:

在 Froyo(2.2) 之前户盯,HttpURLConnection 有個(gè)重大 Bug,調(diào)用 close() 函數(shù)會(huì)影響連接池饲化,導(dǎo)致連接復(fù)用失效莽鸭,所以在 Froyo 之前使用 HttpURLConnection 需要關(guān)閉 keepAlive。

另外在 Gingerbread(2.3) HttpURLConnection 默認(rèn)開(kāi)啟了 gzip 壓縮吃靠,提高了 HTTPS 的性能硫眨,Ice Cream Sandwich(4.0) HttpURLConnection 支持了請(qǐng)求結(jié)果緩存。

再加上 HttpURLConnection 本身 API 相對(duì)簡(jiǎn)單巢块,所以對(duì) Android 來(lái)說(shuō)捺球,在 2.3 之后建議使用 HttpURLConnection,之前建議使用 AndroidHttpClient夕冲。

(3). 關(guān)于 User Agent

通過(guò)代碼我們發(fā)現(xiàn)如果是使用 AndroidHttpClient氮兵,Volley 還會(huì)將請(qǐng)求頭中的 User-Agent 字段設(shè)置為 App 的 ${packageName}/${versionCode},如果異常則使用 "volley/0"歹鱼,不過(guò)這個(gè)獲取 User-Agent 的操作應(yīng)該放到 if else 內(nèi)部更合適泣栈。而對(duì)于 HttpURLConnection 卻沒(méi)有任何操作,為什么呢弥姻?

如果用Fiddler 或 Charles對(duì)數(shù)據(jù)抓包我們會(huì)發(fā)現(xiàn)南片,我們會(huì)發(fā)現(xiàn) HttpURLConnection 默認(rèn)是有 User-Agent 的,類(lèi)似:

Dalvik/1.6.0 (Linux; U; Android 4.1.1; Google Nexus 4 - 4.1.1 - API 16 - 768x1280_1 Build/JRO03S)

經(jīng)常用 WebView 的同學(xué)會(huì)也許會(huì)發(fā)現(xiàn)似曾相識(shí)庭敦,是的疼进,WebView 默認(rèn)的 User-Agent 也是這個(gè)。實(shí)際在請(qǐng)求發(fā)出之前秧廉,會(huì)檢測(cè) User-Agent 是否為空伞广,如果不為空拣帽,則加上系統(tǒng)默認(rèn) User-Agent。在 Android 2.1 之后嚼锄,我們可以通過(guò)

String userAgent = System.getProperty("http.agent");

得到系統(tǒng)默認(rèn)的 User-Agent减拭,Volley 如果希望自定義 User-Agent,可在自定義 Request 中重寫(xiě) getHeaders() 函數(shù)

@Override

public Map getHeaders() throws AuthFailureError {

// self-defined user agent

Map headerMap = new HashMap();

headerMap.put("User-Agent", "android-open-project-analysis/1.0");

return headerMap;

}

4.2.2 Request.java

代表一個(gè)網(wǎng)絡(luò)請(qǐng)求的抽象類(lèi)区丑。我們通過(guò)構(gòu)建一個(gè)Request類(lèi)的非抽象子類(lèi)(StringRequest拧粪、JsonRequest、ImageRequest或自定義)對(duì)象沧侥,并將其加入到·RequestQueue·中來(lái)完成一次網(wǎng)絡(luò)請(qǐng)求操作可霎。

Volley 支持 8 種 Http 請(qǐng)求方式GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH

Request 類(lèi)中包含了請(qǐng)求 url,請(qǐng)求請(qǐng)求方式宴杀,請(qǐng)求 Header癣朗,請(qǐng)求 Body,請(qǐng)求的優(yōu)先級(jí)等信息婴氮。

因?yàn)槭浅橄箢?lèi)斯棒,子類(lèi)必須重寫(xiě)的兩個(gè)方法。

abstract protected Response parseNetworkResponse(NetworkResponse response);

子類(lèi)重寫(xiě)此方法主经,將網(wǎng)絡(luò)返回的原生字節(jié)內(nèi)容荣暮,轉(zhuǎn)換成合適的類(lèi)型。此方法會(huì)在工作線程中被調(diào)用罩驻。

abstract protected void deliverResponse(T response);

子類(lèi)重寫(xiě)此方法穗酥,將解析成合適類(lèi)型的內(nèi)容傳遞給它們的監(jiān)聽(tīng)回調(diào)。

以下兩個(gè)方法也經(jīng)常會(huì)被重寫(xiě)

public byte[] getBody()

重寫(xiě)此方法惠遏,可以構(gòu)建用于 POST砾跃、PUT、PATCH 請(qǐng)求方式的 Body 內(nèi)容节吮。

protected Map getParams()

在上面getBody函數(shù)沒(méi)有被重寫(xiě)情況下抽高,此方法的返回值會(huì)被 key、value 分別編碼后拼裝起來(lái)轉(zhuǎn)換為字節(jié)碼作為 Body 內(nèi)容透绩。

4.2.3 RequestQueue.java

Volley 框架的核心類(lèi)翘骂,將請(qǐng)求Request加入到一個(gè)運(yùn)行的RequestQueue中,來(lái)完成請(qǐng)求操作帚豪。

(1). 主要成員變量

RequestQueue 中維護(hù)了兩個(gè)基于優(yōu)先級(jí)的 Request 隊(duì)列碳竟,緩存請(qǐng)求隊(duì)列和網(wǎng)絡(luò)請(qǐng)求隊(duì)列。

放在緩存請(qǐng)求隊(duì)列中的 Request狸臣,將通過(guò)緩存獲取數(shù)據(jù)莹桅;放在網(wǎng)絡(luò)請(qǐng)求隊(duì)列中的 Request,將通過(guò)網(wǎng)絡(luò)獲取數(shù)據(jù)烛亦。

private final PriorityBlockingQueue> mCacheQueue = new PriorityBlockingQueue>();

private final PriorityBlockingQueue> mNetworkQueue = new PriorityBlockingQueue>();

維護(hù)了一個(gè)正在進(jìn)行中诈泼,尚未完成的請(qǐng)求集合懂拾。

private final Set> mCurrentRequests = new HashSet>();

維護(hù)了一個(gè)等待請(qǐng)求的集合,如果一個(gè)請(qǐng)求正在被處理并且可以被緩存厂汗,后續(xù)的相同 url 的請(qǐng)求委粉,將進(jìn)入此等待隊(duì)列呜师。

private final Map>> mWaitingRequests = new HashMap>>();

(2). 啟動(dòng)隊(duì)列

創(chuàng)建出 RequestQueue 以后娶桦,調(diào)用 start 方法,啟動(dòng)隊(duì)列汁汗。

/**

* 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();

}

}

start 方法中衷畦,開(kāi)啟一個(gè)緩存調(diào)度線程CacheDispatcher和 n 個(gè)網(wǎng)絡(luò)調(diào)度線程N(yùn)etworkDispatcher,這里 n 默認(rèn)為4知牌,存在優(yōu)化的余地祈争,比如可以根據(jù) CPU 核數(shù)以及網(wǎng)絡(luò)類(lèi)型計(jì)算更合適的并發(fā)數(shù)。

緩存調(diào)度線程不斷的從緩存請(qǐng)求隊(duì)列中取出 Request 去處理角寸,網(wǎng)絡(luò)調(diào)度線程不斷的從網(wǎng)絡(luò)請(qǐng)求隊(duì)列中取出 Request 去處理菩混。

(3). 加入請(qǐng)求

public Request add(Request request);

流程圖如下:

(4). 請(qǐng)求完成

void finish(Request request)

Request 請(qǐng)求結(jié)束

(1). 首先從正在進(jìn)行中請(qǐng)求集合mCurrentRequests中移除該請(qǐng)求。

(2). 然后查找請(qǐng)求等待集合mWaitingRequests中是否存在等待的請(qǐng)求扁藕,如果存在沮峡,則將等待隊(duì)列移除,并將等待隊(duì)列所有的請(qǐng)求添加到緩存請(qǐng)求隊(duì)列中亿柑,讓緩存請(qǐng)求處理線程CacheDispatcher自動(dòng)處理邢疙。

(5). 請(qǐng)求取消

public void cancelAll(RequestFilter filter)

public void cancelAll(final Object tag)

取消當(dāng)前請(qǐng)求集合中所有符合條件的請(qǐng)求。

filter 參數(shù)表示可以按照自定義的過(guò)濾器過(guò)濾需要取消的請(qǐng)求望薄。

tag 表示按照Request.setTag設(shè)置好的 tag 取消請(qǐng)求疟游,比如同屬于某個(gè) Activity 的。

4.2.4 CacheDispatcher.java

一個(gè)線程痕支,用于調(diào)度處理走緩存的請(qǐng)求颁虐。啟動(dòng)后會(huì)不斷從緩存請(qǐng)求隊(duì)列中取請(qǐng)求處理,隊(duì)列為空則等待卧须,請(qǐng)求處理結(jié)束則將結(jié)果傳遞給ResponseDelivery去執(zhí)行后續(xù)處理另绩。當(dāng)結(jié)果未緩存過(guò)、緩存失效或緩存需要刷新的情況下故慈,該請(qǐng)求都需要重新進(jìn)入NetworkDispatcher去調(diào)度處理板熊。

(1). 成員變量

BlockingQueue> mCacheQueue緩存請(qǐng)求隊(duì)列

BlockingQueue> mNetworkQueue網(wǎng)絡(luò)請(qǐng)求隊(duì)列

Cache mCache緩存類(lèi),代表了一個(gè)可以獲取請(qǐng)求結(jié)果察绷,存儲(chǔ)請(qǐng)求結(jié)果的緩存

ResponseDelivery mDelivery請(qǐng)求結(jié)果傳遞類(lèi)

(2). 處理流程圖

4.2.5 NetworkDispatcher.java

一個(gè)線程干签,用于調(diào)度處理走網(wǎng)絡(luò)的請(qǐng)求。啟動(dòng)后會(huì)不斷從網(wǎng)絡(luò)請(qǐng)求隊(duì)列中取請(qǐng)求處理拆撼,隊(duì)列為空則等待容劳,請(qǐng)求處理結(jié)束則將結(jié)果傳遞給 ResponseDelivery 去執(zhí)行后續(xù)處理喘沿,并判斷結(jié)果是否要進(jìn)行緩存。

(1). 成員變量

BlockingQueue> mQueue網(wǎng)絡(luò)請(qǐng)求隊(duì)列

Network mNetwork網(wǎng)絡(luò)類(lèi)竭贩,代表了一個(gè)可以執(zhí)行請(qǐng)求的網(wǎng)絡(luò)

Cache mCache緩存類(lèi)蚜印,代表了一個(gè)可以獲取請(qǐng)求結(jié)果,存儲(chǔ)請(qǐng)求結(jié)果的緩存

ResponseDelivery mDelivery請(qǐng)求結(jié)果傳遞類(lèi)留量,可以傳遞請(qǐng)求的結(jié)果或者錯(cuò)誤到調(diào)用者

(2). 處理流程圖

4.2.6 Cache.java

緩存接口窄赋,代表了一個(gè)可以獲取請(qǐng)求結(jié)果,存儲(chǔ)請(qǐng)求結(jié)果的緩存楼熄。

(1). 主要方法:

public Entry get(String key);通過(guò) key 獲取請(qǐng)求的緩存實(shí)體

public void put(String key, Entry entry);存入一個(gè)請(qǐng)求的緩存實(shí)體

public void remove(String key);移除指定的緩存實(shí)體

public void clear();清空緩存

(2). 代表緩存實(shí)體的內(nèi)部類(lèi) Entry

成員變量和方法

byte[] data請(qǐng)求返回的數(shù)據(jù)(Body 實(shí)體)

String etagHttp 響應(yīng)首部中用于緩存新鮮度驗(yàn)證的 ETag

long serverDateHttp 響應(yīng)首部中的響應(yīng)產(chǎn)生時(shí)間

long ttl緩存的過(guò)期時(shí)間

long softTtl緩存的新鮮時(shí)間

Map responseHeaders響應(yīng)的 Headers

boolean isExpired()判斷緩存是否過(guò)期忆绰,過(guò)期緩存不能繼續(xù)使用

boolean refreshNeeded()判斷緩存是否新鮮,不新鮮的緩存需要發(fā)到服務(wù)端做新鮮度的檢測(cè)

4.2.7 DiskBasedCache.java

繼承 Cache 類(lèi)可岂,基于 Disk 的緩存實(shí)現(xiàn)類(lèi)错敢。

(1). 主要方法:

public synchronized void initialize()初始化,掃描緩存目錄得到所有緩存數(shù)據(jù)摘要信息放入內(nèi)存缕粹。

public synchronized Entry get(String key)從緩存中得到數(shù)據(jù)稚茅。先從摘要信息中得到摘要信息,然后讀取緩存數(shù)據(jù)文件得到內(nèi)容平斩。

public synchronized void put(String key, Entry entry)將數(shù)據(jù)存入緩存內(nèi)亚享。先檢查緩存是否會(huì)滿,會(huì)則先刪除緩存中部分?jǐn)?shù)據(jù)双戳,然后再新建緩存文件虹蒋。

private void pruneIfNeeded(int neededSpace)檢查是否能再分配 neededSpace 字節(jié)的空間,如果不能則刪除緩存中部分?jǐn)?shù)據(jù)飒货。

public synchronized void clear()清空緩存魄衅。public synchronized void remove(String key)刪除緩存中某個(gè)元素。

(2). CacheHeader 類(lèi)

CacheHeader 是緩存文件摘要信息塘辅,存儲(chǔ)在緩存文件的頭部晃虫,與上面的Cache.Entry相似。

4.2.8 NoCache.java

繼承 Cache 類(lèi)扣墩,不做任何操作的緩存實(shí)現(xiàn)類(lèi)哲银,可將它作為構(gòu)建RequestQueue的參數(shù)以實(shí)現(xiàn)一個(gè)不帶緩存的請(qǐng)求隊(duì)列。

4.2.9 Network.java

代表網(wǎng)絡(luò)的接口呻惕,處理網(wǎng)絡(luò)請(qǐng)求荆责。

唯一的方法,用于執(zhí)行特定請(qǐng)求亚脆。

public NetworkResponse performRequest(Request request) throws VolleyError;

4.2.10 NetworkResponse.java

Network中方法 performRequest 的返回值做院,Request的 parseNetworkResponse(…) 方法入?yún)ⅲ?Volley 中用于內(nèi)部 Response 轉(zhuǎn)換的一級(jí)。

封裝了網(wǎng)絡(luò)請(qǐng)求響應(yīng)的 StatusCode键耕,Headers 和 Body 等寺滚。

(1). 成員變量

int statusCodeHttp 響應(yīng)狀態(tài)碼

byte[] dataBody 數(shù)據(jù)

Map headers響應(yīng) Headers

boolean notModified表示是否為 304 響應(yīng)

long networkTimeMs請(qǐng)求耗時(shí)

(2). Volley 的內(nèi)部 Response 轉(zhuǎn)換流程圖

從上到下表示從得到數(shù)據(jù)后一步步的處理,箭頭旁的注釋表示該步處理后的實(shí)體類(lèi)屈雄。

4.2.11 BasicNetwork.java

實(shí)現(xiàn) Network村视,Volley 中默認(rèn)的網(wǎng)絡(luò)接口實(shí)現(xiàn)類(lèi)。調(diào)用HttpStack處理請(qǐng)求酒奶,并將結(jié)果轉(zhuǎn)換為可被ResponseDelivery處理的NetworkResponse蚁孔。

主要實(shí)現(xiàn)了以下功能:

(1). 利用 HttpStack 執(zhí)行網(wǎng)絡(luò)請(qǐng)求。

(2). 如果 Request 中帶有實(shí)體信息讥蟆,如 Etag,Last-Modify 等勒虾,則進(jìn)行緩存新鮮度的驗(yàn)證纺阔,并處理 304(Not Modify)響應(yīng)瘸彤。

(3). 如果發(fā)生超時(shí),認(rèn)證失敗等錯(cuò)誤笛钝,進(jìn)行重試操作质况,直到成功、拋出異常(不滿足重試策略等)結(jié)束玻靡。

4.2.12 HttpStack.java

用于處理 Http 請(qǐng)求结榄,返回請(qǐng)求結(jié)果的接口。目前 Volley 中的實(shí)現(xiàn)有基于 HttpURLConnection 的 HurlStack 和 基于 Apache HttpClient 的 HttpClientStack囤捻。

唯一方法臼朗,執(zhí)行請(qǐng)求

public HttpResponse performRequest(Request request, Map additionalHeaders)

throws IOException, AuthFailureError;

執(zhí)行 Request 代表的請(qǐng)求,第二個(gè)參數(shù)表示發(fā)起請(qǐng)求之前蝎土,添加額外的請(qǐng)求 Headers视哑。

4.2.13 HttpClientStack.java

實(shí)現(xiàn) HttpStack 接口,利用 Apache 的 HttpClient 進(jìn)行各種請(qǐng)求方式的請(qǐng)求誊涯。

基本就是 org.apache.http 包下面相關(guān)類(lèi)的常見(jiàn)用法挡毅,不做詳解,不過(guò)與下面 HttpURLConnection 做下對(duì)比就能發(fā)現(xiàn) HttpURLConnection 的 API 相對(duì)簡(jiǎn)單的多暴构。

4.2.14 HurlStack.java

實(shí)現(xiàn) HttpStack 接口跪呈,利用 Java 的 HttpURLConnection 進(jìn)行各種請(qǐng)求方式的請(qǐng)求。

4.2.15 Response.java

封裝了經(jīng)過(guò)解析后的數(shù)據(jù)取逾,用于傳輸耗绿。并且有兩個(gè)內(nèi)部接口 Listener 和 ErrorListener 分別可表示請(qǐng)求失敗和成功后的回調(diào)。

Response 的構(gòu)造函數(shù)被私有化砾隅,而通過(guò)兩個(gè)函數(shù)名更易懂的靜態(tài)方法構(gòu)建對(duì)象误阻。

4.2.16 ByteArrayPool.java

byte[] 的回收池,用于 byte[] 的回收再利用,減少了內(nèi)存的分配和回收堕绩。 主要通過(guò)一個(gè)元素長(zhǎng)度從小到大排序的ArrayList作為 byte[] 的緩存策幼,另有一個(gè)按使用時(shí)間先后排序的ArrayList屬性用于緩存滿時(shí)清理元素。

public synchronized void returnBuf(byte[] buf)

將用過(guò)的 byte[] 回收奴紧,根據(jù) byte[] 長(zhǎng)度按照從小到大的排序?qū)?byte[] 插入到緩存中合適位置特姐。

public synchronized byte[] getBuf(int len)

獲取長(zhǎng)度不小于 len 的 byte[],遍歷緩存黍氮,找出第一個(gè)長(zhǎng)度大于傳入?yún)?shù)len的 byte[]唐含,并返回;如果最終沒(méi)有合適的byte[]沫浆,new 一個(gè)返回捷枯。

private synchronized void trim()

當(dāng)緩存的 byte 超過(guò)預(yù)先設(shè)置的大小時(shí),按照先進(jìn)先出的順序刪除最早的 byte[]专执。

4.2.17 PoolingByteArrayOutputStream.java

繼承ByteArrayOutputStream淮捆,原始 ByteArrayOutputStream 中用于接受寫(xiě)入 bytes 的 buf,每次空間不足時(shí)便會(huì) new 更大容量的 byte[]本股,而 PoolingByteArrayOutputStream 使用了 ByteArrayPool 作為 Byte[] 緩存來(lái)減少這種操作攀痊,從而提高性能。

4.2.18 HttpHeaderParser.java

Http header 的解析工具類(lèi)拄显,在 Volley 中主要作用是用于解析 Header 從而判斷返回結(jié)果是否需要緩存苟径,如果需要返回 Header 中相關(guān)信息。

有三個(gè)方法

public static long parseDateAsEpoch(String dateStr)

解析時(shí)間躬审,將 RFC1123 的時(shí)間格式棘街,解析成 epoch 時(shí)間

public static String parseCharset(Map headers)

解析編碼集,在 Content-Type 首部中獲取編碼集承边,如果沒(méi)有找到遭殉,默認(rèn)返回 ISO-8859-1

public static Cache.Entry parseCacheHeaders(NetworkResponse response)

比較重要的方法,通過(guò)網(wǎng)絡(luò)響應(yīng)中的緩存控制 Header 和 Body 內(nèi)容炒刁,構(gòu)建緩存實(shí)體恩沽。如果 Header 的 Cache-Control 字段含有no-cache或no-store表示不緩存,返回 null翔始。

(1). 根據(jù) Date 首部罗心,獲取響應(yīng)生成時(shí)間

(2). 根據(jù) ETag 首部,獲取響應(yīng)實(shí)體標(biāo)簽

(3). 根據(jù) Cache-Control 和 Expires 首部城瞎,計(jì)算出緩存的過(guò)期時(shí)間渤闷,和緩存的新鮮度時(shí)間

兩點(diǎn)需要說(shuō)明下:

1.沒(méi)有處理Last-Modify首部,而是處理存儲(chǔ)了Date首部脖镀,并在后續(xù)的新鮮度驗(yàn)證時(shí)飒箭,使用Date來(lái)構(gòu)建If-Modified-Since。 這與 Http 1.1 的語(yǔ)義有些違背。

2.計(jì)算過(guò)期時(shí)間弦蹂,Cache-Control 首部?jī)?yōu)先于 Expires 首部肩碟。

4.2.19 RetryPolicy.java

重試策略接口

有三個(gè)方法:

public int getCurrentTimeout();

獲取當(dāng)前請(qǐng)求用時(shí)(用于Log)

public int getCurrentRetryCount();

獲取已經(jīng)重試的次數(shù)(用于Log)

public void retry(VolleyError error) throws VolleyError;

確定是否重試,參數(shù)為這次異常的具體信息凸椿。在請(qǐng)求異常時(shí)此接口會(huì)被調(diào)用削祈,可在此函數(shù)實(shí)現(xiàn)中拋出傳入的異常表示停止重試。

4.2.20 DefaultRetryPolicy.java

實(shí)現(xiàn) RetryPolicy脑漫,Volley 默認(rèn)的重試策略實(shí)現(xiàn)類(lèi)髓抑。主要通過(guò)在 retry(…) 函數(shù)中判斷重試次數(shù)是否達(dá)到上限確定是否繼續(xù)重試。

其中mCurrentTimeoutMs變量表示已經(jīng)重試次數(shù)优幸。

mBackoffMultiplier表示每次重試之前的 timeout 該乘以的因子吨拍。

mCurrentTimeoutMs變量表示當(dāng)前重試的 timeout 時(shí)間,會(huì)以mBackoffMultiplier作為因子累計(jì)前幾次重試的 timeout网杆。

4.2.21 ResponseDelivery.java

請(qǐng)求結(jié)果的傳輸接口羹饰,用于傳遞請(qǐng)求結(jié)果或者請(qǐng)求錯(cuò)誤。

有三個(gè)方法:

public void postResponse(Request request, Response response);

此方法用于傳遞請(qǐng)求結(jié)果跛璧,request和response參數(shù)分別表示請(qǐng)求信息和返回結(jié)果信息严里。

public void postResponse(Request request, Response response, Runnable runnable);

此方法用于傳遞請(qǐng)求結(jié)果,并在完成傳遞后執(zhí)行 Runnable追城。

public void postError(Request request, VolleyError error);

此方法用于傳輸請(qǐng)求錯(cuò)誤氢妈。

4.2.22 ExecutorDelivery.java

請(qǐng)求結(jié)果傳輸接口具體實(shí)現(xiàn)類(lèi)跺讯。

在 Handler 對(duì)應(yīng)線程中傳輸緩存調(diào)度線程或者網(wǎng)絡(luò)調(diào)度線程中產(chǎn)生的請(qǐng)求結(jié)果或請(qǐng)求錯(cuò)誤篮迎,會(huì)在請(qǐng)求成功的情況下調(diào)用 Request.deliverResponse(…) 函數(shù)族吻,失敗時(shí)調(diào)用 Request.deliverError(…) 函數(shù)梧乘。

4.2.23 StringRequest.java

繼承 Request 類(lèi),代表了一個(gè)返回值為 String 的請(qǐng)求晚唇。將網(wǎng)絡(luò)返回的結(jié)果數(shù)據(jù)解析為 String 類(lèi)型贞言。通過(guò)構(gòu)造函數(shù)的 listener 傳參贯溅,支持請(qǐng)求成功后的 onResponse(…) 回調(diào)冠胯。

4.2.24 JsonRequest.java

抽象類(lèi)火诸,繼承自 Request,代表了 body 為 JSON 的請(qǐng)求荠察。提供了構(gòu)建 JSON 請(qǐng)求參數(shù)的方法置蜀。

4.2.25 JsonObjectRequest.java

繼承自 JsonRequest,將網(wǎng)絡(luò)返回的結(jié)果數(shù)據(jù)解析為 JSONObject 類(lèi)型悉盆。

4.2.26 JsonArrayRequest.java

繼承自 JsonRequest盯荤,將網(wǎng)絡(luò)返回的結(jié)果數(shù)據(jù)解析為 JSONArray 類(lèi)型。

4.2.27 ImageRequest.java

繼承 Request 類(lèi)焕盟,代表了一個(gè)返回值為 Image 的請(qǐng)求秋秤。將網(wǎng)絡(luò)返回的結(jié)果數(shù)據(jù)解析為 Bitmap 類(lèi)型。

可以設(shè)置圖片的最大寬度和最大高度,并計(jì)算出合適尺寸返回灼卢。每次最多解析一張圖片防止 OOM绍哎。

4.2.28 ImageLoader.java

封裝了 ImageRequst 的方便使用的圖片加載工具類(lèi)。

1.可以設(shè)置自定義的ImageCache鞋真,可以是內(nèi)存緩存蛇摸,也可以是 Disk 緩存,將獲取的圖片緩存起來(lái)灿巧,重復(fù)利用赶袄,減少請(qǐng)求。

2.可以定義圖片請(qǐng)求過(guò)程中顯示的圖片和請(qǐng)求失敗后顯示的圖片抠藕。

3.相同請(qǐng)求(相同地址饿肺,相同大小)只發(fā)送一個(gè)盾似,可以避免重復(fù)請(qǐng)求敬辣。

// TODO

4.2.29 NetworkImageView.java

利用 ImageLoader,可以加載網(wǎng)絡(luò)圖片的 ImageView

有三個(gè)公開(kāi)的方法:

public void setDefaultImageResId(int defaultImage)

設(shè)置默認(rèn)圖片零院,加載圖片過(guò)程中顯示溉跃。

public void setErrorImageResId(int errorImage)

設(shè)置錯(cuò)誤圖片,加載圖片失敗后顯示告抄。

public void setImageUrl(String url, ImageLoader imageLoader)

設(shè)置網(wǎng)絡(luò)圖片的 Url 和 ImageLoader撰茎,將利用這個(gè) ImageLoader 去獲取網(wǎng)絡(luò)圖片。

如果有新的圖片加載請(qǐng)求打洼,會(huì)把這個(gè)ImageView上舊的加載請(qǐng)求取消龄糊。

4.2.30 ClearCacheRequest.java

用于人為清空 Http 緩存的請(qǐng)求。

添加到 RequestQueue 后能很快執(zhí)行募疮,因?yàn)閮?yōu)先級(jí)很高炫惩,為Priority.IMMEDIATE。并且清空緩存的方法mCache.clear()寫(xiě)在了isCanceled()方法體中阿浓,能最早的得到執(zhí)行他嚷。

ClearCacheRequest 的寫(xiě)法不敢茍同,目前看來(lái)唯一的好處就是可以將清空緩存操作也當(dāng)做一個(gè)請(qǐng)求芭毙。而在isCanceled()中做清空操作本身就造成了歧義筋蓖,不看源碼沒(méi)人知道在NetworkDispatcherrun 方法循環(huán)的過(guò)程中,isCanceled()這個(gè)讀操作竟然做了可能造成緩存被清空稿蹲。只能跟源碼的解釋一樣當(dāng)做一個(gè) Hack 操作扭勉。

4.2.31 Authenticator.java

身份認(rèn)證接口,用于基本認(rèn)證或者摘要認(rèn)證苛聘。這個(gè)類(lèi)是 Volley 用于和身份驗(yàn)證打通的接口涂炎,比如 OAuth忠聚,不過(guò)目前的使用不是特別廣泛和 Volley 的內(nèi)部結(jié)合也不是特別緊密。

4.2.32 AndroidAuthenticator.java

繼承 Authenticator唱捣,基于 Android AccountManager 的認(rèn)證交互實(shí)現(xiàn)類(lèi)两蟀。

4.2.33 VolleyLog.java

Volley 的 Log 工具類(lèi)。

4.2.34 VolleyError.java

Volley 中所有錯(cuò)誤異常的父類(lèi)震缭,繼承自 Exception赂毯,可通過(guò)此類(lèi)設(shè)置和獲取 NetworkResponse 或者請(qǐng)求的耗時(shí)。

4.2.35 AuthFailureError.java

繼承自 VolleyError拣宰,代表請(qǐng)求認(rèn)證失敗錯(cuò)誤党涕,如 RespondeCode 的 401 和 403。

4.2.36 NetworkError.java

繼承自 VolleyError巡社,代表網(wǎng)絡(luò)錯(cuò)誤膛堤。

4.2.37 ParseError.java

繼承自 VolleyError,代表內(nèi)容解析錯(cuò)誤晌该。

4.2.38 ServerError.java

繼承自 VolleyError肥荔,代表服務(wù)端錯(cuò)誤。

4.2.39 TimeoutError.java

繼承自 VolleyError朝群,代表請(qǐng)求超時(shí)錯(cuò)誤燕耿。

4.2.40 NoConnectionError.java

繼承自NetworkError,代表無(wú)法建立連接錯(cuò)誤姜胖。

5. 雜談

5.1 關(guān)于 Http 緩存

Volley 構(gòu)建了一套相對(duì)完整的符合 Http 語(yǔ)義的緩存機(jī)制誉帅。

優(yōu)點(diǎn)和特點(diǎn)

(1). 根據(jù)Cache-Control和Expires首部來(lái)計(jì)算緩存的過(guò)期時(shí)間。如果兩個(gè)首部都存在情況下谭期,以Cache-Control為準(zhǔn)堵第。

(2). 利用If-None-Match和If-Modified-Since對(duì)過(guò)期緩存或者不新鮮緩存,進(jìn)行請(qǐng)求再驗(yàn)證隧出,并處理 304 響應(yīng),更新緩存阀捅。

(3). 默認(rèn)的緩存實(shí)現(xiàn)胀瞪,將緩存以文件的形式存儲(chǔ)在 Disk,程序退出后不會(huì)丟失饲鄙。

我個(gè)人認(rèn)為的不足之處

緩存的再驗(yàn)證方面凄诞,在構(gòu)建If-Modified-Since請(qǐng)求首部時(shí),Volley 使用了服務(wù)端響應(yīng)的Date首部忍级,沒(méi)有使用Last-Modified首部帆谍。整個(gè)框架沒(méi)有使用Last-Modified首部。這與 Http 語(yǔ)義不符轴咱。

private void addCacheHeaders(Map headers, Cache.Entry entry) {

// If there's no cache entry, we're done.

if (entry == null) {

return;

}

if (entry.etag != null) {

headers.put("If-None-Match", entry.etag);

}

if (entry.serverDate > 0) {

Date refTime = new Date(entry.serverDate);

headers.put("If-Modified-Since", DateUtils.formatDate(refTime));

}

}

服務(wù)端根據(jù)請(qǐng)求時(shí)通過(guò)If-Modified-Since首部傳過(guò)來(lái)的時(shí)間汛蝙,判斷資源文件是否在If-Modified-Since時(shí)間以后有改動(dòng)烈涮,如果有改動(dòng),返回新的請(qǐng)求結(jié)果窖剑。如果沒(méi)有改動(dòng)坚洽,返回 304 not modified。

Last-Modified代表了資源文件的最后修改時(shí)間西土。通常使用這個(gè)首部構(gòu)建If-Modified-Since的時(shí)間讶舰。

Date代表了響應(yīng)產(chǎn)生的時(shí)間,正常情況下Date時(shí)間在Last-Modified時(shí)間之后需了。也就是Date>=Last-Modified跳昼。

通過(guò)以上原理,既然Date>=Last-Modified肋乍。那么我利用Date構(gòu)建鹅颊,也是完全正確的。

可能的問(wèn)題出在服務(wù)端的 Http 實(shí)現(xiàn)上住拭,如果服務(wù)端完全遵守 Http 語(yǔ)義挪略,采用時(shí)間比較的方式來(lái)驗(yàn)證If-Modified-Since,判斷服務(wù)器資源文件修改時(shí)間是不是在If-Modified-Since之后滔岳。那么使用Date完全正確杠娱。

可是有的服務(wù)端實(shí)現(xiàn)不是比較時(shí)間,而是直接的判斷服務(wù)器資源文件修改時(shí)間谱煤,是否和If-Modified-Since所傳時(shí)間相等摊求。這樣使用Date就不能實(shí)現(xiàn)正確的再驗(yàn)證,因?yàn)镈ate的時(shí)間總不會(huì)和服務(wù)器資源文件修改時(shí)間相等刘离。

盡管使用Date可能出現(xiàn)的不正確情況室叉,歸結(jié)于服務(wù)端沒(méi)有正確的實(shí)現(xiàn) Http 語(yǔ)義。

但我還是希望Volley也能完全正確的實(shí)現(xiàn)Http語(yǔ)義硫惕,至少同時(shí)處理Last-Modified和Date,并且優(yōu)先使用Last-Modified茧痕。

5.2 Bug

(1). BasicNetwork.performRequest(…)

如下代碼:

@Override

public NetworkResponse performRequest(Request request) throws VolleyError {

……

while (true) {

……

try {

……

} catch (IOException e) {

int statusCode = 0;

NetworkResponse networkResponse = null;

……

if (responseContents != null) {

……

} else {

throw new NetworkError(networkResponse);

}

}

}

}

BasicNetwork.performRequest(…) 最后的

throw new NetworkError(networkResponse);

應(yīng)該是

throw new NetworkError(e);

更合理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恼除,一起剝皮案震驚了整個(gè)濱河市踪旷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌豁辉,老刑警劉巖令野,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異徽级,居然都是意外死亡气破,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)餐抢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)现使,“玉大人低匙,你說(shuō)我怎么就攤上這事∑酉拢” “怎么了努咐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)殴胧。 經(jīng)常有香客問(wèn)我渗稍,道長(zhǎng),這世上最難降的妖魔是什么团滥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任竿屹,我火速辦了婚禮,結(jié)果婚禮上灸姊,老公的妹妹穿的比我還像新娘拱燃。我一直安慰自己,他們只是感情好力惯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布碗誉。 她就那樣靜靜地躺著,像睡著了一般父晶。 火紅的嫁衣襯著肌膚如雪哮缺。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,287評(píng)論 1 301
  • 那天甲喝,我揣著相機(jī)與錄音尝苇,去河邊找鬼。 笑死埠胖,一個(gè)胖子當(dāng)著我的面吹牛糠溜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播直撤,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼非竿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了谋竖?” 一聲冷哼從身側(cè)響起汽馋,我...
    開(kāi)封第一講書(shū)人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎圈盔,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體悄雅,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驱敲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宽闲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片众眨。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡握牧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出娩梨,到底是詐尸還是另有隱情沿腰,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布狈定,位于F島的核電站颂龙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏纽什。R本人自食惡果不足惜措嵌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芦缰。 院中可真熱鬧企巢,春花似錦、人聲如沸让蕾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)探孝。三九已至笋婿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間再姑,已是汗流浹背萌抵。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留元镀,地道東北人绍填。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像栖疑,于是被迫代替她去往敵國(guó)和親讨永。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容