對 FKDownloader 的完全重構(gòu)

原博客鏈接

前言

當(dāng)初編寫 0.x 版本時(shí), 尚未考慮過多邏輯, 整體架構(gòu)就是簡單的封裝系統(tǒng)邏輯, 導(dǎo)致在后期頻頻出問題, 而打補(bǔ)丁只會出更多的問題, 畢竟底子并沒有打好, 所以就起了重構(gòu)的心思.

靈感

剛一開始想?yún)⒖家幌?aria2 這個(gè)大佬級下載軟件, 但奈何這軟件涉及的下載方式過于復(fù)雜, 而且 iOS 的封閉式導(dǎo)致大部分邏輯不能使用, 強(qiáng)行借鑒是沒有好下場的, 只好另作打算.

之前因?yàn)樾枰獮樽约旱?App 編寫后臺, 就接觸到了 Python, 當(dāng)時(shí)為了將一些資源整合到自己的數(shù)據(jù)庫中, 就發(fā)現(xiàn)了 Scrapy 這個(gè)大名鼎鼎的爬蟲框架, 而這個(gè)框架的邏輯恰好符合我的想法.

根據(jù) iOS 下載系統(tǒng)的特性, 下載框架會按下圖的流程進(jìn)行制作:


架構(gòu)流程圖

至于為什么要這么做, 主要還是得看制作一個(gè)后臺下載框架都有什么難點(diǎn).

后臺下載框架主要難點(diǎn)與關(guān)鍵點(diǎn)

  1. 下載任務(wù)和下載流程全部歸系統(tǒng)管理
    首先, iOS 后臺下載的主要流程基本為: 創(chuàng)建 NSURL -> 創(chuàng)建 NSURLRequest -> 使用 SessionNSURLRequest 生成 NSURLSessionDownloadTask, 之后的下載信息, 如下載進(jìn)度, 下載失敗和下載成功等事件可以通過設(shè)置代理后獲得.

    然后, 如何在重啟 App 后獲得下載任務(wù)? 可以通過 -[NSURLSession getTasksWithCompletionHandler:] 獲取.

    但這個(gè)方法真的能獲取所有請求嗎? 官方文檔有解釋:

    The arrays passed to the completion handler contain any tasks that you have created within the session, not including any tasks that have been invalidated after completing, failing, or being cancelled.

    顯而易見, 對于已經(jīng)失效的任務(wù)是獲取不到的, 所以擁有一套任務(wù)記錄模塊是必須的.

    還有一種情況, 鏈接重定向, 這會導(dǎo)致拿到 NSURLSessionDownloadTask 后, 不知道對應(yīng)那個(gè)下載鏈接, 這種情況的解決方案有幾種, 可以監(jiān)聽 NSURLSessionDownloadTaskcurrentRequest 屬性來記錄最終下載鏈接, 也可以使用 taskDescription 屬性放置一個(gè)標(biāo)記, 當(dāng)然, 也許還有其他方法.

  2. 獲取任務(wù)進(jìn)度的自由度
    對于 NSURLSessionDownloadTask, 有 countOfBytesReceivedcountOfBytesExpectedToReceive 這兩個(gè)屬性可以獲取已下載字節(jié)長度和預(yù)計(jì)全部數(shù)據(jù)長度, 還有一些新的屬性也可以獲取這些信息.

    顯然, 對于單一任務(wù)來說, 這已經(jīng)足夠了, 但對于多個(gè)任務(wù), 并且可以任意分組的情況來說, 那就需要一個(gè)統(tǒng)一記錄進(jìn)度的模塊了.

  3. 奇奇怪怪得系統(tǒng) BUG
    這些個(gè) BUG 雖然都有應(yīng)對方法, 但部分應(yīng)對方法會影響一些邏輯.

    比如在 iOS 12/12.1, iPhone 8 以下機(jī)型會出現(xiàn) NSURLSessionDownloadTaskcountOfBytesReceivedcountOfBytesExpectedToReceive 屬性在進(jìn)入后臺, 再回到前臺后, 不再變更數(shù)值的問題, 需要來一個(gè)暫停/恢復(fù)的改變才會繼續(xù)工作, 這就需要框架有一個(gè)監(jiān)聽前/后臺切換的邏輯, 去處理這個(gè)問題.

    還有那個(gè)恢復(fù)數(shù)據(jù)有問題導(dǎo)致恢復(fù)下載失敗的問題, 需要在每次恢復(fù)下載時(shí)做一次修復(fù)處理, 對于一些需要將恢復(fù)數(shù)據(jù)保存到本地的邏輯來說, 就需要特殊邏輯來使這些邏輯共存.

    對于 Apple 來說, 后臺下載這些邏輯明顯是不想讓開發(fā)者過多干涉, 所以在出現(xiàn)一些問題時(shí), 開發(fā)者很被動, 只能找著法子各種規(guī)避了.

  4. 任務(wù)列表
    每個(gè)帶有下載功能的 App 基本上都會有下載列表, 但在下載框架里, 最好不要現(xiàn)實(shí)下載列表功能, 這畢竟屬于 App 的業(yè)務(wù)邏輯.

    而且因?yàn)闃I(yè)務(wù)影響, UI 影響, 會導(dǎo)致數(shù)據(jù)結(jié)構(gòu)千奇百怪, 第三方框架沒必要兼容, 也兼容不了.

    所以任務(wù)信息相關(guān)的模塊最好保持懶加載模式, 需要的時(shí)候直接拿來用, UI 相關(guān)的數(shù)據(jù)只保留最基本的數(shù)據(jù), 其他信息可以由 App 端實(shí)現(xiàn).

