Volley源碼淺析

Volley目前看來是一個很老的框架了揽乱,很早之前我也在項目中使用過密幔,但是當時沒有去深入了解其原理。后來OkHttp出來之后喇肋,便遷移到了Okhttp。Okhttp以他的高效聞名迹辐,而大多數(shù)文章也僅僅只是一筆帶過蝶防,許多人也只是跟風效仿并不知道其中為何高效之處。而同為優(yōu)秀的熱門框架明吩,為何Okhttp更被大家所推薦间学,更多人使用?這是我重新研究Volley的原因贺喝,既然性能有優(yōu)劣菱鸥,那一定是需要對比。所以我們起碼要了解不同框架的原理和實現(xiàn)思路躏鱼,這樣才能知道為什么這個框架更加好更加適合我們的業(yè)務氮采,是否需要使用這個框架,這是對技術(shù)選型判斷的依據(jù)染苛,也是我寫這個文章和后面分析okhttp的目的鹊漠。

下面我會由一個基本的發(fā)起請求調(diào)用開始,一步步分析Volley運行機制茶行。

簡單的調(diào)用

下面的例子是一個最基本的Volley發(fā)起get躯概、post請求的一個調(diào)用。

    fun requst(){
        val url = ""
        val queue = Volley.newRequestQueue(context)
        val getRequest = StringRequest(Request.Method.GET, url,this, this)
        val postRequest = object : StringRequest(Request.Method.POST, url, this, this){
            @Throws(AuthFailureError::class)
            override fun getParams(): Map<String, String> {
                return HashMap<String, String>()
            }
        }
        queue.add(getRequest)
        queue.add(postRequest)
    }

設計圖

大家可以先看一眼這個設計圖畔师,有一個大致的概念娶靡,知道有這么些東西,下面的流程分析中都會涉及到看锉,看完可以再回顧一下這張圖加深印象姿锭。
同時如果想深入了解Volley細節(jié)的,推薦看一下這個文章Volley源碼解析伯铣,圖片也是來自這篇文章的呻此。

image.png

請求內(nèi)部流程

首先分析其構(gòu)造queue的邏輯,newRequestQueue方法最終會調(diào)用到

    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) {
        }
        
        //這里會構(gòu)建一個HurlStack對象腔寡,這個對象是最終建立連接發(fā)起請求的地方
        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));
            }
        }

        //BasicNetwork對象可以理解為發(fā)送請求的輔助類焚鲜,會做一些網(wǎng)絡超時重試讀取寫入response一些操作
        Network network = new BasicNetwork(stack);
        
        RequestQueue queue;
        //如果沒有指定最大的本地緩存文件大小會調(diào)用默認的構(gòu)造方法,默認是5*1024*1024
        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);
        }

        //開啟隊列循環(huán)
        queue.start();

        return queue;
    }

對于這個過程關(guān)鍵的一個地方我寫了一些注釋,先看一下RequestQueue的構(gòu)造過程忿磅。
首先從上面我們可以看到他構(gòu)建了一個DiskBasedCache對象糯彬,這個對象的功能是緩存response。緩存的容器是一個初始大小為16的LinkedHashMap葱她,如果不設置緩存情连,默認的大小是510241024。每次添加緩存的時候會先判斷容器剩余大小是否滿足览效,不足的話會遍歷LinkedHashMap刪除,直達滿足最大容量*0.9虫几,這個里面的寫入請求頭的操作還大量設計到了位運算锤灿。有興趣的可以單獨看一下com.android.volley.toolbox.DiskBasedCache這個類的實現(xiàn)。
分析完DiskBasedCache對象之后辆脸,我們看一下RequestQueue對象構(gòu)建的過程:

    public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }

我貼上來的是最終的構(gòu)造方法但校,實際上如果不指定線程池的大小,會默認創(chuàng)建一個默認大小為4的ExecutorDelivery線程數(shù)組啡氢。
首先看一下ResponseDelivery對象:

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

這個類的作用是對請求的結(jié)果進行分發(fā)状囱,我們也看到了,這里傳入的是一個主線程的handler對象倘是,他的作用實際上也就是把對網(wǎng)絡請求和IO操作的結(jié)果切換到了UI線程亭枷。有興趣的可查看com.android.volley.ExecutorDelivery。
接下來就是開啟隊列的循環(huán):

    public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        //CacheDispatcher對象繼承Thread
        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();
        }
    }

