Android即時(shí)通訊系列文章(1)多進(jìn)程:為什么要把消息服務(wù)拆分到一個(gè)獨(dú)立的進(jìn)程?

這是即時(shí)通訊系列文章的第一篇卵贱,正式開(kāi)始對(duì)IM開(kāi)發(fā)技術(shù)的講解之前滥沫,我們先來(lái)談?wù)効蛻?hù)端在完整聊天系統(tǒng)中所扮演的角色,為此键俱,我們必須先明確客戶(hù)端的職責(zé)兰绣。

現(xiàn)今主流的IM應(yīng)用幾乎都是采用服務(wù)器中轉(zhuǎn)的方式來(lái)進(jìn)行消息傳輸?shù)模瑸榈氖歉玫刂С蛛x線编振、群組等業(yè)務(wù)缀辩。在這種模式下,所有客戶(hù)端都需連接到服務(wù)端踪央,服務(wù)端將不同客戶(hù)端發(fā)給自己的消息根據(jù)消息里攜帶的用戶(hù)標(biāo)識(shí)進(jìn)行轉(zhuǎn)發(fā)或廣播臀玄。

因此,作為消息收發(fā)的終端設(shè)備畅蹂,客戶(hù)端的重要職責(zé)之一就是保持與服務(wù)端的連接健无,該連接的穩(wěn)定性直接決定消息收發(fā)的實(shí)時(shí)性和可靠性。而在上篇文章我們講過(guò)液斜,移動(dòng)設(shè)備是資源受限的睬涧,這對(duì)連接的穩(wěn)定性提出了極大的挑戰(zhàn),具體可體現(xiàn)在以下兩個(gè)方面:

  • 為了維持多任務(wù)環(huán)境的正常運(yùn)行旗唁,Android為每個(gè)應(yīng)用的堆大小設(shè)置了硬性上限畦浓,不同設(shè)備的確切堆大小取決于設(shè)備的總體可用RAM大小,如果應(yīng)用在達(dá)到堆容量上限后嘗試分配更多內(nèi)容检疫,則可能引發(fā)OOM讶请。
  • 當(dāng)用戶(hù)切換到其他應(yīng)用時(shí),系統(tǒng)會(huì)將原有應(yīng)用的進(jìn)程保留在緩存中,稍后如果用戶(hù)返回該應(yīng)用夺溢,系統(tǒng)就會(huì)重復(fù)使用該進(jìn)程论巍,以便加快應(yīng)用切換速度。但當(dāng)系統(tǒng)資源(如內(nèi)存)不足時(shí)风响,系統(tǒng)會(huì)考慮終止占用最多內(nèi)存的嘉汰、優(yōu)先級(jí)較低的進(jìn)程以釋放RAM。

雖然ART和Dalvik虛擬機(jī)會(huì)例行執(zhí)行垃圾回收任務(wù)状勤,但如果應(yīng)用存在內(nèi)存泄漏問(wèn)題鞋怀,并且只有一個(gè)主進(jìn)程,勢(shì)必會(huì)隨著應(yīng)用使用時(shí)間的延長(zhǎng)而逐步增大內(nèi)存使用量持搜,從而增加引發(fā)OOM的概率和緩存進(jìn)程被系統(tǒng)終止的風(fēng)險(xiǎn)密似。

因此,為了保證連接的穩(wěn)定性葫盼,可考慮將負(fù)責(zé)連接保持工作的消息服務(wù)放入一個(gè)獨(dú)立的進(jìn)程中残腌,分離之后即使主進(jìn)程退出、崩潰或者出現(xiàn)內(nèi)存消耗過(guò)高等情況贫导,該服務(wù)仍可正常運(yùn)行抛猫,甚至可以在適當(dāng)?shù)臅r(shí)機(jī)通過(guò)廣播等方式重新喚起主進(jìn)程。

但是孩灯,給應(yīng)用劃分進(jìn)程闺金,往往就意味著需要編寫(xiě)額外的進(jìn)程通訊代碼,特別是對(duì)于消息服務(wù)這種需要高度交互的場(chǎng)景钱反。而由于各個(gè)進(jìn)程都運(yùn)行在相對(duì)獨(dú)立的內(nèi)存空間,因而是無(wú)法直接通訊的匣距。為此面哥,Android提供了AIDL(Android Interface Definition Language,Android接口定義語(yǔ)言)用于實(shí)現(xiàn)進(jìn)程間通信毅待,其本質(zhì)就是實(shí)現(xiàn)對(duì)象的序列化尚卫、傳輸、接收和反序列化尸红,得到可操作的對(duì)象后再進(jìn)行常規(guī)的方法調(diào)用吱涉。

