一篇文章帶你領(lǐng)略Android混淆的魅力

在 Android 日常開發(fā)過程中蜗细,混淆是我們開發(fā) App 的一項必不可少的技能。只要是我們親身經(jīng)歷過 App 打包上線的過程,或多或少都需要了解一些代碼混淆的基本操作毫捣。那么锋谐,混淆到底是什么遍尺?它的好處有哪些?具體效果如何涮拗?別急乾戏,下面我們來一一探索它的"獨特"魅力。

混淆簡介

代碼混淆Obfuscated code)是將程序中的代碼以某種規(guī)則轉(zhuǎn)換為難以閱讀和理解的代碼的一種行為三热。

混淆的好處

混淆的好處就是它的目的:令 APK 難以被逆向工程鼓择,即很大程度上增加反編譯的成本。此外就漾,Android 當(dāng)中的"混淆"還能夠在打包時移除無用資源呐能,顯著減少 APK 體積。最后,還能以變通方式避免 Android 中常見的64k方法數(shù)引用的限制摆出。

我們先來看一下混淆前后的 APK 結(jié)構(gòu)對比:

混淆前

混淆后

從上面兩張圖可以看出:經(jīng)過混淆處理之后朗徊,我們的 APK 中包名、類名偎漫、成員名等都被替換為隨機爷恳、無意義的名稱,增加了代碼閱讀和理解的困難程度象踊,提高了反編譯的成本温亲。細(xì)心的小伙伴可能又會注意到:混淆前后 APK 的體積竟然從 2.7M 減小到了 1.4M,體積縮減了近一倍杯矩!真的有這么神奇嗎栈虚?哈哈,確實是這么神奇史隆,讓我們慢慢來揭開它的神秘面紗吧魂务。

Android 當(dāng)中的混淆

在 Android 中,我們平常所說的"混淆"其實有兩層意思逆害,一個是 Java代碼的混淆头镊,另外一個是資源的壓縮。其實這兩者之間并沒有什么關(guān)聯(lián)魄幕,只不過習(xí)慣性地放在一起來使用相艇。那么,說了這么多纯陨,Android 平臺上到底該如何開啟混淆呢坛芽?

啟用混淆

