從AIDL分析Framework層中的跨進程通信--Binder機制

AIDL簡介

AIDL是Android Interface Definition Language的縮寫少漆,即Android接口定義語言侧馅。它是Android的進程間通信比較常用的一種方式,其原理是通過Binder機制實現(xiàn)進程間通信的

從一個簡單的AIDL實例開始分析

由于Binder機制的進程間通信是基于C/S架構(gòu)的食棕,這里先看下客戶端需要創(chuàng)建的文件以及代碼---

  • 客戶端(Client)應(yīng)用創(chuàng)建:在客戶端定義一個Book實體朗和,如果在Binder通信中傳遞非基本類型,那么除了需要創(chuàng)建一個實現(xiàn)Parcelable接口的實體類外簿晓,還需要再建個和實體類命名一樣的實體AIDL文件眶拉,如下:
// Book.aidl
package com.example.client;
parcelable Book;

在同一個包下定義一個AIDL接口文件,其中有兩個方法:

// IBookManager.aidl
package com.example.client;
//注意即使在同一個包下也需要導(dǎo)入
import com.example.client.Book;

interface IBookManager {
    //添加書本
    void addBook(in Book person);
    //返回圖書列表
    List<Book> getBookList();
}

綁定遠程服務(wù)端代碼(獲取遠程Binder引用):

    private void bindService() {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.example.client", "com.xx.leo_service.LeoAidlService"));
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iBookManager = IBookManager.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iBookManager = null;
        }
    };
  • 服務(wù)端(Server)應(yīng)用創(chuàng)建憔儿,注意需要把Book實體類和AIDL接口文件完全拷貝過來忆植,包名也需要一致,除此之外谒臼,再新建一個服務(wù)朝刊,其內(nèi)部創(chuàng)建一個Binder,繼承IBookManager.Stub蜈缤,實現(xiàn)遠程方法:
    void addBook(in Book book);
    List<Book> getBookList();
    并通過onBind()返回給客戶端
    如下:
public class BookManagerService extends Service {
    private ArrayList<Book> books;
    private String TAG = "BookManagerService";
    @Override
    public IBinder onBind(Intent intent) {
        books = new ArrayList<>();
        Log.e(TAG, "success onBind:"+getApplicationInfo().processName);
        return iBinder;
    }

    private IBinder iBinder = new IBookManager.Stub() {
        @Override
        public void addBook(Book book) throws RemoteException {
            Log.e(TAG, "addBook:"+ Thread.currentThread().getName());
            books.add(book);
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            Log.e(TAG, "getBookList:"+ Thread.currentThread().getName());
            return books;
        }
    };
    @Override
    public void onCreate() {
        super.onCreate();
        Log.e(TAG, "onCreate: success");
    }
}

接下來將兩個應(yīng)用跑起來就可以在Client和Server間進行進程間通信了拾氓,在客戶端調(diào)用以下方法即會調(diào)用到服務(wù)端的具體實現(xiàn)方法:
iBookManager.addBook(new Book("歷史", 1));
List<Book> books = iBookManager.getBookList();

簡單分析下AIDL為我們生成的文件

/*
 * This file is auto-generated.  DO NOT MODIFY.
 */
package com.example.client;

public interface IBookManager extends android.os.IInterface {
    public static abstract class Stub extends android.os.Binder implements com.example.client.IBookManager {
        private static final java.lang.String DESCRIPTOR = "com.example.client.IBookManager";
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
        public static com.example.client.IBookManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.client.IBookManager))) {
                return ((com.example.client.IBookManager) iin);
            }
            return new com.example.client.IBookManager.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.client.Book _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.example.client.Book.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_getBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.example.client.Book> _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.example.client.IBookManager {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public void addBook(com.example.client.Book book) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((book != null)) {
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public java.util.List<com.example.client.Book> getBookList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.example.client.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.example.client.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    public void addBook(com.example.client.Book book) throws android.os.RemoteException;

    public java.util.List<com.example.client.Book> getBookList() throws android.os.RemoteException;
}

這里主要生成了兩個類,一個Stub和一個Proxy底哥,都實現(xiàn)了IBookManager接口咙鞍,很顯然這是代理模式

  • 從客戶的綁定服務(wù)開始,可以看到在ServiceConnection的onServiceConnected方法中返回了一個IBinder參數(shù)service趾徽,通過傳入方法IBookManager.Stub.asInterface(service)中獲取一個客戶端代理類续滋,
  • 在Stub的asInterface()中,queryLocalInterface作用是判斷是否是在同一個進程中孵奶,如果是直接返回當(dāng)前對象疲酌,無需跨進程,如果不是拒课,則將遠程返回的BinderProxy(也實現(xiàn)了IBinder)傳入Proxy徐勃,創(chuàng)建一個本地客戶端代理類
  • 當(dāng)調(diào)用代理類的iBookManager.addBook(new Book("歷史", 1));時事示,將對象序列化入?yún)⒄{(diào)用mRemote.transact()(這個函數(shù)三個重要參數(shù):int code、Parcel data僻肖、Parcel reply肖爵,分別對應(yīng)了被調(diào)函數(shù)編號、參數(shù)包臀脏、響應(yīng)包)劝堪,這里就通過Binder驅(qū)動,調(diào)到了遠程服務(wù)端的Stub(繼承Binder實現(xiàn)IBookManger)的onTransact()方法揉稚,根據(jù)入?yún)⒍ㄎ痪唧w方法秒啦,進而調(diào)用到服務(wù)端的Binder中addBook()方法。getBookList()方法調(diào)用方式一樣