接下來(lái),就讓我們來(lái)一步步實(shí)現(xiàn)跨進(jìn)程的通訊吧外里。

Step1 創(chuàng)建服務(wù)

由于連接保持的工作是需要在后臺(tái)執(zhí)行長(zhǎng)時(shí)間執(zhí)行的操作怎爵,通常不提供操作界面,符合這個(gè)特性的組件就是Service了盅蝗,因此我們選用Service作為與遠(yuǎn)程進(jìn)程進(jìn)行進(jìn)程間通信(IPC)的組件鳖链。創(chuàng)建Service的子類(lèi)時(shí),必須實(shí)現(xiàn)onBind回調(diào)方法墩莫,此處我們暫時(shí)返回空實(shí)現(xiàn)芙委。

class MessageAccessService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}

另外使用Service還有一個(gè)好處就是逞敷,我們可以在適當(dāng)?shù)臅r(shí)機(jī)將其升級(jí)為前臺(tái)服務(wù),前臺(tái)服務(wù)是用戶(hù)主動(dòng)意識(shí)到的一種服務(wù)灌侣,進(jìn)程優(yōu)先級(jí)較高推捐,因此在內(nèi)存不足時(shí),系統(tǒng)也不會(huì)考慮將其終止侧啼。

使用前臺(tái)服務(wù)唯一的缺點(diǎn)就是必須在抽屜式通知欄提供一條不可移除的通知牛柒,對(duì)于用戶(hù)體驗(yàn)極不友好,但是我們可以通過(guò)定制通知樣式進(jìn)行協(xié)調(diào)慨菱,后續(xù)的文章中會(huì)講到焰络。

step2 指定進(jìn)程

默認(rèn)情況下,同一應(yīng)用的所有組件均在相同的進(jìn)程中運(yùn)行符喝。如需控制某個(gè)組件所屬的進(jìn)程闪彼,可通過(guò)在清單文件中設(shè)置android:process屬性實(shí)現(xiàn):

<manifest ...>
    <application ...>
        <service
            android:name=".service.MessageAccessService"
            android:exported="true"
            android:process=":remote" />
    </application>
</manifest>

另外,為使其他進(jìn)程的組件能調(diào)用服務(wù)或與之交互协饲,還需設(shè)置android:exported屬性為true畏腕。

step3 創(chuàng)建.aidl 文件

讓我們重新把目光放回onBind回調(diào)方法,該方法要求返回IBinder對(duì)象茉稠,客戶(hù)端可使用該對(duì)象定義好的接口與服務(wù)進(jìn)行通信描馅。IBinder是遠(yuǎn)程對(duì)象的基礎(chǔ)接口,該接口描述了與遠(yuǎn)程對(duì)象交互的抽象協(xié)議而线,但不建議直接實(shí)現(xiàn)此接口铭污,而應(yīng)從Binder擴(kuò)展。通常做法是是使用.aidl文件來(lái)描述所需的接口膀篮,使其生成適當(dāng)?shù)腂inder子類(lèi)嘹狞。

那么,這個(gè)最關(guān)鍵的.aidl文件該如何創(chuàng)建誓竿,又該定義哪些接口呢磅网?

創(chuàng)建.aidl文件很簡(jiǎn)單,Android Studio本身就提供了創(chuàng)建AIDL文件方法:項(xiàng)目右鍵 -> New -> AIDL -> AIDL File

前面講過(guò)筷屡,客戶(hù)端是消息收發(fā)的終端設(shè)備涧偷,而接入服務(wù)則是為客戶(hù)端提供了消息收發(fā)的出入口”兴溃客戶(hù)端發(fā)出的消息經(jīng)由接入服務(wù)發(fā)送到服務(wù)端燎潮,同時(shí)客戶(hù)端會(huì)委托接入服務(wù)幫忙收取消息,當(dāng)服務(wù)端有消息推送過(guò)來(lái)時(shí)通知自己扼倘。