框架模塊

接下來就是根據(jù)架構(gòu)流程圖創(chuàng)建各種模塊了, 在 FKDownloader 中有兩種類型的模塊, 一種公開類型, 一種私有類型.

公開模塊為面向用戶的模塊, 包含 FKBuilder(構(gòu)建任務(wù)), FKMessager(獲取信息), FKConfigure(下載配置), FKControl(控制任務(wù)), FKMiddleware(中間件) 五個(gè)模塊.

私有模塊為框架內(nèi)部使用模塊, 包含主要模塊: FKEngine, FKCache, FKObserver, FKScheduler, FKSessionDelegater.

還有輔助模塊: FKSingleNumber, FKFileManager, FKLogger, FKCoder, FKMIMEType.

還有一些數(shù)據(jù)模型: FKObserverModel, FKCacheRequestModel, FKResponse.

它們將按照流程圖來處理下載任務(wù), 大體上來講, FKBuilder 是輸入, FKMessager 是輸出, FKEngine 是處理, FKControl 是控制, FKCache 是儲存.

框架模塊詳細(xì)講解

首先, 是公開模塊.

FKBuilder

該模塊主要對應(yīng) 創(chuàng)建NSURLRequest 階段, 獨(dú)立出來是為了更好的控制構(gòu)建 NSURLReqest 的過程.

鑒于 NSURLReqest 的屬性和方法會隨著系統(tǒng)更新變得越來越多, 越來越復(fù)雜, 且自定義不會對生成 NSURLSessionDownloadTask 流程產(chǎn)生影響, 所以 FKBuilder 直接繼承于 NSMutableURLRequest, 用戶可以像操作 NSMutableURLRequest 一樣操作 FKBuilder. 對于用戶傳入的 URL 是否合法, 也可在初始化時(shí)進(jìn)行校驗(yàn).

FKBuilder 需要顯式執(zhí)行預(yù)處理, 這樣才能將任務(wù)加入隊(duì)列中.

FKMessager

該模塊對應(yīng)請求信息收集邏輯, 比如進(jìn)度、狀態(tài)绣溜、錯(cuò)誤等.

基本上所有有關(guān)下載的業(yè)務(wù)邏輯, 添加下載的界面和查看下載進(jìn)度的是分開的, 所以, FKDownloader 就干脆將任務(wù)信息收集相關(guān)邏輯全部獨(dú)立出來, 同時(shí)也可以更好的對應(yīng)列表樣式的信息獲取.

FKConfigure

一個(gè)下載框架如果不能自定義配置, 那就沒有靈魂.

查看 NSURLSessionConfiguration 的官方文檔就會發(fā)現(xiàn)系統(tǒng)提供的參數(shù)巨多, 而且還包含了新版本特性, 這就導(dǎo)致對外開放什么屬性/方法成了難題, 多了不可控, 少了又沒有高度自定義那味, 所以 FKConfigure 直接提供了一個(gè) NSURLSessionConfiguration 模版, 化為屬性, 只把其中的能否使用蜂窩 allowsCellularAccess 默認(rèn)設(shè)置為允許, 其他通通由用戶自定義.

至于 NSMutableURLRequestNSURLSessionConfiguration 的部分屬性沖突, 這部分官方已經(jīng)在注釋里講的很明白了, 用戶可自行斟酌.

