Binder 應用概述

本文目的

理解Binder對于理解整個Android系統有著非常重要的作用,Android系統的四大組件,AMS,PMS等系統服務無一不與Binder掛鉤;如果對Binder不甚了解,那么就很難了解這些系統機制.

要真正的弄明白Binder機制還是比較麻煩的,本文只是大致的介紹一下相關的概念以及在應用層該怎么使用.

本文目標:

  1. 不依賴AIDL工具,手寫遠程Service完成跨進程通信
  • 弄明白AIDL生成的相關代碼

  • 基于AIDL代碼的分析,了解系統相關服務的代碼邏輯.

Linux相關概念

因為是講進程間的通信,而android又是基于linux,所以對于linux系統需要一定的了解.
推薦 linux內核設計與實現 一書,其主要是講一些系統概念.

  • 操作系統的不同進程之間,數據不共享;對于每個進程來說,都以為自己獨享了整個系統,完全不知道其他進程的存在;因此一個進程需要與另外一個進程通信,需要某種 系統機制 才能完成.

  • 用戶程序只可以訪問某些許可的資源,不許可的資源是拒絕被訪問的,于是人為的就把Kernel和上層的應用程序抽像的隔離開,分別稱之為 內核空間(Kernel Space)用戶空間(User Space) .

  • 用戶空間訪問內核空間的唯一方式就是 系統調用 ;通過這個統一入口,所有的資源訪問都是在內核的控制下執行,這樣可以避免用戶程序對系統資源的越權訪問,從而保障了系統的安全和穩定.

  • 當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處于 內核運行態(內核態) 此時處理器處于特權級最高的內核代碼中執行。當進程在執行用戶自己的代碼時,則稱其處于 用戶運行態(用戶態)。處理器在特權等級高的時候才能執行那些特權CPU指令。

  • 通過系統調用,用戶空間可以訪問內核空間. 如果一個用戶空間想與另外一個用戶空間進行通信,一般是需要操作系統內核添加支持.

  • Linux有個比較好的機制,就是可以 動態加載內核模塊模塊 是具有獨立功能的程序,它可以被單獨編譯,但不能獨立運行。它在運行時被鏈接到內核作為內核的一部分在內核空間運行。

  • Linux已擁有的進程間通信IPC手段包括: Pipe、Signal、Socket、Message、Share Memory 以及信號量Semaphore等.

什么是Binder驅動

由于Linux的動態加載內核模塊的機制,這樣Android系統就可以在Linux的基礎之上,通過添加一個內核模塊運行在內核空間,用戶進程之間可通過這個模塊完成通信。這個模塊就是所謂的 Binder驅動 .

盡管名叫驅動,實際上和硬件設備沒有任何關系,只是實現方式和設備驅動程序是一樣的:它工作于內核態,提供open(),mmap(),poll(),ioctl()等標準文件設備的操作.

為何使用Binder

為什么要單獨弄一套, 而不是使用linux系統提供的那些進程間通信的方式

主要是考慮到性能和安全,還有易用性.

  • 性能: Bindre使用mmap直接把接收端的內存映射到內存空間,避免了數據的餓直接拷貝;另外通過data_buffer等方式讓數據僅包含定長的消息頭,這樣就不會因為由于數據大小的不確定,而導致需要分配一個很大的空間來裝數據,或者是采用動態擴容的方式.

  • 安全性: 傳統IPC沒有任何安全措施,完全依賴上層協議來確保,且無法建立私有通道;例如Socket通信的話,socket的ip地址或文件名都是開放的,只要知道這些接入點的程序都可以和對端建立連接,不管怎樣都無法阻止惡意程序通過猜測接收方地址獲得連接。 而Binder機制對于通信雙方的身份是內核進行校驗支持的。

  • 易用性: 共享內存不需要copy,性能夠高,可是使用復雜; B/S模式的通信,如果Pipe/Message還得進行包裝;而Binder使用面向對象的方式設計,進行一次遠程過程調用就好像直接調用本地對象一樣,比較方便,Binder驅動的底層實現對上層應用來說完全透明。

Binder通信模型

應用層大家所熟知的通信結構, 如下圖:


binder通信結構.jpg
  1. 從表面上來看,是client通過獲得一個server的代理接口,對server進行直接調用;

  2. 實際上,代理接口中定義的方法與server中定義的方法是一一對應的;

  3. client調用某個代理接口中的方法時,代理接口的方法會將client傳遞的參數打包成為Parcel對象;

  4. 代理接口將該Parcel發送給內核中的binder driver.

  5. server會讀取binder driver中的請求數據,如果是發送給自己的,解包Parcel對象,處理并將結果返回;

  6. 整個的調用過程是一個同步過程,在server處理的時候,client會block住。

在整個Binder系統中,Binder框架定義了四個角色:Server,Client,ServiceManager 以及Binder驅動。其中Server,Client,SM運行于用戶空間,驅動運行于內核空間,他們之間的關系如下圖(參考老羅的Android之旅-Binder篇):

Binder各角色之間的關系.jpg

說明如下:

  1. Client、Server和Service Manager實現在用戶空間中,Binder驅動程序實現在內核空間中

  2. Binder驅動程序和Service Manager在Android平臺中已經實現,開發者只需要在用戶空間實現自己的Client和Server

  3. Binder驅動程序提供設備文件/dev/binder與用戶空間交互,Client、Server和Service Manager通過open和ioctl文件操作函數與Binder驅動程序進行通信

  4. Client和Server之間的進程間通信通過Binder驅動程序間接實現

  5. Service Manager是一個守護進程,用來管理Server,并向Client提供查詢Server接口的能力

可以看出驅動是整個通信過程的核心,完成跨進程通信的秘密全部隱藏在驅動里面;這里Client與SM的通信,以及Client與Server的通信,都會經過驅動

相關接口(addService/getService)可參見 native/libs/binder/IServiceManager.cpp 以及對應的 service_manager.c 文件

Binder機制跨進程原理
binder-跨進程原理.jpg
  1. 首先Server進程要向SM注冊;告訴自己是誰,自己有什么能力;在這個場景就是Server告訴SM,它叫Server_A,它有一個object對象,可以執行add 操作;于是SM建立了一張表:Server_A這個名字對應進程Server; 如原代碼中 .//native/libs/binder/IServiceManager.cpp, 它會將server名以及對應的server進程通過Parcel給到Binder Driver中去.

     virtual status_t addService(const String16& name, const sp<IBinder>& service,
         bool allowIsolated)
     {
         Parcel data, reply;
     data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
         data.writeString16(name);
         data.writeStrongBinder(service);
         data.writeInt32(allowIsolated ? 1 : 0);
         status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
         return err == NO_ERROR ? reply.readExceptionCode() : err;
     }
    
  • 然后Client向SM查詢:名字叫做Server_A的進程里面的object對象;進程之間通信的數據都會經過運行在內核空間里面的驅動,驅動在數據流過的時候會做一些處理,它并不會給Client進程返回一個真正的object對象,而是返回一個看起來跟object一模一樣的代理對象objectProxy,這個objectProxy也有一個add方法,但是這個add方法沒有Server進程里面object對象的add方法那個能力;它唯一做的事情就是把參數包裝然后交給驅動。

  • 驅動收到這個消息,發現是這個objectProxy;通過查表就知道:之前用objectProxy替換了object發送給Client了,它真正應該要訪問的是object對象的add方法;于是Binder驅動通知Server進程,調用它的object對象的add方法,然后把結果發給binder驅動,Sever進程收到這個消息,執行add之后將結果返回驅動,驅動然后把結果返回給Client進程;于是整個過程就完成了.

Binder跨進程傳輸并不是真的把一個對象傳輸到了另外一個進程;傳輸過程是在Binder跨進程穿越的時候,它在一個進程留下了一個本體,在另外一個進程則使用該對象的一個proxy;Client進程的操作其實是對于proxy的操作,proxy利用Binder驅動最終讓真正的binder對象完成操作。

Android系統實現這種機制使用的是代理模式, 對于Binder的訪問,如果是在同一個進程,那么直接返回原始的Binder實體;如果在不同進程,那么就給它一個代理對象- 如后面demo中的代碼...

