細(xì)化 Flutter List 內(nèi)存回收,解決大 Cell 問題

****前言****


何謂大 Cell 問題贫橙?在基于 Native List 的渲染方案中顽铸,都會遇到大 Cell 問題。比如 Weex 業(yè)務(wù)中料皇,經(jīng)常出現(xiàn)頁面內(nèi)存飆高谓松,排查后發(fā)現(xiàn)多為前端寫法導(dǎo)致的一個(gè)大 Cell 中存在過多圖片星压,導(dǎo)致內(nèi)存過高。在 Flutter 里同樣有這個(gè)問題鬼譬,本質(zhì)原因都是因?yàn)?List 進(jìn)行回收的單位是 Cell娜膘,而不是 Cell 中的圖片。在瀏覽器體系下优质,不存在這個(gè)問題竣贪,想必是瀏覽器進(jìn)行了額外的運(yùn)算,可以正確回收出屏的圖片巩螃。
在開發(fā) Flutter 版本淘寶商品詳情頁面時(shí)演怎,我們同樣遇到了大 Cell 的問題。一個(gè)商品的詳情由多張圖片拼接而成避乏,這些圖片尺寸未知爷耀,需要進(jìn)行高度自適應(yīng),圖片被放在同一個(gè) Cell 中拍皮。發(fā)現(xiàn)列表滾動到特定位置歹叮,大量圖片同時(shí)加載并生成紋理,內(nèi)存突然飆高铆帽。

image

該問題有兩個(gè)解決方案:

  1. 重構(gòu)業(yè)務(wù)層代碼咆耿,把圖片分散在多個(gè) Cell 里。但是因?yàn)槿狈Ω叨刃畔⒌鳎珻ell 仍然會一次性全部出現(xiàn)萨螺,帶來內(nèi)存問題。

  2. 細(xì)化 Flutter List 的回收能力愧驱,在 Cell 回收的基礎(chǔ)上屑迂,可以做到以圖片為單位進(jìn)行回收。

方案1只能說治標(biāo)不治本冯键,而且成本較高惹盼。根據(jù) Weex 的經(jīng)驗(yàn),業(yè)務(wù)開發(fā)同學(xué)難免會因?yàn)椴蛔⒁舛斐纱?Cell 的實(shí)際存在導(dǎo)致線上內(nèi)存問題惫确。而方案2就是本文要探索的方法手报,在 Flutter 體系內(nèi)增強(qiáng)圖片回收能力,降低內(nèi)存占用改化。

****方案探索過程****


? 繪制圖片的坐標(biāo)信息

Flutter 里掩蛤,圖片的繪制在 Dart 層調(diào)用到 RenderImage.paint 方法。在里面打日志陈肛,發(fā)現(xiàn)繪制的時(shí)候揍鸟,可以近似認(rèn)為 offset 參數(shù)的值就是圖片相對頁面左上角的距離。(如果頁面層級更復(fù)雜句旱,比如 List 非全屏阳藻,上面有 TabBar 等晰奖,該偏移值可能不準(zhǔn)確。)

image

<pre class="code-snippet__js" data-lang="css" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 74.4)``2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 449.4)``2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 824.4)``2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 1199.4)``2020-02-06 Runner[45049:2962074] flutter: [AA] Render offset: Offset(0.0, 1574.4)``....</pre>

? 提根據(jù)坐標(biāo)判斷圖片是否在屏幕內(nèi)

有了坐標(biāo)信息腥泥,也就有了一個(gè)粗略的方法判斷圖片是否在屏幕內(nèi)匾南。在實(shí)際代碼中,我使用下面的方法來判斷蛔外。這個(gè)方法只能判斷是否在屏幕內(nèi)蛆楞,不能判斷是否滑出 List 或被 NavigationBar 遮蓋等場景。