FKControl

這就是個(gè)控制任務(wù)的, 激活幕帆、暫停侮繁、繼續(xù)、停止如孝、刪除, 沒什么好說的, 獨(dú)立出來只是為了面向用戶, 不用跟其他私有模塊產(chǎn)生沖突, 而且多出一層, 可操作性也會多一層.

FKMiddleware

中間件模塊, 我也就在爬蟲框架和后端框架中見過, FKDownloader 中有這種模塊主要是為了有一種可以統(tǒng)一處理的方式.

目前該模塊只在生成 NSURLSessionDownloadTask 前, 下載成功/失敗后會有介入, 前者是為了諸如請求統(tǒng)一加簽宪哩、繞過瀏覽器限制等操作, 后者可以當(dāng)成 NSURLSession 代理中下載成功/失敗的回調(diào)即可.

更多的操作可根據(jù)業(yè)務(wù)自行調(diào)整.



再來是私有模塊.

FKEngine

該模塊基本就是框架運(yùn)轉(zhuǎn)的核心.

在一般下載邏輯中, 會為每個(gè)任務(wù)創(chuàng)建一個(gè)計(jì)時(shí)器, 實(shí)現(xiàn)進(jìn)度信息分發(fā)邏輯, 但這一塊兒其實(shí)用不了那么多, 還會因?yàn)楣芾聿贿^來出現(xiàn)問題(0.x版本就有這類問題), 所以在 FKEngine 中只有兩個(gè)個(gè)計(jì)時(shí)器, 只為了更簡潔的操作, 畢竟在實(shí)際業(yè)務(wù)中, 這些進(jìn)度條各走各還是一起走都無所謂,

計(jì)時(shí)器主要完成以下任務(wù):

  1. 檢查任務(wù)隊(duì)列, 進(jìn)行下一個(gè)任務(wù)

首先, FKBuilder 的預(yù)處理會將生成的任務(wù)信息模型(FKCacheRequestModel) 存入 FKCache 的緩存隊(duì)列中, 但不開始任務(wù), 也不創(chuàng)建 NSURLSessionDownloadTask, 這是前提, 與計(jì)時(shí)器無關(guān).

然后, 計(jì)時(shí)器被觸發(fā)后就會檢查隊(duì)列中正在執(zhí)行的任務(wù)是否超過設(shè)置, 超過了就什么也不做, 沒超過就開始用任務(wù)信息創(chuàng)建 NSURLSessionDownloadTask, 進(jìn)行下載, 期間走過中間件流程, 本地信息緩存流程, 監(jiān)聽信息流程等等流程.

執(zhí)行任務(wù)計(jì)時(shí)器的間隔為 1s, 不可自定義.

  1. 分發(fā)任務(wù)信息

在使用 FKMessager 時(shí), 回調(diào)會被緩存, 計(jì)時(shí)器被觸發(fā)后將會輪詢執(zhí)行這些回調(diào)

以上任務(wù)都會在計(jì)時(shí)器觸發(fā)后執(zhí)行. 默認(rèn)情況下, 這個(gè)計(jì)時(shí)器是停止的, 需要用 FKConfigure 來激活, 這是為了保證在 NSURLSession 被創(chuàng)建后再執(zhí)行任務(wù).

計(jì)時(shí)器重復(fù)時(shí)間默認(rèn)設(shè)定為 1s, 這個(gè)時(shí)間剛剛好, 少了太頻繁, 多了感覺慢.
也支持用戶自定義速率, 目前 1 倍速率為 0.2 秒, 倍率在 1 ~ 10 倍區(qū)間可自定義.

除此之外, 應(yīng)用啟動后還會去查詢后臺已經(jīng)存在的下載任務(wù), 將這些任務(wù)添加到緩存中, 讓它們和其他任務(wù)一致.

基本上其他模塊只是制造信息, 輸出信息和保存信息, 而 FKEngine 則是讓整個(gè)框架活過來.

FKCache

主要負(fù)責(zé)信息緩存, 任務(wù)的信息, NSURLSessionDownloadTask 等等.

獨(dú)立的緩存模塊是必須的, 要緩存的信息有很多, 集中起來更容易管理.

FKDownloader 中, 對任務(wù)有一個(gè)主要理念: 任務(wù)即文件, 文件即任務(wù), 每一個(gè)任務(wù)都有一個(gè)屬于自己的唯一標(biāo)識, 標(biāo)識與用戶輸入的鏈接息息相關(guān), 也和本地緩存有著千絲萬縷的聯(lián)系.