public static ICalculate asInterface(IBinder obj) {
        if(obj == null) {
            return null;
        } else {
            IInterface iin = obj.queryLocalInterface("com.zhangfl.jpush.ICalculate");
            
            return (ICalculate)(iin != null && iin instanceof ICalculate?(ICalculate)iin:new ICalculate.Stub.Proxy(obj));
        }
    }

Client進程只不過是持有了Server端的代理;代理對象協助驅動完成了跨進程通信。

proxy代理模式

模式中的三種角色:

proxy.jpg
  • 抽象角色:聲明真實對象和代理對象的共同接口。

  • 代理角色:代理對象角色內部含有對真實對象的引用,從而可以操作真實對象,同時代理對象提供與真實對象相同的接口以便在任何時刻都能代替真實對象。同時,代理對象可以在執行真實對象操作時,附加其他的操作,相當于對真實對象進行封裝。

  • 真實角色:代理角色所代表的真實對象,是我們最終要引用的對象。

模式原則: 對修改關閉,對擴展開放,保證了系統的穩定性

驅動里面的Binder

略過: 具體可以參考binder.c源碼以及 Binder設計與實現 一文

Java層的Binder

IBinder/IInterface/Binder/BinderProxy/Stub

  • IBinder是一個接口,它代表了一種跨進程傳輸的能力;只要實現了這個接口,就能將這個對象進行跨進程傳遞;這是驅動底層支持的;在跨進程數據流經驅動的時候,驅動會識別IBinder類型的數據,從而自動完成不同進程Binder本地對象以及Binder代理對象的轉換。

  • IInterface代表的就是遠程server對象具有的能力。具體來說,就是aidl里面的接口。

  • Java層的Binder類,代表的其實就是Binder本地對象。BinderProxy類是Binder類的一個內部類,它代表遠程進程的Binder對象的本地代理;這兩個類都繼承自IBinder, 因而都具有跨進程傳輸的能力;實際上,在跨越進程的時候,Binder驅動會自動完成這兩個對象的轉換。

  • 在使用AIDL的時候,編譯工具會給我們生成一個Stub的靜態內部類;這個類繼承了Binder, 說明它是一個Binder本地對象,它實現了IInterface接口,表明它具有遠程Server承諾給Client的能力;Stub是一個抽象類,具體的IInterface的相關實現需要我們手動完成. 其實這里使用了策略模式.

Demo - 不依賴AIDL工具,手寫遠程Service完成跨進程通信

通過上面的一些概念以及Binder相關的設計論述,我們手寫遠程Service完成跨進程通信就很簡單了.

client:

private IBinder mRemote = null;
private ServiceConnection serviceConn = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service){
        mRemote = service;
    }
   
    @Override
    public void onServiceDisconnected(ComponentName name) {}
};

private void binder() {
    Intent intent = new Intent(BinderActivity.this, CalculateService.class);
    bindService(intent,serviceConn, Context.BIND_AUTO_CREATE);
}
    
private void add() {
    String strA = etFirst.getText().toString();
    String strB = etSecond.getText().toString();
    int a = (StringUtils.isEmpty(strA) ? 0 : Integer.parseInt(strA));
    int b = ((StringUtils.isEmpty(strB) ? 0 : Integer.parseInt(strB)));

    Parcel _data = Parcel.obtain();
    Parcel _reply = Parcel.obtain();
    int _result = -100;

    try {
        _data.writeInt(a);
        _data.writeInt(b);
        mRemote.transact(1, _data, _reply, 0);
        _result = _reply.readInt();
    } catch (RemoteException e) {
        Logger.e(TAG, "RemoteException:", e);
    } finally {
        _reply.recycle();
        _data.recycle();
    }

    Logger.d(TAG, "binder:" + a + " + " + b + " = " + _result);
}

Server:

public class CalculateService extends Service {