整體流程很簡單搀玖,通過綁定遠程服務(wù)-->獲取遠程BinderProxy代理引用-->作為入?yún)?chuàng)建本地客戶端Proxy-->調(diào)用本地Proxy方法--->mRemote.transact()-->通過Binder底層驅(qū)動處理-->調(diào)用到遠程Stub的onTransact方法-->最終調(diào)用到遠程服務(wù)的目標實現(xiàn)方法余境。雖然服務(wù)端和客戶端有著一模一樣的代碼,可以看出Stub主要給服務(wù)端使用灌诅,而Proxy主要是給客戶端使用

類關(guān)系圖如下:
AIDL類關(guān)系圖.png

bindService()如何獲取到遠程服務(wù)Binder引用

進程間通信客戶端如何從服務(wù)端獲取Binder引用是關(guān)鍵芳来,拿到了引用才能調(diào)用其遠程方法,接下來簡單分析下bindService()源碼(Android-23)猜拾。

@Override
    public boolean bindService(Intent service, ServiceConnection conn,
            int flags) {
        return mBase.bindService(service, conn, flags);
    }

  • mBase是上下文context即舌,而調(diào)用context的bindService()實際上實際上是調(diào)用它的實現(xiàn)類ContextImpl的bindService()方法,所以最終會調(diào)用到ContextImpl的bindServiceCommon()方法:
    bindServiceCommon.png

1.構(gòu)建IServiceConnection后面用于給客戶端回傳服務(wù)端Binder引用挎袜。
2.ActivityManagerNative.getDefault()獲取的其實是ActivityManagerService本地客戶端(即當(dāng)前用戶進程)的代理類顽聂,調(diào)用ActivityManagerService代理類的bindService()方法,最終通過Binder驅(qū)動會跨進程調(diào)用Systemserver進程中ActivityManagerServicebindService()方法盯仪。

  • 看下ActivityManagerNative.getDefault()
ActivityManagerNative.getDefault().png
  1. 由于AMS所在進程是Systemserver進程紊搪,因此應(yīng)用進程調(diào)用系統(tǒng)服務(wù)ActivityManagerService的方法也需要跨進程,那就必須拿到(即Systemserver進程)ActivityManagerService服務(wù)的Binder引用全景,才能跨進程通信嗦明,所以上面代碼1處即通過ServiceManager獲取ActivityManagerService在遠程服務(wù)端的Binder引用,然后在2處傳入本地客戶端代理蚪燕,最終返回客戶端代理類ActivityManagerProxy娶牌,當(dāng)調(diào)用代理類的bindService()時即會跨進程調(diào)用服務(wù)端ActivityManagerService方法綁定服務(wù)。可以看出這里是一次跨進程調(diào)用馆纳,應(yīng)用進程與Systemserver進程的通信诗良,通過系統(tǒng)服務(wù)ActivityManagerService去綁定一個服務(wù)
  2. 而1處是如何拿到AMS遠程服務(wù)的Binder引用的呢?
  • 接著看下ServiceManager.getService("activity")
 public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }
private static IServiceManager getIServiceManager() {
        if (sServiceManager != null) {
            return sServiceManager;
        }
        // Find the service manager
        sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
        return sServiceManager;
    }