<pre class="code-snippet__js" data-lang="java" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">void paint(PaintingContext context, Offset offset) { // Check if Rect(offset & size) intersects with screen bounds. final double screenWidth = ui.window.physicalSize.width / ui.window.devicePixelRatio; final double screenHeight = ui.window.physicalSize.height / ui.window.devicePixelRatio; if (offset.dy >= screenHeight - 1 || offset.dy <= -size.height + 1 || offset.dx >= screenWidth - 1 || offset.dx <= -size.width + 1) { // 在屏幕外 } ....``}</pre>

? 強(qiáng)制每幀重新繪制該 Cell

打日志發(fā)現(xiàn)夹厌,即使是個(gè)超長的 Cell豹爹,F(xiàn)lutter 也只會繪制一次,生成一個(gè)大的紋理矛纹。之后在滾動過程中便不會有 RenderImage.paint 調(diào)用了臂聋。研究代碼發(fā)現(xiàn),在 sliver.dart 文件中崖技,每個(gè) Cell 被強(qiáng)制包裹在 RepaintBoundary 中。而這個(gè) addRepaintBoundaries 參數(shù)默認(rèn)是 true钟哥。根據(jù) Flutter 代碼里的注釋迎献,將 Cell 加到 RepaintBoundary 中是為了獲得更好的滾動性能。

<pre class="code-snippet__js" data-lang="cs" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">// Class SliverChildBuilderDelegate``/// Whether to wrap each child in a [RepaintBoundary].``///``/// Typically, children in a scrolling container are wrapped in repaint``/// boundaries so that they do not need to be repainted as the list scrolls.``/// If the children are easy to repaint (e.g., solid color blocks or a short``/// snippet of text), it might be more efficient to not add a repaint boundary``/// and simply repaint the children during scrolling.``///``/// Defaults to true.``final bool addRepaintBoundaries;</pre>

這里腻贰,我們想辦法對特定的 Cell 屏蔽 RepaintBoundary 功能吁恍,添加一個(gè)空的純虛類 NoRepaintBoundaryHint。

<pre class="code-snippet__js" data-lang="cs" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">/// A widget that tells sliver not to create repaint boundary for a cell content.``abstract class NoRepaintBoundaryHint {``}</pre>

并修改 SliverChildBuilderDelegate 和 SliverChildListDelegate 類的 build 方法播演。當(dāng)child 繼承自 NoRepaintBoundaryHint 時(shí)冀瓦,不要添加 RepaintBoundary。

<pre class="code-snippet__js" data-lang="cs" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">if (addRepaintBoundaries && (child is! NoRepaintBoundaryHint)) { child = RepaintBoundary(child: child);``}</pre>

這樣写烤,我們自定義的 Widget 只需要假裝實(shí)現(xiàn)一下 NoRepaintBoundaryHint 接口即可翼闽,這也是本方案唯一需要業(yè)務(wù)層配合修改的地方。

<pre class="code-snippet__js" data-lang="java" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">class MyListItem extends StatefulWidget implements NoRepaintBoundaryHint {``}</pre>

? 添加通知進(jìn)行圖片加載與回收

對于 _ImageState 類洲炊,其會創(chuàng)建 RawImage 組件感局,RawImage 又會創(chuàng)建 RenderImage。對這個(gè)鏈路添加回調(diào)方法暂衡,同時(shí)新建子類 AutoreleaseRawImage 和 AutoreleaseRenderImage询微。

<pre class="code-snippet__js" data-lang="cs" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">/// On drawing image, AutoreleaseRenderImage will notify image moving inside or outside screen event to owner.``typedef SetNeedsImageCallback = void Function(bool value);</pre>

在出屏?xí)r,調(diào)用 SetNeedsImageCallback(false)狂巢,并將各自持有的 ui.Image 置 null撑毛,釋放紋理。
在入屏?xí)r唧领,調(diào)用 SetNeedsImageCallback(true)藻雌,重新請求圖片雌续。代碼大致如下(省略了一部分):

