Replugin插件化框架簡要分析

題記

寫這篇關(guān)于Replugin插件化框架的分析,旨在引導(dǎo)讀者去快速的了解RePlugin的大概實(shí)現(xiàn)原理,文中會拋出需要了解的知識點(diǎn),并明確的指出大致的流程,指引你去更快速的理解它,避免走過多彎路。因?yàn)镽eplugin的源碼中文注釋已經(jīng)夠詳細(xì)了,這里我不貼源碼,節(jié)省讀者的閱讀時間,需要具體了解的對照著看源碼,想必會更加清晰。同時,想要看具體的Replugin的實(shí)現(xiàn)原理詳細(xì)說明,推薦Replugin 全面解析,講解的很不錯。

插件化的好處

對用戶來說:

  1. 一切按需,按需加載,減少內(nèi)存,存儲的消耗。做到小而精。
  2. 隨時體驗(yàn)新版,不需要去應(yīng)用市場更新應(yīng)用,隨時升級更新。
    對開發(fā)者來說:
  3. 隨時發(fā)版
  4. 組織結(jié)構(gòu)靈活,模塊之間開發(fā)獨(dú)立性強(qiáng)

Replugin插件化框架的優(yōu)勢

它屬于占坑類插件化方案,相比其他插件,它只hook了ClassLoader,最大程度的保證了穩(wěn)定性,兼容性,和可維護(hù)性。它的具體優(yōu)勢參考全面插件化:RePlugin的使命
Replugin只hook ClassLoader,它的原理都是圍繞它進(jìn)行展開。

Replugin項(xiàng)目結(jié)構(gòu)

Replugin有4個相關(guān)項(xiàng)目。

  • replugin-host-gradle 作用于宿主項(xiàng)目的腳本項(xiàng)目
  • replugin-host-library 作用于宿主項(xiàng)目的依賴庫
  • replugin-plugin-gradle 作用于插件項(xiàng)目的腳本項(xiàng)目
  • replugin-plugin-library 作用于插件項(xiàng)目的依賴庫

RePlugin框架的基本原理

其實(shí)就是通過占坑替換的方式,欺騙AMS,來完成組件的啟動。例如Activity的啟動。
1.應(yīng)用打包時,replugin-host-gradle 這個宿主項(xiàng)目的gradle插件項(xiàng)目會在編譯時自動將一些坑位Activity寫入注冊到AndroidManifest.xml中。
2.我們在啟動插件的ActivityA時,Replugin會將它替換成其中的一個坑位Activity,讓系統(tǒng)啟動坑位Activity。
3.坑位Activity在AMS認(rèn)證通過后,通知ActivityThread加載和初始化這個坑位Activity,準(zhǔn)備開始調(diào)用它的生命周期。
4.而在加載的這個過程中,我們的RepluginClassLoader偷偷的轉(zhuǎn)去加載該坑位Activity對應(yīng)的目標(biāo)Activity,然后交給ActivityThread繼續(xù)初始化和調(diào)用生命周期。這樣AMS以為它啟動的是坑位Activity,而ActivityThread這一邊實(shí)際啟動的是插件的目標(biāo)Activity,這樣就完成了加載啟動插件Activity的功能了。

ClassLoader相關(guān)

Replugin圍繞著hook相關(guān)的ClassLoader來進(jìn)行插件化工作,有必要了解ClassLoader相關(guān)。
ClassLoader類加載器采用雙親代理模型的加載方式。這里主要注意的是PathClassLoader和DexClassLoader。

  • PathClassLoader 是應(yīng)用啟動時創(chuàng)建的,只能加載內(nèi)部dex。
  • DexClassLoader 可以加載外部的dex。
    Replugin中存在兩個主要的ClassLoader:
  1. RePluginClassLoader: 宿主App中的ClassLoader,繼承PathClassLoader,也是唯一Hook住系統(tǒng)的Loader。RePluginClassLoader是在Replugin.attachBaseContext開始初始化的,接著進(jìn)入PMF.init,然后是PatchClassLoaderUtils.patch,在這里面,獲取了宿主APP的Application的Context上下文,然后反射獲取Context的mPackageInfo(PackageInfo),接著再反射獲取PackageInfo中的mClassLoader(PathClassLoader),通過反射替換這個mClassLoader為RePluginClassLoader,這樣,后期的類加載就由它取代負(fù)責(zé)了。
  2. PluginDexClassLoader: 用于加載插件中類的ClassLoader,繼承DexClassLoader。PluginDexClassLoader主要是在Loader.loadDex中進(jìn)行初始化的,并且在這里填充到創(chuàng)建的PluginContext中,這樣插件中的context.getClassLoader獲取的就是PluginDexClassLoader了。當(dāng)加載插件中的一個Activity類時,是由RepluginClassLoader.loadClass進(jìn)入到PM.loadClass,再進(jìn)入PmBase.loadClass,這里會加載Plugin,獲取Plugin的ClassLoader,也就是PluginDexClassLoader,去加載這個插件Activity類了。