根據(jù)系統(tǒng)服務(wù)名鲁驶,先從Cache中查找是否已有鉴裹,沒有的話則通過getIServiceManager().getService(name)獲取,
getIServiceManager()中通過BinderInternal.getContextObject()獲取遠程服務(wù)端ServiceManager服務(wù)的Binder引用,并創(chuàng)建本地代理類径荔,通過代理類的getService()方法調(diào)用遠程服務(wù)的getService()方法獲取ActivityManagerService系統(tǒng)服務(wù)督禽。
可以看到這里又是一次進程間通信,通過ServiceManager獲取系統(tǒng)服務(wù)ActivityManagerService的Binder引用還需要與ServiceManager所在進程跨進程通信总处,那么與ServiceManager服務(wù)通信的Binder引用又從哪里來呢狈惫??鹦马?
這里簡單介紹下ServiceManager服務(wù)胧谈,進程間通信的客戶端想要獲取服務(wù)端的 Bind引用都需要通過它來獲取,而所有服務(wù)端都需要向ServiceManager注冊自己的Binder以供客戶端使用荸频,由于其他進程與ServiceManager服務(wù)進程通信也必須獲取它的Binder引用菱肖,所以ServiceManager 提供的 Binder 比較特殊,它沒有名字也不需要注冊旭从。當(dāng)一個進程使用BINDERSETCONTEXT_MGR 命令將自己注冊成 ServiceManager時 Binder 驅(qū)動會自動為它創(chuàng)建 Binder 實體稳强,其次這個 Binder 實體的引用在所有 Client 中都固定為 0 而無需通過其它手段獲得。也就是說和悦,一個 Server 想要向ServiceManager注冊自己的 Binder 就必須通過這個 0 號引用Binder和 ServiceManager服務(wù)進行通信键袱。

  • 回到ActivityManagerNative.getDefault().bindService(),經(jīng)過上面的分析可以知道其實這里調(diào)用是用戶應(yīng)用進程的ActivityManagerService代理類(其實現(xiàn)IActivityManager接口)的bindService()摹闽,即是:ActivityManagerProxy.bindService()
public int bindService(IApplicationThread caller, IBinder token,
            Intent service, String resolvedType, IServiceConnection connection,
            int flags,  String callingPackage, int userId) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        data.writeInterfaceToken(IActivityManager.descriptor);
        data.writeStrongBinder(caller != null ? caller.asBinder() : null);
        data.writeStrongBinder(token);
        service.writeToParcel(data, 0);
        data.writeString(resolvedType);
        data.writeStrongBinder(connection.asBinder());
        data.writeInt(flags);
        data.writeString(callingPackage);
        data.writeInt(userId);
        mRemote.transact(BIND_SERVICE_TRANSACTION, data, reply, 0);
        reply.readException();
        int res = reply.readInt();
        data.recycle();
        reply.recycle();
        return res;
    }

其實就是AIDL中代理類的方法,mRemote.transact(BIND_SERVICE_TRANSACTION, data, reply, 0);最終會通過這個方法調(diào)用到遠程服務(wù)ActivityManagerServiceonTransact()方法褐健,ActivityManagerService繼承自ActivityManagerNative付鹿,并實現(xiàn)了IActivityManager中相應(yīng)的方法。
所以先看下ActivityManagerNative中的transact()方法中對應(yīng)的BIND_SERVICE_TRANSACTION位置:

ActivityManagerNative.png

上述代碼1處即調(diào)用ActivityManagerServicebindService()方法蚜迅,至此舵匾,從用戶進程切換到了AMS所在進程進行綁定服務(wù)操作。
AMS服務(wù)中的相關(guān)AIDL類關(guān)系:
image.png

進入到ActivityManagerService中谁不,看下bindService具體實現(xiàn)流程

由于細節(jié)太多坐梯,這里僅僅列出主線,可自行根據(jù)主線方法名跟入源碼仔細研究

  • ActivityManagerService#bindService會再調(diào)用ActiveServices#bindServiceLocked,
    ActiveServices#bindServiceLocked中如果目標進程會去創(chuàng)建目標進程刹帕,如果服務(wù)未啟動吵血,會啟動服務(wù)
  • ActiveServices中啟動服務(wù)端調(diào)用流程:bindServiceLocked()-->bringUpServiceLocked()-->realStartServiceLocked()-->app.thread.scheduleCreateService()在這里app.thread其實又是一個跨進程通信偷溺,從AMS進程進入到目標用戶進程蹋辅,其AIDL接口是IApplicationThread,Stub是ApplicationThreadNative挫掏,Proxy是ApplicationThreadProxy侦另,遠程服務(wù)端實現(xiàn)是ActivityThread內(nèi)部類ApplicationThread,它繼承自ApplicationThreadNative(這里用戶進程變成了服務(wù)端,AMS服務(wù)所在進程變成了客戶端),所以先看下客戶端代理類的ApplicationThreadProxy#scheduleCreateService方法:
    image.png