    private static final String TAG = "CalculateService";

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Logger.d(TAG, "binder success");
        return binder;
    }

    private IBinder binder = new Binder() {
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            if (code == 1) {
                int _arg0;
                _arg0 = data.readInt();
                int _arg1;
                _arg1 = data.readInt();
                int _result = this.add(_arg0, _arg1);
                reply.writeInt(_result);
                return true;
            }
            return super.onTransact(code, data, reply, flags);
        }

        public int add(int a, int b) {
            Logger.d(TAG, "PID:" + android.os.Process.myPid());
            Logger.d(TAG, "a:" + a + ", b:" + b);
            return a + b;
        }
    };
}
  1. 首先client通過 binder()得到 server端的 IBinder(我們已經知道,IBinder是一個接口,它代表了一種跨進程傳輸的能力;只要實現了這個接口,就能將這個對象進行跨進程傳遞)

  2. 然后客戶端調用 mRemote.transact方法完成進程間的通信: mRemote是遠程對象,在調用transact方法會執行onTransact方法;通過把Client端的參數轉換成Parcel(_data)傳遞到Server端

  3. 最后在server端執行onTransact方法,解包Parcel對象,得到由client傳入的參數,最終將執行結果封包成Parcel對象給到client, client最后也通過解包得到相應的結果.這樣整個過程就形成了一次跨進程之間的通信.

PS: 記得在manifest.xml中將 server配成遠程服務.

demo - 通過aidl來實現跨進程通信

client:

private ICalculate calculate = null;
private ServiceConnection serviceConnectionAidl = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        calculate = ICalculate.Stub.asInterface(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

private void aidlBinder() {
    Intent intent = new Intent(BinderActivity.this, CalculateAidlService.class);
    bindService(intent,serviceConnectionAidl, Context.BIND_AUTO_CREATE);
}

private void addByAidl() {
    String strA = etFirst.getText().toString();
    String strB = etSecond.getText().toString();
    int a = (StringUtils.isEmpty(strA) ? 0 : Integer.parseInt(strA));
    int b = ((StringUtils.isEmpty(strB) ? 0 : Integer.parseInt(strB)));
    
    try {
        int result = calculate.add(a, b);
        Logger.d(TAG, "aidl:" + a + " + " + b + " = " + result);
    } catch (RemoteException e) {
        Logger.e(TAG, "RemoteException:", e);
    }
}

server:

public class CalculateAidlService extends Service {

    private static final String TAG = "CalculateAidlService";

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Logger.d(TAG, "binder success");
        return binder;
    }

    private IBinder binder = new ICalculate.Stub() {

        @Override
        public int add(int a, int b) throws RemoteException {
            Logger.d(TAG, "PID:" + android.os.Process.myPid());
            Logger.d(TAG, "a:" + a + ", b:" + b);
            return a + b;
        }
    };
}

通過之前的分析以及同自己寫遠程server的對比,我們可以看出通過aidl方式來實現跨進程通信是多么的簡潔. 可以看看aidl自動生成的代碼在背后做了些什么.

aidl interface:

// ICalculate.aidl
package com.zhangfl.jpush;

// Declare any non-default types here with import statements

interface ICalculate {
    int add(int a, int b);
}

上面的interface通過aidl工具生成的相關代碼:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.zhangfl.jpush;

import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;

public interface ICalculate extends IInterface {
    int add(int var1, int var2) throws RemoteException;

    public abstract static class Stub extends Binder implements ICalculate {
        private static final String DESCRIPTOR = "com.zhangfl.jpush.ICalculate";
        static final int TRANSACTION_add = 1;

        public Stub() {
            this.attachInterface(this, "com.zhangfl.jpush.ICalculate");
        }

        public static ICalculate asInterface(IBinder obj) {
            if(obj == null) {
                return null;
            } else {
                IInterface iin = obj.queryLocalInterface("com.zhangfl.jpush.ICalculate");
                return (ICalculate)(iin != null && iin instanceof ICalculate?(ICalculate)iin:new ICalculate.Stub.Proxy(obj));
            }
        }

        public IBinder asBinder() {
            return this;
        }

        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            switch(code) {
                case 1:
                    data.enforceInterface("com.zhangfl.jpush.ICalculate");
                    int _arg0 = data.readInt();
                    int _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                case 1598968902:
                    reply.writeString("com.zhangfl.jpush.ICalculate");
                    return true;
                default:
                    return super.onTransact(code, data, reply, flags);
            }
        }