RePlugin的初始化

  1. RePlugin的初始化是從宿主APP的Application的attachBaseContext開始的。這里的Application會在編譯時轉(zhuǎn)換成繼承自RePluginApplication,所以就進(jìn)入到RePluginApplication的attachBaseContext。
  2. 然后進(jìn)入到RePlugin.App.attachBaseContext,進(jìn)行真正的初始化工作。主要做了初始化IPC進(jìn)程信息,獲取宿主RePluginHostConfig配置信息,調(diào)用PMF.init來初始化PmBase和hook系統(tǒng)的ClassLoader為RepluginCLassLoader,調(diào)用PMF.callAttach來為所有插件信息填充一些數(shù)據(jù),加載默認(rèn)插件。
  3. 看PMF的init實(shí)現(xiàn),初始化了PmBase,內(nèi)部會初始化PluginProcessPer,PluginCommImpl,PluginLibraryInternalProxy,然后調(diào)用init去做初始化工作,其中分為initForServer和initForClient,如果是常駐進(jìn)程,走initForServer,如果是非常駐進(jìn)程,走initClient(正常情況-常駐進(jìn)程存在的情況下)。
  • initForServer做服務(wù)端即persistent的初始化工作,有創(chuàng)建PmHostSvc,PluginProcessMain.installHost(mHostSvc)去緩存自己的IPluginHost,還有Builder.builder掃描出一系列插件信息PluginInfo,保存在PxAll中,然后根據(jù)這些插件信息構(gòu)建出相應(yīng)的插件對象Plugin緩存起來,更新所有的插件信息。
  • initForClient做Client即UI或者第三方進(jìn)程的初始化工作,這里通過PluginProcessMain.connectToHostSvc去連接常駐進(jìn)程,并從常駐進(jìn)程中獲取所有插件信息,并更新緩存起來。
  1. 繼續(xù)看PMF的callAttach實(shí)現(xiàn),為所有插件Plugin填充一些內(nèi)容,并加載默認(rèn)插件,在callAppLocked加載插件時,會創(chuàng)建或獲取代表插件Application的PluginApplicationClient,以便主動調(diào)用去模擬插件中Application的attach,onCreate生命周期。

RePlugin解析manifest文件

從ManifestParser的parse開始,解析當(dāng)前插件的AndroidManifest.xml字符串內(nèi)容,然后到parseManifest,這里采用sax解析方式,傳入XmlHandler來處理解析結(jié)果,可以得到AndroidManifest.xml中各個節(jié)點(diǎn)的的信息。

插件中的Context是什么東東?

插件在編譯打包時,會將注冊的這些Activity等轉(zhuǎn)換成繼承PluginActivity,當(dāng)系統(tǒng)執(zhí)行Activity的attachBaseContext時,就會調(diào)用插件中的PluginActivity中attachBaseContext,這里會創(chuàng)建PluginContent,并替換了默認(rèn)的Context。所以插件中所獲取的Context都是該插件的PluginContext.當(dāng)然,包括Application中的Context也是一樣的道理。