這里我們可以看到兩個對象搀崭,CacheDispatcher叨粘、NetworkDispatcher。這里一個是負責處理復用本地緩存請求瘤睹,一個是獲取網(wǎng)絡數(shù)據(jù)的升敲,與兩個隊列mCacheQueue、mNetworkQueue相對應轰传。這里的邏輯就是開啟一個請求緩存的線程驴党,開啟指定數(shù)量的獲取網(wǎng)絡請求的線程,至于隊列中的數(shù)據(jù)是從何而來获茬,這個我們待會兒分析港庄,先看看NetworkDispatcher這個線程是如何運行的:

    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Request<?> request;
        //開啟循環(huán),不斷的從隊列中獲取需要處理的請求
        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try {
                // Take a request from the queue.
                request = mQueue.take();
            } catch (InterruptedException e) {
                //這里如果我們在外部調(diào)用了quit 會停止循環(huán)
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }

            try {
                request.addMarker("network-queue-take");
                
                //如果請求已經(jīng)手動取消 則移出當前正在請求的隊列
                // 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");

                //如果服務器返回304并且已經(jīng)響應過這個請求 移出當前正在請求的隊列 并加入請求緩存隊列
                // 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");

                //如果request需要緩存(默認true)且response正常返回則把reponse寫入緩存
                // 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");
                }

                //對request進行標記锦茁,緩存是可用的
                // 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);
            }
        }
    }

上面比較重要的地方我都寫了注釋攘轩,邏輯其實比較簡單。比較重要的就是finish方法:

    <T> void finish(Request<T> request) {
        // Remove from the set of requests currently being processed.
        synchronized (mCurrentRequests) {
            mCurrentRequests.remove(request);
        }
        synchronized (mFinishedListeners) {
          for (RequestFinishedListener<T> listener : mFinishedListeners) {
            listener.onRequestFinished(request);
          }
        }

        //這里是最關(guān)鍵的邏輯码俩,如果需要緩存對該請求的響應度帮,會拼接請求類型和url作為key
        //從mWaitingRequests集合中移除對應request的隊列,并全部添加到緩存隊列中
        if (request.shouldCache()) {
            synchronized (mWaitingRequests) {
                String cacheKey = request.getCacheKey();
                Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
                if (waitingRequests != null) {
                    if (VolleyLog.DEBUG) {
                        VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
                                waitingRequests.size(), cacheKey);
                    }
                    // Process all queued up requests. They won't be considered as in flight, but
                    // that's not a problem as the cache has been primed by 'request'.
                    mCacheQueue.addAll(waitingRequests);
                }
            }
        }
    }

上面關(guān)鍵邏輯我寫了備注,直接看是不好理解的笨篷,這個跟前面我們調(diào)用時的add方法是相關(guān)的瞳秽,而上面提到的queue的數(shù)據(jù)就是來自這里:

    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");

        //如果不需要緩存,就直接添加到網(wǎng)絡隊列中并返回
        // If the request is uncacheable, skip the cache queue and go straight to the network.
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

        //這部分是重要的邏輯
        //如果mWaitingRequests集合中有request這個key率翅,則把這次的request繼續(xù)添加到這個隊列中
        //如果這個集合中沒有與request匹配的隊列练俐,則直接把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;
        }
    }

add方法和剛剛的finish方法應該結(jié)合一起看,方便理解冕臭。大致的流程是:當我們調(diào)用add添加請求時腺晾,會根據(jù)是否需要緩存去做不同的處理。不需要緩存的這里不贅述很好理解辜贵,需要緩存的情況下Volley實際上是會把請求緩存在mWaitingRequests這么一個集合當中悯蝉,mWaitingRequests是一個HashMap對象。
這樣可以保證當頻繁的重復請求時會把所有的重復請求都放在一個隊列中托慨,而在finish方法中我們可以看到鼻由,當請求復用緩存的時候,會把所有相同的請求都一起添加到緩存隊列中厚棵。
其實當我看到add方法中蕉世,會緩存相同請求到同一個隊列中時我就有點疑惑這樣做的目的到底是什么?
這個其實跟我上面一筆帶過的DiskBasedCache有關(guān)聯(lián)婆硬,這個上面講過是負責緩存response的狠轻,而他緩存的容器是LinkedHashMap:

    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