......? android {? ? buildTypes {? ? ? ? release {? ? ? ? ? ? minifyEnabled true? ? ? ? ? ? shrinkResources true? ? ? ? ? ? proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'? ? ? ? }? ? }}

以上就是開啟混淆的基本操作了,通過minifyEnabled設(shè)置為true來開啟混淆翼抠。同時咙轩,可以設(shè)置shrinkResources為true來開啟資源的壓縮。不難看出阴颖,我們一般在打 release 包時才啟用混淆活喊,因為混淆會增加額外的編譯時間,所以不建議在 debug 模式下啟用量愧。此外钾菊,需要注意的是:只有在啟用混淆的前提下開啟資源壓縮才會有效!以上代碼中的proguard-android.txt表示 Android 系統(tǒng)為我們提供的默認(rèn)混淆規(guī)則文件偎肃,而proguard-rules.pro則是我們想要自定義的混淆規(guī)則煞烫,至于如何自定義混淆規(guī)則我們將在接下來會講到。

代碼混淆

其實累颂,Java 平臺為我們提供了Proguard混淆工具來幫助我們快速地對代碼進(jìn)行混淆滞详。根據(jù) Java 官方介紹,Proguard 對應(yīng)的具體中文定義如下:

它是一個包含代碼文件壓縮優(yōu)化料饥、混淆校驗等功能的工具

它能夠檢測并刪除無用的類蒲犬、變量、方法和屬性

它能夠優(yōu)化字節(jié)碼并刪除未使用的指令

它能夠?qū)㈩愊』稹⒆兞亢头椒ǖ拿种孛麨闊o意義的名稱從而達(dá)到混淆效果

最后暖哨,它還會校驗處理后的代碼赌朋,主要針對 Java 6 及以上版本和 Java ME

資源壓縮

Android 中凰狞,編譯器為我們提供了另外一項強大的功能:資源的壓縮。資源壓縮能夠幫助我們移除項目及依賴倉庫中未使用到的資源沛慢,有效地降低了apk包的大小赡若。由于資源壓縮與代碼混淆是協(xié)同工作,所以团甲,如果需要開啟資源的壓縮逾冬,切記要先開啟代碼混淆,否則會出現(xiàn)以下問題:

ERROR:Removingunused resources requires unused code shrinking to be turned on.Seehttp://d.android.com/r/tools/shrink-resources.htmlformore information.AffectedModules:app

自定義要保留的資源

當(dāng)我們開啟了資源壓縮之后躺苦,系統(tǒng)會默認(rèn)替我們移除所有未使用的資源身腻,假如我們需要保留某些特定的資源,可以在我們項目中創(chuàng)建一個被<resources>標(biāo)記的 XML 文件(如res/raw/keep.xml)匹厘,并在tools:keep屬性中指定每個要保留的資源嘀趟,在tools:discard屬性中指定每個要舍棄的資源。這兩個屬性都接受逗號分隔的資源名稱列表愈诚。同樣她按,我們可以使用字符*作為通配符。如:

<?xml version="1.0" encoding="utf-8"?><resourcesxmlns:tools="http://schemas.android.com/tools"tools:keep="@layout/activity_video*,@layout/dialog_update_v2"tools:discard="@layout/unused_layout,@drawable/unused_selector"/>

啟用嚴(yán)格檢查模式

正常情況下炕柔,資源壓縮器可準(zhǔn)確判定系統(tǒng)是否使用了資源酌泰。不過,如果您的代碼(包含庫)調(diào)用Resources.getIdentifier()匕累,這就表示您的代碼將根據(jù)動態(tài)生成的字符串查詢資源名稱陵刹。這時,資源壓縮器會采取防御性行為欢嘿,將所有具有匹配名稱格式的資源標(biāo)記為可能已使用衰琐,無法移除。例如际插,以下代碼會使所有帶img_前綴的資源標(biāo)記為已使用:

Stringname=String.format("img_%1d",angle+1);res=getResources().getIdentifier(name,"drawable",getPackageName());

這時碘耳,我可以開啟資源的嚴(yán)格審查模式,只會保留確定已使用的資源框弛。

移除備用資源

Gradle 資源壓縮器只會移除未被應(yīng)用引用的資源辛辨,這意味著它不會移除用于不同設(shè)備配置的備用資源。必要時,我們可以使用 Android Gradle 插件的resConfigs屬性來移除您的應(yīng)用不需要的備用資源文件(常見的有用于國際化支持的strings.xml斗搞,適配用的layout.xml等):

android {? ? defaultConfig {? ? ? ? ...? ? ? ? //保留中文和英文國際化支持? ? ? ? resConfigs "en", "zh"? ? }}

自定義混淆規(guī)則

品嘗完了以上"配菜"指攒,下面讓我們來品味一下本文的"主菜":自定義混淆規(guī)則。首先僻焚,我們來了解一下常見的混淆命令允悦。

keep 命令

這里說的keep命令指的是一系列以-keep開頭的命令,它主要用來保留 Java 中不需要進(jìn)行混淆的元素虑啤。以下是常見的 -keep 命令:

-keep

作用:保留指定的類和成員隙弛,防止被混淆處理。例如:

# 保留包:com.moos.media.entity 下面的類以及類成員-keeppublicclasscom.moos.media.entity.**# 保留類:NumberProgressBar-keeppublicclasscom.moos.media.widget.NumberProgressBar{*;}

-keepclassmembers

作用:保留指定的類的成員(變量/方法)狞山,它們將不會被混淆全闷。如:

# 保留類的成員:MediaUtils類中的特定成員方法-keepclassmembersclasscom.moos.media.MediaUtils{publicstatic***getLocalVideos(android.content.Context);publicstatic***getLocalPictures(android.content.Context);}

-keepclasseswithmembers

作用:保留指定的類和其成員(變量/方法),前提是它們在壓縮階段沒有被刪除萍启。與-keep使用方式類似:

# 保留類:BaseMediaEntity 的子類-keepclasseswithmemberspublicclass*extendscom.moos.media.entity.BaseMediaEntity{*;}# 保留類:OnProgressBarListener接口的實現(xiàn)類-keeppublicclass*implementscom.moos.media.widget.OnProgressBarListener{*;}

@Keep

除了以上方式总珠,你也可以選擇使用@Keep注解來保留期望代碼,防止它們被混淆處理勘纯。比如局服,我們通過@Keep修飾一個類來保留它不被混淆:

@KeepdataclassCloudMusicBean(varcreateDate:String,varid:Long,varname:String,varurl:String,valimgUrl:String)

同樣地,我們也可以讓@Keep來修飾方法或者字段進(jìn)而保留它們驳遵。

其他命令

dontwarn

-dontwarn命令一般在我們引入新的 library 時會使用到淫奔,常用于處理 library 中無法解決的警告。如:

-keepclasstwitter4j.**{*;}-dontwarn twitter4j.**

其他的命令用法可參考 Android 系統(tǒng)提供的默認(rèn)混淆規(guī)則:

#混淆時不生成大小寫混合的類名-dontusemixedcaseclassnames#不跳過非公共的庫的類-dontskipnonpubliclibraryclasses#混淆過程中記錄日志-verbose#關(guān)閉預(yù)校驗-dontpreverify#關(guān)閉優(yōu)化-dontoptimize#保留注解-keepattributes*Annotation*#保留所有擁有本地方法的類名及本地方法名-keepclasseswithmembernamesclass*{native<methods>;}#保留自定義View的get和set方法-keepclassmemberspublicclass*extends android.view.View{voidset*(***);***get*();}#保留Activity中View及其子類入?yún)⒌姆椒ǔ瘢?onClick(android.view.View)-keepclassmembersclass*extends android.app.Activity{publicvoid*(android.view.View);}#保留枚舉-keepclassmembersenum*{**[]$VALUES;public*;}#保留序列化的類-keepclassmembersclass*implements android.os.Parcelable{publicstaticfinalandroid.os.Parcelable$Creator CREATOR;}#保留R文件的靜態(tài)成員-keepclassmembersclass**.R$*{publicstatic<fields>;}-dontwarn android.support.**-keepclassandroid.support.annotation.Keep-keep@android.support.annotation.Keepclass*{*;}-keepclasseswithmembersclass*{@android.support.annotation.Keep<methods>;}-keepclasseswithmembersclass*{@android.support.annotation.Keep<fields>;}-keepclasseswithmembersclass*{@android.support.annotation.Keep<init>(...);}

更多混淆命令可以參考文章:Proguard 最全混淆規(guī)則說明搏讶,這里就不做詳細(xì)講解了。

混淆"黑名單"

我們在了解了混淆的基本命令之后霍殴,很多人應(yīng)該還是一頭霧水:到底哪些內(nèi)容該混淆媒惕?其實,我們在使用代碼混淆時来庭,ProGuard 對我們項目中大部分代碼進(jìn)行了混淆操作妒蔚,為了防止編譯時出錯,我們應(yīng)該通過keep命令保留一些元素不被混淆月弛。所以肴盏,我們只需要知道哪些元素不應(yīng)該被混淆

枚舉

項目中難免可能會用到枚舉類型,然而它不能參與到混淆當(dāng)中去帽衙。原因是:枚舉類內(nèi)部存在values方法菜皂,混淆后該方法會被重新命名,并拋出NoSuchMethodException厉萝。慶幸的是恍飘,Android 系統(tǒng)默認(rèn)的混淆規(guī)則中已經(jīng)添加了對于枚舉類的處理榨崩,我們無需再去做額外工作。想了解更多枚舉內(nèi)部細(xì)節(jié)可以去查看源碼章母,篇幅有限不再細(xì)說母蛛。

被反射的元素

被反射使用的類、變量乳怎、方法彩郊、包名等不應(yīng)該被混淆處理。原因在于:代碼混淆過程中蚪缀,被反射使用的元素會被重命名秫逝,然而反射依舊是按照先前的名稱去尋找元素,所以會經(jīng)常發(fā)生NoSuchMethodException和NoSuchFiledException問題椿胯。

實體類

實體類即我們常說的"數(shù)據(jù)類"筷登,當(dāng)然經(jīng)常伴隨著序列化反序列化操作剃根。很多人也應(yīng)該都想到了哩盲,混淆是將原本有特定含義的"元素"轉(zhuǎn)變?yōu)闊o意義的名稱,所以狈醉,經(jīng)過混淆的"洗禮"之后廉油,序列化之后的value對應(yīng)的key已然變?yōu)闆]有意義的字段,這肯定是我們不希望的苗傅。同時抒线,反序列化的過程創(chuàng)建對象從根本上來說還是借助于反射,混淆之后key會被改變渣慕,所以也會違背我們預(yù)期的效果嘶炭。

四大組件

Android 中的四大組件同樣不應(yīng)該被混淆。原因在于:

四大組件使用前都需要在AndroidManifest.xml文件中進(jìn)行注冊聲明逊桦,然而混淆處理之后眨猎,四大組件的類名就會被篡改,實際使用的類與manifest中注冊的類并不匹配强经,故而出錯睡陪。

其他應(yīng)用程序訪問組件時可能會用到類的包名加類名,如果經(jīng)過混淆匿情,可能會無法找到對應(yīng)組件或者產(chǎn)生異常兰迫。

JNI 調(diào)用的Java 方法

當(dāng) JNI 調(diào)用的 Java 方法被混淆后,方法名會變成無意義的名稱炬称,這就與 C++ 中原本的 Java 方法名不匹配汁果,因而會無法找到所調(diào)用的方法。

其他不應(yīng)該被混淆的

自定義控件不需要被混淆

JavaScript 調(diào)用 Java 的方法不應(yīng)混淆

Java 的 native 方法不應(yīng)該被混淆

項目中引用的第三方庫也不建議混淆

混淆后的堆棧跟蹤

代碼經(jīng)過 ProGuard 混淆處理后玲躯,想要讀取StackTrace(堆棧追蹤)信息就會變得很困難据德。由于方法名稱和類的名稱都經(jīng)過混淆處理鲸伴,即使程序發(fā)生崩潰問題,也很難定位問題所在晋控。幸運的是汞窗,ProGuard 為我們提供了補救的措施,在著手進(jìn)行之前赡译,我們先來看一下 ProGuard 每次構(gòu)建后生成了哪些內(nèi)容仲吏。

混淆輸出結(jié)果

混淆構(gòu)建完成之后,會在<module-name>/build/outputs/mapping/release/目錄下生成以下文件:

dump.txt

說明 APK 內(nèi)所有類文件的內(nèi)部結(jié)構(gòu)蝌焚。

mapping.txt

提供混淆前后的內(nèi)容對照表裹唆,內(nèi)容主要包含類、方法和類的成員變量只洒。

seeds.txt

羅列出未進(jìn)行混淆處理的類和成員许帐。

usage.txt

羅列出從 APK 中移除的代碼。

恢復(fù)堆棧跟蹤

了解完混淆構(gòu)建完畢后輸出的內(nèi)容之后毕谴,我們現(xiàn)在就來看一下之前的問題:混淆處理后成畦,StackTrace 定位困難。如何來恢復(fù) StackTrace 的定位能力呢涝开?系統(tǒng)為我們提供了retrace工具循帐,結(jié)合上文提到的mapping.txt文件,就可以將混淆后的崩潰堆棧追蹤信息還原成正常情況下的StackTrace信息舀武。主要有兩種方式來恢復(fù) StackTrace拄养,為了方便理解,我們以下面這段崩潰信息為例银舱,借助兩種方式分別來還原:

java.lang.RuntimeException:Unableto start activityCausedby:kotlin.KotlinNullPointerExceptionat com.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71)at com.moos.media.ui.ImageSelectActivity.onCreate(ImageSelectActivity.kt:58)at android.app.Activity.performCreate(Activity.java:6237)at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)