<pre class="code-snippet__js" data-lang="cs" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">// Class _ImageState``void didChangeDependencies() { _updateInvertColors(); if (_releaseImageWhenOutsideScreen) { return; // 如果有標(biāo)記,不再加載圖片蹦疑,等待繪制指令 } .... 請求圖片 super.didChangeDependencies();``}``void __setNeedsImage(bool value) { if (value) { if (_imageStream == null) { 請求圖片 } } else { 清空圖片 }``}``void _setNeedsImage(bool value) { // AutoreleaseRenderImage 回調(diào)該方法 Future<void>(() { __setNeedsImage(value); // 在 paint 過程西雀,不允許 setState,所以需要異步一下 });``}</pre>

? Demo 測試運(yùn)行

在 Demo 中歉摧,每隔十個(gè) Cell 添加一個(gè)大 Cell艇肴,大 Cell 中有十張圖片。代碼如下:

<pre class="code-snippet__js" data-lang="properties" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">Widget build(BuildContext context) { if (widget.index % 10 == 0) { final images = <Widget>[]; for (var i = 0; i < 10; i++) { images.add(new Image.external_adapter( 'https://i.picsum.photos/id/' + (widget.index + i).toString() + '/1000/1000.jpg', height: 375, width: 375, )); } return Column( children: images ); } else { return Container( width: 375, height: 375, child: Text(widget.index.toString()), ); }``}</pre>

在 Demo 中效果非常好叁温,原先滾動到圖片時(shí)再悼,一次性十張圖片全部被加載;修改后膝但,即使十張圖片放在同一個(gè) Cell 里冲九,也一張一張加載并回收。如圖跟束,在底層打印紋理個(gè)數(shù)莺奸,并觀察內(nèi)存占用。

image

? 真實(shí)業(yè)務(wù)場景測試

然而在商品詳情真實(shí)場景冀宴,圖片完全加載不出來灭贷。調(diào)試發(fā)現(xiàn),在 Demo 里我為每個(gè) Image 指定了寬高略贮,Image 可以正常排版甚疟。而在業(yè)務(wù)場景里,解析 HTML 產(chǎn)生的圖片組件逃延,缺少寬高信息览妖,需要等到圖片真正加載完成,RenderImage 才能獲取到圖片尺寸信息并進(jìn)行排版揽祥。

<pre class="code-snippet__js" data-lang="kotlin" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">// Class RenderImage``Size _sizeForConstraints(BoxConstraints constraints) { constraints = BoxConstraints.tightFor( width: _width, // 為 null height: _height, // 為 null ).enforce(constraints); if (_image == null) return constraints.smallest; // 圖片也沒有加載完成時(shí)讽膏,該 Widget 根本沒有尺寸 return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( _image.width.toDouble() / _scale, _image.height.toDouble() / _scale, ));``}</pre>

這里似乎陷入一個(gè)悖論:

  • 圖片不存在,無法排版拄丰,無法顯示桅打。

  • 加載圖片,導(dǎo)致本應(yīng)在屏幕外的圖片紋理全部上傳到 GPU愈案;然后才能完成排版挺尾,再次繪制時(shí)發(fā)現(xiàn)在屏幕外,再刪除紋理站绪。

如果按照這個(gè)流程遭铺,圖片必須完成加載才能排版,優(yōu)化效果大打折扣了。其實(shí)魂挂,排版需要的只是圖片的尺寸甫题,并不需要 GPU 紋理,這里給了我們優(yōu)化的余地涂召。

? 提前獲取圖片尺寸