關(guān)鍵的原因就在于這里初始化構(gòu)造的accessOrder參數(shù),而這個參數(shù)會影響你查詢的策略彬犯,false是基于插入順序哈误,true是基于訪問順序,具體實現(xiàn)方式可以自行查看LinkedHashMap的get方法躏嚎。這樣的話會對查詢重復的元素效率提升巨大蜜自。
關(guān)于CacheDispatcher,其實他的實現(xiàn)跟上面的NetworkDispatcher是類似的卢佣,也是會循環(huán)的從queue中取數(shù)據(jù)重荠,然后去緩存中查找,會根據(jù)緩存是否失效是否存在等判斷查找緩存虚茶,如果沒有命中緩存則會把請求添加到網(wǎng)絡隊列中戈鲁。
上面還有一點需要提到的就是緩存request的隊列PriorityBlockingQueue,這是一個實現(xiàn)了阻塞的優(yōu)先級隊列嘹叫,其內(nèi)部實際上是一個堆婆殿,而request實現(xiàn)了compare接口保證我們的請求是按照添加進去的順序來決定優(yōu)先級的。這個PriorityBlockingQueue具體細節(jié)我會在之后的文章里面講解罩扇,有興趣的也可以自行查看源碼婆芦。
看完了Volley如何進行一次完整的請求以及緩存怕磨、線程、隊列的流程消约,下面就是最重要的一點網(wǎng)絡連接的實現(xiàn)肠鲫,上面有提到過,真正建立連接是在HurlStack對象中的createConnection()方法進行的:

    protected HttpURLConnection createConnection(URL url) throws IOException {
        return (HttpURLConnection) url.openConnection();
    }

這個實際上就是Android自帶的庫java.net.Url完成的請求或粮,這個里面最重要的就是getURLStreamHandler()方法生成的handler:

    static URLStreamHandler getURLStreamHandler(String protocol) {

        URLStreamHandler handler = handlers.get(protocol);
        if (handler == null) {

            boolean checkedWithFactory = false;

            //step1 從緩存中查找對應handler
            // Use the factory (if any)
            if (factory != null) {
                handler = factory.createURLStreamHandler(protocol);
                checkedWithFactory = true;
            }

            //step2 在指定包名下查找是否有自定義的協(xié)議
            // Try java protocol handler
            if (handler == null) {
                final String packagePrefixList = System.getProperty(protocolPathProp,"");
                StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|");

                while (handler == null &&
                       packagePrefixIter.hasMoreTokens()) {

                    String packagePrefix = packagePrefixIter.nextToken().trim();
                    try {
                        String clsName = packagePrefix + "." + protocol +
                          ".Handler";
                        Class<?> cls = null;
                        try {
                            ClassLoader cl = ClassLoader.getSystemClassLoader();
                            cls = Class.forName(clsName, true, cl);
                        } catch (ClassNotFoundException e) {
                            ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
                            if (contextLoader != null) {
                                cls = Class.forName(clsName, true, contextLoader);
                            }
                        }
                        if (cls != null) {
                            handler  =
                              (URLStreamHandler)cls.newInstance();
                        }
                    } catch (ReflectiveOperationException ignored) {
                    }
                }
            }

            //step3 通過不同的協(xié)議通過反射創(chuàng)建對應的handler导饲,http請求由okhttp執(zhí)行
            // Fallback to built-in stream handler.
            // Makes okhttp the default http/https handler
            if (handler == null) {
                try {
                    // BEGIN Android-changed
                    // Use of okhttp for http and https
                    // Removed unnecessary use of reflection for sun classes
                    if (protocol.equals("file")) {
                        handler = new sun.net.www.protocol.file.Handler();
                    } else if (protocol.equals("ftp")) {
                        handler = new sun.net.www.protocol.ftp.Handler();
                    } else if (protocol.equals("jar")) {
                        handler = new sun.net.www.protocol.jar.Handler();
                    } else if (protocol.equals("http")) {
                        handler = (URLStreamHandler)Class.
                            forName("com.android.okhttp.HttpHandler").newInstance();
                    } else if (protocol.equals("https")) {
                        handler = (URLStreamHandler)Class.
                            forName("com.android.okhttp.HttpsHandler").newInstance();
                    }
                    // END Android-changed
                } catch (Exception e) {
                    throw new AssertionError(e);
                }
            }

            synchronized (streamHandlerLock) {

                URLStreamHandler handler2 = null;

                // Check again with hashtable just in case another
                // thread created a handler since we last checked
                handler2 = handlers.get(protocol);

                if (handler2 != null) {
                    return handler2;
                }

                // Check with factory if another thread set a
                // factory since our last check
                if (!checkedWithFactory && factory != null) {
                    handler2 = factory.createURLStreamHandler(protocol);
                }
 
                if (handler2 != null) {
                    // The handler from the factory must be given more
                    // importance. Discard the default handler that
                    // this thread created.
                    handler = handler2;
                }

                // Insert this handler into the hashtable
                if (handler != null) {
                    handlers.put(protocol, handler);
                }

            }
        }

        return handler;

    }