通過 retrace 腳本工具

首先我們要進(jìn)入到 Android SDK 路徑的/tools/proguard/bin目錄中瘪匿,這里以 Mac 系統(tǒng)為例,可以看到如下內(nèi)容:

retrace腳本目錄

可以看到如上三個文件寻馏,而proguardgui.sh才是我們需要的retrace腳本(Windows系統(tǒng)下為proguardgui.bat)棋弥。Windows 系統(tǒng)中只需要雙擊腳本proguardgui.bat即可運行,至于 Mac 系統(tǒng)操软,如果你沒有做任何配置嘁锯,只需要將proguardgui.sh腳本拖動到 Mac 自帶的終端中,回車鍵即可運行聂薪。接著家乘,我們會看到如下界面:

retrace腳本界面

選擇ReTrace欄 ,并添加我們項目中混淆生成的mapping.txt文件所在位置藏澳,然后將我們的混淆后的崩潰信息復(fù)制到Obfuscated stack trace那一欄仁锯,點擊ReTrace!按鈕即可還原出我們的崩潰日志信息,結(jié)果如上圖所示翔悠,我們之前的混淆日志:at com.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71)被還原成了at com.moos.media.ui.ImageSelectActivity.initView(ImageSelectActivity.kt:71)业崖。ImageSelectActivity.k是我們混淆后的方法名野芒,ImageSelectActivity.initView則是最初未混淆前的方法名,借助于 ReTrace 工具的幫助双炕,我們就可以像以前一樣很快定位到崩潰代碼區(qū)域了狞悲。