        private static class Proxy implements ICalculate {
            private IBinder mRemote;

            Proxy(IBinder remote) {
                this.mRemote = remote;
            }

            public IBinder asBinder() {
                return this.mRemote;
            }

            public String getInterfaceDescriptor() {
                return "com.zhangfl.jpush.ICalculate";
            }

            public int add(int a, int b) throws RemoteException {
                Parcel _data = Parcel.obtain();
                Parcel _reply = Parcel.obtain();

                int _result;
                try {
                    _data.writeInterfaceToken("com.zhangfl.jpush.ICalculate");
                    _data.writeInt(a);
                    _data.writeInt(b);
                    this.mRemote.transact(1, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }

                return _result;
            }
        }
    }
}

在使用AIDL的時候,編譯工具會給我們生成一個Stub的靜態內部類;這個類繼承了Binder, 說明它是一個Binder本地對象,它實現了IInterface接口,表明它具有遠程Server承諾給Client的能力;Stub是一個抽象類,具體的IInterface的相關實現需要自己手動完成(其實也僅僅只是完成具體的功能而已,完全沒必要理會跨進程;因為aidl生成的代碼在底層已經完全實現了,大家仔細看看就非常清楚它同我們自己手寫的遠程server的代碼非常的像,只是用了一下proxy的設計模式,包裝了一下而已).

Proxy與Stub不一樣,雖然他們都既是Binder又是IInterface,不同的是Stub采用的是繼承(is 關系), Proxy采用的是組合(has 關系)。他們均實現了所有的IInterface函數,不同的是,Stub使用策略模式 調用的是虛函數(待子類實現),而Proxy則使用組合模式。為什么Stub采用繼承而Proxy采用組合? 事實上,Stub本身is一個IBinder(Binder),它本身就是一個能跨越進程邊界傳輸的對象,所以它得繼 承IBinder實現transact這個函數從而得到跨越進程的能力(這個能力由驅動賦予)。Proxy類使用組合,是因為他不關心自己是什么,它也不需要跨越進程傳輸,它只需要擁有這個能力即可,要擁有這個能力,只需要保留一個對IBinder的引用.

AIDL過程分析, 一種固定的模式:

  • 一個需要跨進程傳遞的對象一定繼承自IBinder,如果是Binder本地對象,那么一定繼承Binder實現IInterface,如果是代理對象,那么就實現了IInterface并持有IBinder引用.
  • ICalculate, ICalculate.Stub以及 ICalculate.Stub.Proxy 它們三者之間的關系同之前說的代理模式中的三個角色.
系統服務分析

IXXX、IXXX.Stub和IXXX.Stub.Proxy,并做好對應。這樣看相關的系統服務就比較容易了,以ServiceManager為例

實際上ServerManager既是系統服務的管理者,同時也是一個系統服務。因此它肯定是基于Binder實現的

  • 與IXXX相對應的類就是IServiceManager類,封裝了遠程調用的幾個主要函數

  • 與IXXX.Stub對應的類就是ServiceManagerStub,查看源碼沒有發現該類,我們通過搜索關鍵字 "implements IServiceManager":發現與IXXX.Stub對應的類就是ServiceManagerNative

    IServiceManager.jpg
  • 與IXXX.Stub.Proxy對應的類ServiceManagerProxy

    查看上面相關類的代碼,實際上和使用adil生成的代碼沒什么兩樣。僅僅是類命名不一樣,將三個類分開寫了而已。

再看看ActivityManager中的Binder。

  • IActivityManager對應IXXX接口

  • 同理可以通過關鍵字"implements IActivityManager"查找,發現ActivityManagerNative對應IXXX.Stub類,繼承自Binder類。

  • ActivityManagerProxy對應IXXX.Stub.Proxy類。

    AMS的服務端就是ActivityManagerService類,這個類繼承自ActivityManagerNative,實現了IActivityManager接口中的方法用來進行IPC。

    只要在客戶端通過server名得到這個遠程服務端的Binder引用就可以進行IPC通信了

參考

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

推薦閱讀更多精彩內容