這個地方稍微講解一下Java提供為網(wǎng)絡請求提供的庫,在建立連接的時候我們會創(chuàng)建一個URL資源和一個URLConnection對象氯材,而針對不同的協(xié)議會有不同的URLStreamHandler和對應的URLConnection來分別負責對協(xié)議的解析渣锦,以及與服務器的交互(數(shù)據(jù)轉(zhuǎn)換等)。
上面我注釋的step1和step3都比較好理解氢哮,而step2是留給用戶拓展泡挺,開發(fā)自定義的通訊協(xié)議使用的,這里了解一下就行命浴。我們需要關(guān)心的是step3,http的請求實際上是通過okhttp實現(xiàn)的贱除,大家查閱資料或者看源碼都能知道android4.4后原生的網(wǎng)絡請求已經(jīng)替換為okhttp了生闲。

總結(jié)

至此,我們對Volley的分析已經(jīng)結(jié)束≡禄希現(xiàn)在稍微總結(jié)一下碍讯,Volley實現(xiàn)了一套完整的符合Http語義的緩存機制,并且對性能方面有一些優(yōu)化(緩存的命中扯躺、緩存的寫入捉兴、重復請求的隊列)。在設計中录语,Volley定義了大量的接口倍啥,正是由于這些設計,可以使得Volley擁有高度的擴展性澎埠,用戶可以針對自己的需求自由的訂制其中的實現(xiàn)虽缕。針對接口編程,不針對具體細節(jié)實現(xiàn)編程蒲稳,多用組合氮趋,少用繼承。許多優(yōu)秀的框架也擁有同樣的特性江耀,這也是我們在平時開發(fā)過程中能夠?qū)W習運用的剩胁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祥国,隨后出現(xiàn)的幾起案子昵观,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件索昂,死亡現(xiàn)場離奇詭異建车,居然都是意外死亡,警方通過查閱死者的電腦和手機椒惨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門缤至,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人康谆,你說我怎么就攤上這事领斥。” “怎么了沃暗?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵月洛,是天一觀的道長。 經(jīng)常有香客問我孽锥,道長嚼黔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任惜辑,我火速辦了婚禮唬涧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盛撑。我一直安慰自己碎节,他們只是感情好,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布抵卫。 她就那樣靜靜地躺著狮荔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪介粘。 梳的紋絲不亂的頭發(fā)上殖氏,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音姻采,去河邊找鬼受葛。 笑死,一個胖子當著我的面吹牛偎谁,可吹牛的內(nèi)容都是我干的总滩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼巡雨,長吁一口氣:“原來是場噩夢啊……” “哼闰渔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铐望,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤冈涧,失蹤者是張志新(化名)和其女友劉穎茂附,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體督弓,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡营曼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了愚隧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒂阱。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖狂塘,靈堂內(nèi)的尸體忽然破棺而出录煤,到底是詐尸還是另有隱情,我是刑警寧澤荞胡,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布妈踊,位于F島的核電站,受9級特大地震影響泪漂,放射性物質(zhì)發(fā)生泄漏廊营。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一萝勤、第九天 我趴在偏房一處隱蔽的房頂上張望露筒。 院中可真熱鬧,春花似錦纵刘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鞍历,卻和暖如春舵抹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劣砍。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工惧蛹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刑枝。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓香嗓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親装畅。 傳聞我的和親對象是個殘疾皇子靠娱,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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