在 AliFlutter 的圖片方案中坠非,實(shí)現(xiàn)了自定義的 ExternalAdapterImageFrameCodec,它提供的 getNextFrame 接口用于獲取圖片果正,上傳紋理后返回可用的 ui.Image炎码。為了提前獲取圖片尺寸,我們添加一個(gè)接口 getImageInfo秋泳。這個(gè)接口從圖片庫獲取圖片后(比如 UIImage)潦闲,只取其基本信息,并不上傳紋理迫皱。在 _ImageState 中歉闰,判斷 widget 的寬高是否被指定。如果任一個(gè)參數(shù)未被指定卓起,請求圖片時(shí)攜帶參數(shù)和敬,只獲取圖片的基本信息,不上傳紋理戏阅。

<pre class="code-snippet__js" data-lang="java" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">// Class _ImageState``void didChangeDependencies() { if (_releaseImageWhenOutsideScreen) { if (widget.width == null || widget.height == null) { _resolveImage(true); // 只獲取圖片尺寸昼弟,不上傳紋理 _listenToStream(); } } .... 以下略``}``void _handleImageInfo(int width, int height, int frameCount, int durationInMs, int repetitionCount) { setState(() { // 獲取到圖片尺寸后,記錄下來饲握,并更新給 RenderObject _imageWidth = width; _imageHeight = height; });``}</pre>

其中 _resolveImage(true); 告知 ExternalAdapterImageStreamCompleter 調(diào)用 getImageInfo 而不是 getNextFrame 接口私杜。 在獲取到圖片尺寸后蚕键,記錄下來救欧,并通過 setState 告知給 AutoreleaseRenderImage。重寫 AutoreleaseRenderImage 方法的 _sizeForConstraints 方法锣光,處理圖片紋理不存在笆怠,但是圖片的尺寸已經(jīng)得知的場景潘明,保證排版順利進(jìn)行盖袭。這里我們優(yōu)先仍然使用 _image 來獲取寬高,當(dāng) _image 為空時(shí)笨忌,使用上層指定的 _imageWidth 和 _imageHeight 來計(jì)算排版频丘。

<pre class="code-snippet__js" data-lang="kotlin" style="margin: 0px; padding: 1em 1em 1em 0px; max-width: 1000%; box-sizing: border-box !important; overflow-wrap: break-word !important; overflow-x: auto; white-space: normal; -webkit-box-flex: 1; flex: 1 1 0%;">Size _sizeForConstraints(BoxConstraints constraints) { constraints = BoxConstraints.tightFor( width: _width, height: _height, ).enforce(constraints); // No intrinsic from image itself or image pixel dimension info. if (_image == null && (_imageWidth == null || _imageHeight == null)) return constraints.smallest; // Use _image if not null if (_image != null) { return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( _image.width.toDouble() / _scale, _image.height.toDouble() / _scale, )); } // Or else use image dimension info. return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( _imageWidth.toDouble(), _imageHeight.toDouble(), ));``}</pre>

? 進(jìn)一步優(yōu)化

通過給 ExternalAdapterImageFrameCodec 添加 getImageInfo 接口办成,我們可以避免了離屏紋理的上傳。但是因?yàn)閳D片缺乏高度信息搂漠,因此一進(jìn)入頁面時(shí)迂卢,仍然是堆疊在一起,產(chǎn)生了大量圖片請求。這些圖片請求通過外接圖片庫返回 UIImage(或 Android Bitmap) 對象而克,即使沒有上傳成紋理靶壮,仍然是較大的內(nèi)存開銷。商品詳情業(yè)務(wù)的特點(diǎn)是多張圖片拼接而成员萍,我們只能指定圖片的寬度腾降,需要圖片高度自適應(yīng)。因此針對這種場景碎绎,我們給 Flutter 的官方圖片組件添加了一個(gè)給排版用的虛擬尺寸參數(shù)螃壤。

image