可以看到調(diào)用了s.onBind褒傅,就是我們在服務(wù)端服務(wù)中定義的onBind方法
獲取我們的Binder對象弃锐,接著又調(diào)用ActivityManagerNative.getDefault()publishService(),這里非常眼熟殿托,與前面的bindService就很類似霹菊,又是跨進程通信,拿到AMS的客戶端代理碌尔,執(zhí)行它的publishService()浇辜,最終又會交給AMS去執(zhí)行,不再重復(fù)看

  • 直接到ActivityManagerService中找publishService()方法唾戚,發(fā)現(xiàn)它又會調(diào)用ActiveServices#publishServiceLocked()
 void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {
        final long origId = Binder.clearCallingIdentity();
        try {
            if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "PUBLISHING " + r
                    + " " + intent + ": " + service);
            if (r != null) {
                Intent.FilterComparison filter
                        = new Intent.FilterComparison(intent);
                IntentBindRecord b = r.bindings.get(filter);
                if (b != null && !b.received) {
                    b.binder = service;
                    b.requested = true;
                    b.received = true;
                    for (int conni=r.connections.size()-1; conni>=0; conni--) {
                        ArrayList<ConnectionRecord> clist = r.connections.valueAt(conni);
                        for (int i=0; i<clist.size(); i++) {
                            ConnectionRecord c = clist.get(i);
                            if (!filter.equals(c.binding.intent.intent)) {
                                if (DEBUG_SERVICE) Slog.v(
                                        TAG_SERVICE, "Not publishing to: " + c);
                                if (DEBUG_SERVICE) Slog.v(
                                        TAG_SERVICE, "Bound intent: " + c.binding.intent.intent);
                                if (DEBUG_SERVICE) Slog.v(
                                        TAG_SERVICE, "Published intent: " + intent);
                                continue;
                            }
                            if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Publishing to: " + c);
                            try {
                                c.conn.connected(r.name, service);
                            } catch (Exception e) {
                                Slog.w(TAG, "Failure sending service " + r.name +
                                      " to connection " + c.conn.asBinder() +
                                      " (in " + c.binding.client.processName + ")", e);
                            }
                        }
                    }
                }

                serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

可以看到核心部分c.conn.connected(r.name, service)柳洋,(其實這里涉及到一開始bindServiceCommon中構(gòu)建的IServiceConnection,在LoadedApk中的ServiceDispatcher.InnerConnection中叹坦,其實本質(zhì)又是一次跨進程通信熊镣,感興趣的可以看下源碼)這里就會再調(diào)用到我們在客戶端綁定服務(wù)時傳入的ServiceConnection的onServiceConnected方法,最終客戶端拿到服務(wù)端IBinder的引用D际椤P鞔选!
至此莹捡,終于結(jié)束了鬼吵,可以發(fā)現(xiàn)為了實現(xiàn)自定義的一次跨進程通信,其內(nèi)部經(jīng)歷了無數(shù)次跨進程篮赢。齿椅。。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末启泣,一起剝皮案震驚了整個濱河市涣脚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寥茫,老刑警劉巖遣蚀,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異纱耻,居然都是意外死亡芭梯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人圣贸,你說我怎么就攤上這事∶⑽校” “怎么了柴灯?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長费尽。 經(jīng)常有香客問我赠群,道長,這世上最難降的妖魔是什么旱幼? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任查描,我火速辦了婚禮,結(jié)果婚禮上柏卤,老公的妹妹穿的比我還像新娘冬三。我一直安慰自己,他們只是感情好缘缚,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布勾笆。 她就那樣靜靜地躺著,像睡著了一般桥滨。 火紅的嫁衣襯著肌膚如雪窝爪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天齐媒,我揣著相機與錄音蒲每,去河邊找鬼。 笑死喻括,一個胖子當(dāng)著我的面吹牛邀杏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播唬血,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼望蜡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了刁品?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤浩姥,失蹤者是張志新(化名)和其女友劉穎挑随,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勒叠,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡兜挨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了眯分。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拌汇。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖弊决,靈堂內(nèi)的尸體忽然破棺而出噪舀,到底是詐尸還是另有隱情魁淳,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布与倡,位于F島的核電站界逛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纺座。R本人自食惡果不足惜息拜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望净响。 院中可真熱鬧少欺,春花似錦、人聲如沸馋贤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掸掸。三九已至氯庆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扰付,已是汗流浹背堤撵。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留羽莺,地道東北人实昨。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像盐固,于是被迫代替她去往敵國和親荒给。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354