FKDownloader 中的文件就是 FKCacheRequestModel 對應(yīng)的歸檔文件, 這個(gè)模型提供的信息有:

@property (nonatomic, strong) NSString *requestID; // 請求標(biāo)識, SHA256(URL)
@property (nonatomic, strong) NSString *requestSingleID; // 唯一請求標(biāo)識, SingleNumber_SHA256(URL)
@property (nonatomic, strong) NSString *idx; // 唯一順序編碼
@property (nonatomic, strong) NSString *url; // 原始請求鏈接
@property (nonatomic, strong) NSMutableURLRequest *request; // 請求
@property (nonatomic, assign) FKState state; // 請求狀態(tài)
@property (nonatomic, assign) int64_t receivedLength; // 接收的數(shù)據(jù)長度
@property (nonatomic, assign) int64_t dataLength; // 數(shù)據(jù)長度
@property (nonatomic, strong) NSString *extension; // 文件后綴, `.*`
@property (nonatomic, strong, nullable) NSData *resumeData; // 恢復(fù)數(shù)據(jù)
@property (nonatomic, strong, nullable) NSError *error; // 錯(cuò)誤

基本上可以構(gòu)成/恢復(fù)任務(wù)的信息都在里面, 每一個(gè)任務(wù)都有自己的文件夾保存這些信息, 分而治之有利于管理, 0.x版本中都是所有任務(wù)都在一個(gè)文件中, 不管從性能上看, 還是管理上看, 都有很大的問題.

FKObserver

眾多任務(wù)需要監(jiān)聽的流程太過繁雜也太過分散, 系統(tǒng) BUG 還導(dǎo)致這些監(jiān)聽還需要重新添加, 這就更分散了. 而獲取進(jìn)度信息在業(yè)務(wù)上來講并不頻繁使用, 這些監(jiān)聽到的信息全放在任務(wù)信息模型里也不合適, 那么, 直接獨(dú)立出來成模塊豈不美哉.

FKObserver 以專門監(jiān)聽 NSURLSessionDownloadTask 而生, 所有任務(wù)的進(jìn)度信息都在這里.

FKObserver 使用 FKObserverModel 保存進(jìn)度信息, 基本信息如下:

@interface FKObserverModel : NSObject

@property (nonatomic, strong) NSString *requestID; // SHA256(Request.URL)
@property (nonatomic, assign) int64_t countOfBytesReceived;
@property (nonatomic, assign) int64_t countOfBytesPreviousReceived;
@property (nonatomic, assign) int64_t countOfBytesExpectedToReceive;

@end

簡約而不簡單, 并且和 FKMessager 配合完美, 一個(gè)對外, 一個(gè)對內(nèi).

FKScheduler

FKBuilderFKEngine 之間的模塊, FKControl 的實(shí)現(xiàn), 主要任務(wù)如下:

  1. FKBuilder 的預(yù)處理邏輯進(jìn)行了更細(xì)節(jié)的處理. 如創(chuàng)建任務(wù)信息文件, 添加內(nèi)存/本地緩存, 忽略已存在任務(wù)等.
  2. 實(shí)現(xiàn) FKControl 的操作, 激活、暫停第晰、繼續(xù)锁孟、取消、刪除.
FKSessionDelegater

實(shí)現(xiàn) NSURLSession 的代理, 沒啥好說, 單獨(dú)摘出來是因?yàn)榇矸椒ㄟ€是很多很復(fù)雜的, 為了之后更好的擴(kuò)展, 這樣更好一些.



還有一些輔助用模塊

FKSingleNumber

在執(zhí)行下一個(gè)任務(wù)時(shí), 哪個(gè)才是下一個(gè)? 按添加順序可不一定準(zhǔn), 所以 FKDownloader 直接使用 stdatomic.h 中的 atomic_ullong 來創(chuàng)建一個(gè)不受線程影響的原子數(shù), 再讓它被獲取時(shí)自增.

FKCacheRequestModelrequestSingleID 就是原子數(shù)和下載鏈接的哈希值拼接出來的.

當(dāng)然, 從業(yè)務(wù)上來講, 任務(wù)的執(zhí)行順序是否按照列表所示順序依次進(jìn)行好像并不怎么重要.

FKFileManager

文件管理的封裝, 主要負(fù)責(zé)創(chuàng)建/刪除任務(wù)對應(yīng)的文件/文件夾.

FKLogger