通過 retrace 命令行

我們先要將崩潰信息復(fù)制到txt格式的文件(如:proguard_stacktrace.txt)中保存,然后執(zhí)行以下命令即可(MAC系統(tǒng)):

retrace.sh -verbose mapping.txt proguard_stacktrace.txt

如果你是 windows 系統(tǒng)妇斤,可以執(zhí)行以下命令:

retrace.bat -verbose mapping.txt proguard_stacktrace.txt

最終還原的結(jié)果和之前效果一樣:

命令行還原stacktrace

也許你通過以上兩種方式在對 stackTrace 進(jìn)行恢復(fù)時摇锋,發(fā)現(xiàn)Unknown Source問題:

資源壓縮注意點.png

值得注意的是,記得在混淆規(guī)則中加上如下配置來提升我們的 StackSource 查找效率:

# 保留源文件名和具體代碼行號-keepattributes SourceFile,LineNumberTable

此外站超,我們每次使用 ProGuard 創(chuàng)建發(fā)布構(gòu)建時都都會覆蓋之前版本的mapping.txt文件荸恕,因此我們每次發(fā)布新版本時都必須小心地保存一個副本。通過為每個發(fā)布構(gòu)建保留一個mapping.txt文件副本死相,我們就可以在用戶提交的已混淆的 StackTrace 來對舊版本應(yīng)用的問題進(jìn)行調(diào)試和修復(fù)融求。