插件中Activity的實(shí)現(xiàn)

  1. 我們知道當(dāng)調(diào)用context.startActivity時,它其實(shí)是走的PluginContext.startActivity,然后會調(diào)用Factory2.startActivity。
  2. PluginContext.startActivity這個方法會執(zhí)行兩次,第一次是正常外部的調(diào)用,這次調(diào)用做的操作是找到intent符合對應(yīng)的坑位Activity,再次執(zhí)行PluginContext.startActivity方法,這樣就進(jìn)入到了第二次調(diào)用,第二次調(diào)用,就直接走系統(tǒng)的context.startActivity流程,啟動坑位Activity。
  3. 我們主要分析第一次的PluginContext.startActivity,進(jìn)入Factory2.startActivity,然后是PluginLibraryInternalProxy.startActivity,然后是Factory.startActivityWithNoInjectCN,然后是PluginCommImpl.startActivity,然后是PluginLibraryInternalProxy.startActivity。
  4. 在PluginLibraryInternalProxy.startActivity中,會調(diào)用PluginCommImpl.loadPluginActivity去加載插件的Activity,啟動插件進(jìn)程,調(diào)用遠(yuǎn)程接口為其分配坑位Activity,然后調(diào)用PluginContext.startActivity進(jìn)入到第二次的正常啟動坑位Activity流程。
  5. 現(xiàn)在坑位Activity就啟動起來了。

插件中Activity坑位分配的原理

坑位Activity的分配是在PluginProcessPer.allocActivityContainer中開處理的,接著進(jìn)入bindActivity,會調(diào)用PluginContainers對象mACM的alloc或者alloc2來開始具體的分配。alloc為插件的目標(biāo)Activity指定在宿主進(jìn)程時的坑位分配策略,alloc2為插件的目標(biāo)Activity在自定義進(jìn)程時的坑位分配策略。

為什么需要坑位Activity來代替目標(biāo)Activity?

因?yàn)椴寮械腁ctivity是沒有注冊到宿主APP的AndroidManifest.xml中的,這樣啟動這個插件Activity的話,系統(tǒng)是不認(rèn)識的,會報(bào)錯。所以為了繞過這個限制,就RePlugin就通過replugin-host-gradle插件預(yù)先在APP編譯時,將一些坑位Activity寫入到宿主APP的AndroidManifest.xml中,這些坑位Activity就作為后面啟動插件Activity的代理了。根據(jù)Activity不同屬性,會內(nèi)置不同的坑位Activity,以滿足不同Activity的需求。

插件中坑位Activity啟動后,是如何啟動插件中的目標(biāo)Activity的?

當(dāng)坑位Activity啟動時,ActivityManagerService這邊對坑位Activity驗(yàn)證通過并記錄了,并且告知ActivityThread這邊啟動這個Activity實(shí)例,這樣就會調(diào)用ClassLoader去加載這個坑位Activity了,因?yàn)槲覀兺低堤鎿Q這個ClassLoader為我們的PluginClassLoader,這樣loadClass方法就進(jìn)入到PluginClassLoader.loadClass了,在里面,調(diào)用了PMF.loadClass,接著是PmBase.loadClass,在這里,會從mContainerActivities中查找當(dāng)前類名是否存在其中,也就是查找這個坑位Activity是否已經(jīng)記錄過了的,如果是,就調(diào)用PluginProcessPer.resolveActivityClass去找到該坑位Activity對應(yīng)的目標(biāo)Activity,并且加載這個目標(biāo)Activity,這樣,就實(shí)現(xiàn)了坑位Activity變換成加載目標(biāo)Activity的邏輯了。同樣,對于Service,ContentProvider也是一樣的道理。

插件中的Activity和PluginActivity有什么關(guān)系?

總的來說是繼承關(guān)系,我們在插件中寫的Activity,比如ActivityA,它繼承自Activity,在編譯時,replugin-host-gradle插件會自動將ActivityA替換為繼承自PluginActivity,這樣ActivityA就具有PluginActivity的功能了,同理繼承自AppCompatActivity會轉(zhuǎn)換為繼承PluginAppCompatActivity??梢钥吹絇luginActivity內(nèi)部是交給RePluginInternal來處理的,