輔助信息日志, 這倒是沒啥好說的, 只是為了更方便調(diào)試, 信息只會在 DEBUG 環(huán)境下執(zhí)行.

FKCoder

URL 編解碼.

先說編碼, 有一個(gè)問題便是用戶傳入的下載鏈接是否已經(jīng)編碼過, 這個(gè)可以循環(huán)解碼至和上一個(gè)結(jié)果一致時(shí)停下, 這個(gè)問題不算太大. 但 URL 可能有帶有 emoji,fragment 的情況, emoji 可以用系統(tǒng)的 URLQueryAllowedCharacterSet 直接處理, 但 fragment 就會編碼錯(cuò)誤, 這時(shí)就需要分段處理.

再說解碼, 這個(gè)就簡單了, 直接 stringByRemovingPercentEncoding 走起.

FKMIMEType.

既然是文件下載, 那基本上都有后綴吧, 直接從 URL 里獲取是不現(xiàn)實(shí)的, 畢竟有的鏈接是加簽的, 后綴是不存在的, 所以要用 Response 中的 Content-Type 也就是 MIMEType 來轉(zhuǎn)為后綴名.

系統(tǒng)可以講 MIMEType 轉(zhuǎn)為后綴, 但并不全面, 所以需要將其他常用的加入轉(zhuǎn)換列表中, 如果實(shí)在沒有對應(yīng)后綴名, 就用 unknown 為后綴名.

關(guān)于后臺下載的 Tips

后臺下載功能中也存在一些需要知道的東西.

后臺任務(wù)由系統(tǒng)啟動后的各種限制

先說結(jié)論, 目前沒有什么好的方法去繞過. 系統(tǒng)的限制基本上有以下幾種:

  1. 限制下載速度
  2. 限制何時(shí)啟動下一個(gè)任務(wù)的時(shí)間
  3. 限制任務(wù)啟動數(shù)量
  4. ....

總的來說, iOS 為了達(dá)到完美的運(yùn)行并且不會影響系統(tǒng)的穩(wěn)定性, 后臺下載的內(nèi)核做了非常多的限制, 而且為什么有這些限制, 又是怎樣做到的, 官方并沒有明說, 只能從這里看出一個(gè)重點(diǎn)信息, NSURLSession Background Download 是系統(tǒng)包攬的, 開發(fā)者最好不要深入研究.

測試后臺下載流程

這里可以看出, 測試時(shí)一定要嚴(yán)格遵守以下幾點(diǎn):

  1. Test on a real device, not the simulator. 在真機(jī)上測試, 而不是模擬器.
  2. Run your app from the Home screen rather than running it from Xcode. 從主屏幕上運(yùn)行, 而不是 Xcode 直接運(yùn)行.
  3. Do not use force quit to test the ‘relaunch in the background case’. 不要從任務(wù)管理中強(qiáng)制退出 App 來模擬后臺下載流程中的強(qiáng)制中斷 App 邏輯, 而是在合適的地方使用 exit() 來退出 App.

參考

  1. MIMEType IANA
  2. MIMEType to Extension
  3. NSURLSession’s Resume Rate Limiter
  4. Testing Background Session Code
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子竭翠,更是在濱河造成了極大的恐慌晚吞,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溪猿,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盆昙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焊虏,“玉大人淡喜,你說我怎么就攤上這事∷斜眨” “怎么了炼团?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長疏尿。 經(jīng)常有香客問我瘟芝,道長,這世上最難降的妖魔是什么润歉? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任模狭,我火速辦了婚禮,結(jié)果婚禮上踩衩,老公的妹妹穿的比我還像新娘嚼鹉。我一直安慰自己,他們只是感情好驱富,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布锚赤。 她就那樣靜靜地躺著,像睡著了一般褐鸥。 火紅的嫁衣襯著肌膚如雪线脚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音浑侥,去河邊找鬼姊舵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛寓落,可吹牛的內(nèi)容都是我干的括丁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼伶选,長吁一口氣:“原來是場噩夢啊……” “哼史飞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仰税,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤构资,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后陨簇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吐绵,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年塞帐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拦赠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡葵姥,死狀恐怖荷鼠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情榔幸,我是刑警寧澤允乐,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站削咆,受9級特大地震影響牍疏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拨齐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一鳞陨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞻惋,春花似錦厦滤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至羽峰,卻和暖如春趟咆,著一層夾襖步出監(jiān)牢的瞬間添瓷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工值纱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鳞贷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓虐唠,卻偏偏與公主長得像悄晃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子凿滤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

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