如此一來(lái)便很清晰了跟啤,我們要定義的接口總共有三個(gè),分別為:

  • 發(fā)送消息
  • 注冊(cè)消息接收器
  • 反注冊(cè)消息接收器

MessageCarrier.aidl

package com.xxx.imsdk.comp.remote;
import com.xxx.imsdk.comp.remote.bean.Envelope;
import com.xxx.imsdk.comp.remote.listener.MessageReceiver;

interface MessageCarrier {
    void sendMessage(in Envelope envelope);
    void registerReceiveListener(MessageReceiver messageReceiver);
    void unregisterReceiveListener(MessageReceiver messageReceiver);
}

這里解釋一下上述接口中攜帶的參數(shù)的含義:

Envelope ->

解釋這個(gè)參數(shù)之前,得先介紹Envelope.java這個(gè)類(lèi)隅肥,該類(lèi)是多進(jìn)程通訊中作為數(shù)據(jù)傳輸?shù)膶?shí)體類(lèi)竿奏。AIDL支持的數(shù)據(jù)類(lèi)型除了基本數(shù)據(jù)類(lèi)型、String和CharSequence腥放,還有就是實(shí)現(xiàn)了Parcelable接口的對(duì)象泛啸,以及其中元素為以上幾種的List和Map。

Envelope.java

**
 * 用于多進(jìn)程通訊的信封類(lèi)
 * <p>
 * 在AIDL中傳遞的對(duì)象秃症,需要在類(lèi)文件相同路徑下候址,創(chuàng)建同名、但是后綴為.aidl的文件种柑,并在文件中使用parcelable關(guān)鍵字聲明這個(gè)類(lèi)岗仑;
 * 但實(shí)際業(yè)務(wù)中需要傳遞的對(duì)象所屬的類(lèi)往往分散在不同的模塊,所以通過(guò)構(gòu)建一個(gè)包裝類(lèi)來(lái)包含真正需要被傳遞的對(duì)象(必須也實(shí)現(xiàn)Parcelable接口)
 */
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
                    val noticeVo: NoticeVo? = null) : Parcelable {
}

另外聚请,在AIDL中傳遞的對(duì)象荠雕,需要在上述類(lèi)文件的相同包路徑下,創(chuàng)建同名驶赏、但是后綴為.aidl的文件炸卑,并在文件中使用parcelable關(guān)鍵字聲明這個(gè)類(lèi),Envelope.aidl就是對(duì)應(yīng)Envelope.java而創(chuàng)建的煤傍;

Envelope.aidl

package com.xxx.imsdk.comp.remote.bean;

parcelable Envelope;

兩個(gè)文件對(duì)應(yīng)的路徑比較如下:

clipboard.png

那為什么是Envelope類(lèi)而不直接是MessageVO類(lèi)(消息視圖對(duì)象)呢盖文?這是由于考慮到實(shí)際業(yè)務(wù)中需要傳遞的對(duì)象所屬的類(lèi)往往分散在不同的模塊(MessageVO從屬于另外一個(gè)模塊,需要被其他模塊引用)蚯姆,所以通過(guò)構(gòu)建一個(gè)包裝類(lèi)來(lái)包含真正需要被傳遞的對(duì)象(該對(duì)象必須也實(shí)現(xiàn)Parcelable接口)五续,這也是該類(lèi)命名為Envelope(信封)的含義。

MessageReceiver ->

跨進(jìn)程的消息收取回調(diào)接口龄恋,用于將消息接入服務(wù)收取到的服務(wù)端消息傳遞到客戶(hù)端疙驾。但這里使用的回調(diào)接口有點(diǎn)不一樣,在AIDL中傳遞的接口篙挽,不能是普通的接口荆萤,只能是AIDL接口镊靴,因此我們還需要新建多一個(gè).aidl文件:

MessageReceiver.aidl

package com.xxx.imsdk.comp.remote.listener;
import com.xxx.imsdk.comp.remote.bean.Envelope;

interface MessageReceiver {
    void onMessageReceived(in Envelope envelope);
}

包目錄結(jié)構(gòu)如下圖:

FE55B9D0FFFC48829667C01C212B2668.jpg
step4 返回IBinder接口