漲姿勢的操作

經(jīng)過上文的介紹,我們知道算撮,APK 在經(jīng)過代碼混淆處理后生宛,包名、類名钮惠、成員名被轉(zhuǎn)化為無意義茅糜、難以理解的名稱,增加反編譯的成本素挽。Android ProGuard 為我們提供了默認(rèn)的"混淆字典",即將元素名稱轉(zhuǎn)為英文小寫字母的形式狸驳。那么预明,我們可以定義自己的混淆字典嗎?賣個關(guān)子耙箍,我們先來看一張效果圖:

自定義混淆字典效果

這個波操作是不是有點"出類拔萃"了撰糠?哈哈,就不賣關(guān)子了辩昆,其實很簡單阅酪,只要生成一套自己的txt格式的混淆字典,然后在混淆規(guī)則Proguard-rules.pro中應(yīng)用一下即可:

混淆字典配置

本文中使用的混淆字典可以在此處查看并下載:proguard_tradition.txt

當(dāng)然汁针,大家也可以自己去定制化自己的"混淆字典"术辐,增加反編譯的難度。

一路走下來施无,我們發(fā)現(xiàn)辉词,從混淆技術(shù)的必要性和優(yōu)點來看,它還是很值得我們?nèi)ド钊雽W(xué)習(xí)和研究的猾骡,本文帶大家領(lǐng)略的僅僅是"冰山一角"瑞躺。由于本人的技術(shù)水平有限敷搪,若大家發(fā)現(xiàn)有問題或者闡述不當(dāng)之處,歡迎指出并修正幢哨。

相關(guān)參考

Shrink your app

讀懂Android中的代碼混淆

Practical ProGuard rules example

Android ProGuard 代碼混淆那些事兒

Proguard 最全混淆規(guī)則說明

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赡勘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子捞镰,更是在濱河造成了極大的恐慌狮含,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件曼振,死亡現(xiàn)場離奇詭異几迄,居然都是意外死亡,警方通過查閱死者的電腦和手機冰评,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門映胁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甲雅,你說我怎么就攤上這事解孙。” “怎么了抛人?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵弛姜,是天一觀的道長。 經(jīng)常有香客問我妖枚,道長廷臼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任绝页,我火速辦了婚禮荠商,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘续誉。我一直安慰自己莱没,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布酷鸦。 她就那樣靜靜地躺著饰躲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪臼隔。 梳的紋絲不亂的頭發(fā)上嘹裂,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音躬翁,去河邊找鬼焦蘑。 笑死,一個胖子當(dāng)著我的面吹牛盒发,可吹牛的內(nèi)容都是我干的例嘱。 我是一名探鬼主播狡逢,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拼卵!你這毒婦竟也來了奢浑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤腋腮,失蹤者是張志新(化名)和其女友劉穎雀彼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體即寡,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡徊哑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了聪富。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莺丑。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖墩蔓,靈堂內(nèi)的尸體忽然破棺而出梢莽,到底是詐尸還是另有隱情,我是刑警寧澤奸披,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布昏名,位于F島的核電站,受9級特大地震影響阵面,放射性物質(zhì)發(fā)生泄漏轻局。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一膜钓、第九天 我趴在偏房一處隱蔽的房頂上張望嗽交。 院中可真熱鬧,春花似錦颂斜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梅肤,卻和暖如春司蔬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姨蝴。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工俊啼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人左医。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓授帕,卻偏偏與公主長得像同木,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子跛十,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354