插件中Service服務(wù)的實(shí)現(xiàn)

  1. 我們知道當(dāng)調(diào)用context.startService時,也就會調(diào)用PluginContext的startService,接著進(jìn)入PluginServiceClient的startService。
  2. PluginServiceClient的startService中,將intent中的component設(shè)置為插件中的component信息。然后取得IPluginServiceServer接口,這里如果是在persistent進(jìn)程,則直接通過PluginProcessMain.getPluginHost獲取IPluginHost接口(即PmHostSvc),再從IPluginHost獲取IPluginServiceServer接口(即PluginServiceServer),如果是非persistent進(jìn)程,則要先通過MP.startPluginProcess啟動插件進(jìn)程,獲取IPluginClient接口(即PluginProcessPer),然后再調(diào)用IPluginClient的fetchServiceServer獲取IPluginServiceServer接口(也是PluginServiceServer)。
  3. 取得了IPluginServiceServer接口后,調(diào)用startService就可以通知通知遠(yuǎn)端的PluginServiceServer接收了,真實(shí)的遠(yuǎn)端是PluginServiceServer的內(nèi)部類Stub,繼承自IPluginServiceServer.Stub,是binder的服務(wù)端,這里就會調(diào)用PluginServiceServer的startServiceLocked。
  4. 服務(wù)端PluginServiceServer的startServiceLocked會調(diào)用installServiceIfNeededLocked,接著進(jìn)入installServiceLocked,這里就會通過反射加載插件的該Service對象,將pluginContext注入到Service的Context中,并且主動調(diào)用onCreate,模擬Service的生命周期。然后再通過系統(tǒng)調(diào)用真正的啟動一個坑位Service,防止當(dāng)前的進(jìn)程容易被殺(因?yàn)檫M(jìn)程有活動的Service的話,被殺死的概率會低一些)。
  5. 后面在startServiceLocked中會通過Handler發(fā)送一個執(zhí)行OnStart的消息去執(zhí)行Service的OnStart生命周期回調(diào)。
  6. 到這里,插件的Service所在的進(jìn)程也啟動了,插件的Service也加載并反射實(shí)例化了,并且也模擬執(zhí)行了OnCreate,OnStart生命周期,同時對應(yīng)的坑Service也真正啟動了,整個Service也就啟動工作了。

插件中自定義進(jìn)程的啟動流程

  1. 自定義進(jìn)程的啟動是從PM的startPluginProcess開始,然后是取得IPluginHost接口,調(diào)用得IPluginHost接口的startPluginProcess。
  2. 通過binder,會調(diào)用到PmHostSvc(IPluginHost接口的服務(wù)端實(shí)現(xiàn))的startPluginProcess,接著進(jìn)入PmBase的startPluginProcessLocked。
  3. 入PmBase的startPluginProcessLocked中會調(diào)用PluginProviderStub.proxyStartPluginProcess啟動插件進(jìn)程,它的實(shí)現(xiàn)是找到這個進(jìn)程對應(yīng)的ContentProvider,然后構(gòu)造這個ContentProvider對應(yīng)的uri,這樣就會啟動這個ContentProvider,也就會同時啟動這個進(jìn)程了。

插件中廣播的實(shí)現(xiàn)

廣播的注冊

  1. 從Loader的regReceivers開始,遠(yuǎn)程調(diào)用IPluginHost接口的regReceiver,經(jīng)過binder傳輸之后,調(diào)用到Persistent進(jìn)程中的PmHostSvc的regReceiver(PmHostSvc繼承自IPluginHost.Stub,是一個binder對象,實(shí)現(xiàn)了IPluginHost接口)。
  2. 的PmHostSvc的regReceiver中,會創(chuàng)建一個PluginReceiverProxy(繼承自BroadcastReceiver),它其實(shí)就是一個廣播,然后遍歷參數(shù)傳來的廣播對應(yīng)的IntentFilter列表的Map,將這些都添加到這個PluginReceiverProxy廣播中去,這樣這個代理廣播就能收到這個插件中所有靜態(tài)注冊的廣播的消息了。這里同時還有針對action,來所有插件的廣播接收器按action進(jìn)行分組記錄起來,以便后面接收某個action廣播時,能迅速找到對應(yīng)action的所有插件中符合的廣播來接收了。