構(gòu)建應(yīng)用時(shí)铣卡,Android SDK會(huì)生成基于.aidl 文件的IBinder接口文件,并將其保存到項(xiàng)目的gen/目錄中偏竟。生成文件的名稱(chēng)與.aidl 文件的名稱(chēng)保持一致煮落,區(qū)別在于其使用.java 擴(kuò)展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)踊谋。此接口擁有一個(gè)名為Stub的內(nèi)部抽象類(lèi)蝉仇,用于擴(kuò)展 Binder 類(lèi)并實(shí)現(xiàn) AIDL 接口中的方法。

/** 根據(jù)MessageCarrier.aidl文件自動(dòng)生成的Binder對(duì)象,需要返回給客戶(hù)端 */
private val messageCarrier: IBinder = object : MessageCarrier.Stub() {

    override fun sendMessage(envelope: Envelope?) {

    }

    override fun registerReceiveListener(messageReceiver: MessageReceiver?) {
        remoteCallbackList.register(messageReceiver)
    }

    override fun unregisterReceiveListener(messageReceiver: MessageReceiver?) {
        remoteCallbackList.unregister(messageReceiver)
    }

}

override fun onBind(intent: Intent?): IBinder? {
    return messageCarrier
}
step5 綁定服務(wù)

組件(例如 Activity)可以通過(guò)調(diào)用bindService方法綁定到服務(wù)轿衔,該方法必須提供ServiceConnection 的實(shí)現(xiàn)以監(jiān)控與服務(wù)的連接沉迹。當(dāng)組件與服務(wù)之間的連接建立成功后, ServiceConnection上的 onServiceConnected()方法將被回調(diào)害驹,該方法包含上一步返回的IBinder對(duì)象鞭呕,隨后便可使用該對(duì)象與綁定的服務(wù)進(jìn)行通信。

/**
 * ## 綁定消息接入服務(wù)
 * 同時(shí)調(diào)用bindService和startService, 可以使unbind后Service仍保持運(yùn)行
 * @param context   上下文
 */
@Synchronized
fun setupService(context: Context? = null) {
    if (!::appContext.isInitialized) {
        appContext = context!!.applicationContext
    }

    val intent = Intent(appContext, MessageAccessService::class.java)

    // 記錄綁定服務(wù)的結(jié)果宛官,避免解綁服務(wù)時(shí)出錯(cuò)
    if (!isBound) {
        isBound = appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    startService(intent)
}

/** 監(jiān)聽(tīng)與服務(wù)連接狀態(tài)的接口 */
private val serviceConnection = object : ServiceConnection {

    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        // 取得MessageCarrier.aidl對(duì)應(yīng)的操作接口
        messageCarrier = MessageCarrier.Stub.asInterface(service)
        ...
    }

    override fun onServiceDisconnected(name: ComponentName?) {
    }

}

可以同時(shí)將多個(gè)組件綁定到同一個(gè)服務(wù)葫松,但當(dāng)最后一個(gè)組件取消與服務(wù)的綁定時(shí),系統(tǒng)會(huì)銷(xiāo)毀該服務(wù)底洗。為了使服務(wù)能夠無(wú)限期運(yùn)行腋么,可同時(shí)調(diào)用startService()和bindService(),創(chuàng)建同時(shí)具有已啟動(dòng)和已綁定兩種狀態(tài)的服務(wù)亥揖。這樣珊擂,即使所有組件均解綁服務(wù),系統(tǒng)也不會(huì)銷(xiāo)毀該服務(wù)徐块,直至調(diào)用 stopSelf() 或 stopService() 才會(huì)顯式停止該服務(wù)未玻。

/**
 * 啟動(dòng)消息接入服務(wù)
 * @param intent    意圖
 * @param action    操作
 */
private fun startService(
    intent: Intent = Intent(appContext, MessageAccessService::class.java),
    action: String? = null
) {
    // Android8.0不再允許后臺(tái)service直接通過(guò)startService方式去啟動(dòng),將引發(fā)IllegalStateException
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
        && !ProcessUtil.isForeground(appContext)
    ) {
        if (!TextUtils.isEmpty(action)) intent.action = action
        intent.putExtra(KeepForegroundService.EXTRA_ABANDON_FOREGROUND, false)
        appContext.startForegroundService(intent)
    } else {
        appContext.startService(intent)
    }
}