根據(jù)詳情業(yè)務(wù)特點(diǎn),指定 Image Widget 的寬度為頁面寬度混卵,虛擬高度與圖片寬度相同映穗。在 ImageWidgetState 的 build 方法中,創(chuàng)建底層的 RenderObject 時(shí)幕随,將這個(gè)虛擬尺寸傳給底層的 RenderObject蚁滋,使圖片獲得一個(gè)大致的排版后的位置。整個(gè)圖片的排版加載邏輯如下:

  1. 當(dāng) Image Widget 擁有確定寬赘淮、高時(shí)辕录,依賴?yán)L制階段的在屏判斷進(jìn)行圖片加載。

  2. 當(dāng) Image Widget 缺失寬梢卸、高信息時(shí)走诞,如果有排版的虛擬尺寸,以這個(gè)虛擬尺寸進(jìn)行預(yù)排版蛤高。排版后首次繪制時(shí)蚣旱,如果在屏,進(jìn)行圖片真正加載戴陡。圖片加載完成后塞绿,如果尺寸與虛擬尺寸不符合,會重新排版恤批。

? 效果

經(jīng)過優(yōu)化后异吻,圖文詳情部分仍然是一個(gè)大 Cell,里面羅列了一系列高度自適應(yīng)的商品圖片喜庞。我們的方案避免了 Cell 首次出現(xiàn)時(shí)诀浪,所有圖片一次性全部加載,導(dǎo)致內(nèi)存突然飆高造成 OOM延都。同時(shí)在列表滾動過程雷猪,同一個(gè) Cell 中的圖片可以按需回收,使內(nèi)存水位保持在合理水平晰房。

image

****總結(jié)****


本文探索出的方案屬于 AliFlutter 提供的外接圖片庫的功能之一求摇。這個(gè)方案保障了淘寶商品圖片詳情這種場景下的穩(wěn)定性酵颁。我們測試發(fā)現(xiàn),使用官方的 Image.network 加載圖片月帝,并且不優(yōu)化大 Cell 場景的話躏惋,一個(gè)較復(fù)雜的商品內(nèi)存可能暴漲到 1GB,幾乎 100% 造成低端機(jī)的 OOM嚷辅。這種情況簿姨,業(yè)務(wù)是完全無法上線的。這個(gè)方案中圖片在屏簸搞、離屏判斷扁位,未來會繼續(xù)和官方人員討論并進(jìn)行優(yōu)化。
https://mp.weixin.qq.com/s/Mcfj3lRR8VJACxsjjIiVsA

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末趁俊,一起剝皮案震驚了整個(gè)濱河市域仇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寺擂,老刑警劉巖暇务,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怔软,居然都是意外死亡垦细,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門挡逼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來括改,“玉大人,你說我怎么就攤上這事家坎≈瞿埽” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵虱疏,是天一觀的道長惹骂。 經(jīng)常有香客問我,道長订框,這世上最難降的妖魔是什么析苫? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任兜叨,我火速辦了婚禮穿扳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘国旷。我一直安慰自己矛物,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布跪但。 她就那樣靜靜地躺著履羞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忆首,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天爱榔,我揣著相機(jī)與錄音,去河邊找鬼糙及。 笑死详幽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浸锨。 我是一名探鬼主播唇聘,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柱搜!你這毒婦竟也來了迟郎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤聪蘸,失蹤者是張志新(化名)和其女友劉穎宪肖,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體健爬,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匈庭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浑劳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阱持。...
    茶點(diǎn)故事閱讀 40,912評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖魔熏,靈堂內(nèi)的尸體忽然破棺而出衷咽,到底是詐尸還是另有隱情,我是刑警寧澤蒜绽,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布镶骗,位于F島的核電站,受9級特大地震影響躲雅,放射性物質(zhì)發(fā)生泄漏鼎姊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一相赁、第九天 我趴在偏房一處隱蔽的房頂上張望相寇。 院中可真熱鬧,春花似錦钮科、人聲如沸唤衫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽佳励。三九已至休里,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赃承,已是汗流浹背妙黍。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞧剖,地道東北人废境。 一個(gè)月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像筒繁,于是被迫代替她去往敵國和親噩凹。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評論 2 361