廣播的接收

  1. 那廣播接收就是由個PluginReceiverProxy這個代理廣播來接收了,因?yàn)樗攀钦嬲缘较到y(tǒng)中的廣播,系統(tǒng)只認(rèn)它,看PluginReceiverProxy的onReceive,先取得action,然后根據(jù)action找到符合條件的所有廣播接收器(所有插件中的),然后遍歷處理。
  2. 遍歷中,會取得該廣播接收器所在的進(jìn)程,如果是在persistent進(jìn)程,則獲取遠(yuǎn)程接口IPluginHost即PluginProcessPer,調(diào)用它的onReceive處理,然后交給PluginReceiverHelper的onPluginReceiverReceived處理。這里,會去到當(dāng)前插件的Context即PluginContext,進(jìn)而得到該插件的ClassLoader即DexPluginClassLoader,用它去加載當(dāng)前廣播接收器,然后反射實(shí)例化這個對象,如果之前有緩存則直接去這個對象,然后就交給這個廣播接收器的onReceive處理了,這樣插件中的廣播接收器就能接收自己想要的廣播了。
  3. 遍歷中,如果是在非persistent進(jìn)程,那我就要啟動該廣播所在的進(jìn)程,取得該進(jìn)程對應(yīng)插件的IPluginClient,因?yàn)閷τ趐ersistent進(jìn)程來說,其他進(jìn)程都屬于IPluginClient。然后其實(shí)也是進(jìn)入到PluginProcessPer,因?yàn)镻luginProcessPer就是IPluginClient的實(shí)現(xiàn)。這樣的話也就跟上面的處理方式一樣了。

宿主和插件中廣播操作的區(qū)別

插件中的PluginLocalBroadcastManager用于插件中廣播注冊,發(fā)送的管理,對應(yīng)宿主中的LocalBroadcastManager。插件中的注冊發(fā)送等方法會調(diào)用內(nèi)部的ProxyLocalBroadcastManagerVar的注冊發(fā)送等方法,也就是反射調(diào)用的LocalBroadcastManager對應(yīng)的方法,實(shí)現(xiàn)和宿主一樣對廣播的管理。

插件中ContentProvider的實(shí)現(xiàn)

插件中使用ContentProvider是通過PluginProviderClient操作的,它就相當(dāng)于我們平時用的ContentResolver,而PluginProviderClient內(nèi)部其實(shí)也是用ContentResolver實(shí)現(xiàn)的,這里會做轉(zhuǎn)換,將你傳入的uri轉(zhuǎn)換成新的uri,這個新的uri對應(yīng)了所屬的插件信息,也就是會定位到所屬插件中定義好了的坑位ContentProvider,這樣坑位ContentProvider就能收到請求,然后它還原出原始的uri,然后通過ClassLoader加載目標(biāo)provider,創(chuàng)建出該實(shí)例,然后調(diào)用對應(yīng)的方法。

  1. PluginPitProviderUI,ProcessPitProviderPersist等一些ContentProvider(這些繼承PluginPitProviderBase,再往上繼承ContentProvier),在編譯時被靜態(tài)注冊到宿主APP的AndroidManifest.xml中。
  2. 通過PluginProviderClient(相當(dāng)于ContentResolver)來訪問操作數(shù)據(jù),這里會對插件中的uri進(jìn)行轉(zhuǎn)換,再調(diào)用內(nèi)部的ContentResolver執(zhí)行增刪改查。
  3. 執(zhí)行了操作之后,系統(tǒng)就會調(diào)用到之前注冊了的這些坑位ContentProvider了,比如PluginPitProviderUI,它繼承自PluginPitProviderBase。
  4. PluginPitProviderBase中的收到uri之后,會還原uri為之前請求的uri,然后通過PluginProviderHelper取得該uri對應(yīng)的ContentProvider對象。有緩存則取ContentProvider緩存,沒有的話,去加載并實(shí)例化符合uri條件的ContentProvider對象。
  5. 有了符合uri條件的ContentProvider對象后,在分別調(diào)用它的增刪改查操作就可以了。

宿主和插件中ContentProvider操作的區(qū)別

插件中的PluginLocalBroadcastManager的增刪改查操作是通過調(diào)用ProxyRePluginProviderClientVar中的對應(yīng)的MethodInvoker,來反射宿主中的PluginProviderClient對應(yīng)的增刪改成方法,這樣插件就能和宿主一樣操作了。

插件和宿主中相同的接口是怎么回事?

插件中存在和宿主中相同的接口如Replugin,PluginProviderClient等等,是怎么回事?

答:對比分析宿主中和插件中的Replugin可以發(fā)現(xiàn),兩者提供了基本一樣的接口,而宿主中的Replugin是真正的實(shí)現(xiàn),插件中的Replugin是通過反射調(diào)用宿主中的Replugin來實(shí)現(xiàn)對應(yīng)的方法,這樣的話,在插件看來,插件中的方法調(diào)用和在宿主中的方法調(diào)用好像是一樣的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評論 2 371

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