/**
 * 停止消息接入服務(wù)
 */
fun stopService() {
    // 立即清除緩存的WebSocket服務(wù)器地址,防止登錄時(shí)再次使用舊的WebSocket服務(wù)器地址(帶的會(huì)話已失效)胡控,導(dǎo)致收到用戶(hù)下線的通知
    GlobalScope.launch {
        DataStoreUtil.writeString(appContext, RemoteDataStoreKey.WEB_SOCKET_SERVER_URL, "")
    }

    unbindService()

    appContext.stopService(Intent(appContext, MessageAccessService::class.java))
}

/**
 * 解綁消息接入服務(wù)
 */
@Synchronized
fun unbindService() {
    if (!isBound) return // 必須判斷服務(wù)是否已解除綁定扳剿,否則會(huì)報(bào)java.lang.IllegalArgumentException: Service not registered

    // 解除消息監(jiān)聽(tīng)接口
    if (messageCarrier?.asBinder()?.isBinderAlive == true) {
        messageCarrier?.unregisterReceiveListener(messageReceiver)
        messageCarrier = null
    }

    appContext.unbindService(serviceConnection)

    isBound = false
}

總結(jié)

通過(guò)以上代碼的實(shí)踐,最終我們得以將應(yīng)用拆分為主進(jìn)程和遠(yuǎn)程進(jìn)程昼激。主進(jìn)程主要負(fù)責(zé)用戶(hù)交互庇绽、界面展示,而遠(yuǎn)程進(jìn)程則主要負(fù)責(zé)消息收發(fā)橙困、連接保持等瞧掺。由于遠(yuǎn)程進(jìn)程僅保持了最小限度的業(yè)務(wù)邏輯處理,內(nèi)存增長(zhǎng)相對(duì)穩(wěn)定凡傅,因此會(huì)大大降低系統(tǒng)內(nèi)存緊張時(shí)遠(yuǎn)端進(jìn)程被終止的概率辟狈,即使主進(jìn)程因?yàn)橐馔馇闆r退出了,遠(yuǎn)程進(jìn)程仍可保持運(yùn)行夏跷,從而保證連接的穩(wěn)定性哼转。

參考

WebSocket詳解(一):初步認(rèn)識(shí)WebSocket技術(shù)

http://www.52im.net/thread-331-1-1.html

內(nèi)存管理概覽

https://developer.android.google.cn/topic/performance/memory-overview

進(jìn)程和應(yīng)用生命周期

https://developer.android.google.cn/guide/components/activities/process-lifecycle

服務(wù)概覽

https://developer.android.google.cn/guide/components/services

綁定服務(wù)概覽

https://developer.android.google.cn/guide/components/bound-services

Android 接口定義語(yǔ)言 (AIDL)

https://developer.android.google.cn/guide/components/aidl

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市槽华,隨后出現(xiàn)的幾起案子壹蔓,更是在濱河造成了極大的恐慌,老刑警劉巖猫态,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佣蓉,死亡現(xiàn)場(chǎng)離奇詭異披摄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)勇凭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)疚膊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人虾标,你說(shuō)我怎么就攤上這事酿联。” “怎么了夺巩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵贞让,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我柳譬,道長(zhǎng)喳张,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任美澳,我火速辦了婚禮销部,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘制跟。我一直安慰自己舅桩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布雨膨。 她就那樣靜靜地躺著擂涛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪聊记。 梳的紋絲不亂的頭發(fā)上撒妈,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音排监,去河邊找鬼狰右。 笑死,一個(gè)胖子當(dāng)著我的面吹牛舆床,可吹牛的內(nèi)容都是我干的棋蚌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挨队,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼谷暮!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瞒瘸,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤坷备,失蹤者是張志新(化名)和其女友劉穎熄浓,沒(méi)想到半個(gè)月后情臭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體省撑,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年俯在,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了竟秫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡跷乐,死狀恐怖肥败,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情愕提,我是刑警寧澤馒稍,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站浅侨,受9級(jí)特大地震影響纽谒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜如输,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一鼓黔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧不见,春花似錦澳化、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至灶似,卻和暖如春慎陵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喻奥。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工席纽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撞蚕。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓润梯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親甥厦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纺铭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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