Android-IPC系列(一)

未經(jīng)博主同意缰儿,不得轉(zhuǎn)載該篇文章

前言

IPC-進程間通信。安卓雖然是一個基于linux內(nèi)核的系統(tǒng)琢唾,但是安卓卻有自己的一套IPC機制楞件。想要弄懂安卓的IPC機制首先要理解幾個framework層的概念,安卓的序列化機制以及binder和AIDL的實現(xiàn)流程(其它基于binder的ipc實現(xiàn)方式就不再多說了~)视粮。在寫這篇博客之前反復(fù)看了幾遍《安卓開發(fā)藝術(shù)探索》的第二章细办,以及凱子哥(csdn)的framework層的文章and與一位大神學(xué)長交流,以保證文章的可靠性蕾殴。這篇文章我將以自己的語言總結(jié)對于ipc的學(xué)習(xí)成果~

Demo地址:IPC-demo

幾個概念

1.Binder:

Binder是安卓的一個類笑撞,實現(xiàn)了Binder接口(后面多次出現(xiàn))。Binder我理解成安卓IPC機制的核心钓觉,也就是實現(xiàn)IPC的核心工具茴肥。它不僅用于安卓開發(fā)層還用于安卓的framework層。在架構(gòu)層里面binder是各種系統(tǒng)manager之間連接的工具(WindowManager, ActivityManager等等)荡灾。開發(fā)層用于service瓤狐,別告訴我你不知道service里面會返回一個IBinder。

2.AIDL:

前不久在一個安卓群看到有個面試官問別人aidl是什么批幌。础锐。別人當(dāng)時就蒙蔽了。荧缘。

AIDL(Android Interface Definition Language)皆警,安卓接口定義語言。它是安卓實現(xiàn)IPC通信的一種比較重要的方式胜宇,并且底層基于binder耀怜。所以我們就講這個啦!

3.linux的進程

這個問題我專門請教了一個學(xué)長~

Linux沒有很嚴格的純粹進程概念桐愉。一堆線程共享一塊內(nèi)存區(qū)域就是一個進程财破。當(dāng)你的手機啟動的時候,系統(tǒng)會啟動一個init進程从诲,這個應(yīng)該可以成為Linux的主進程了左痢。之后的所有進程都是從init進程fork出來的,比如zygote進程和SystemServer進程系洛。

4.zygote進程

顧名思義(受精卵)俊性,這個進程會像個受精卵一樣不停的“分裂”,去fork出別的進程描扯。幾乎后面出現(xiàn)的所有進程都是從這個進程fork出來的定页。包括SystemServer進程,ActivityManagerService等等绽诚。但是具體來說典徊,AMS是SystemServer里面fork出來的杭煎。也許你會問為什么要這樣做,那是因為這樣設(shè)計更高效(當(dāng)然我只是個普通的開發(fā)者卒落,并不懂那些大神是怎么想的哈哈)羡铲。

4.ActivityManagerService:

這個玩意,我覺得是相當(dāng)重要的儡毕,為什么這么說呢也切,因為它管理著手機中所有Activity的生死。你說重不重要腰湾?雷恃??當(dāng)你打開一個app后檐盟,AMS會立馬在zygote里面fork一個進程出來褂萧,并且復(fù)制一個虛擬機(Dalvik or ART)和一些資源以及一個線程(是的押桃,這就是UI線程~葵萎,不要懷疑自己!)唱凯。啟動一個app是AMS和Lanucher, ActivityThread一起合作做到的羡忘。具體的實現(xiàn),自己可以去看看別的文章磕昼,這不是我們要講的重點卷雕。另外說一點,AMS, activity之間也是通過binder來進行通信票从,你要知道漫雕,AMS, zygote, activity都是在不同的進程里面。

5.App與進程:

一個app對應(yīng)一個進程峰鄙。這種說法我不太敢茍同浸间。首先一個app,一個進程這種說法太模糊吟榴,因為app是可以設(shè)置多進程的哇魁蒜。。(設(shè)置組件的process)吩翻,所以我覺得多進程的app應(yīng)該看成共享apk資源的多個應(yīng)用兜看。

6.ShareUID:

之前在一個群里面聽前輩們討論app資源共享的問題,多次看到這個單詞狭瞎,當(dāng)時在想细移,臥槽,這tm什么鬼熊锭?弧轧!

這個東西你可以大概理解成每個apk的ID缔刹。一個apk對應(yīng)一個uid,所以一個app里面跑在同一個進程里面的組件數(shù)據(jù)可以共享劣针。如果一個app里面某個組件讓他跑在別的進程里面校镐,相當(dāng)于是創(chuàng)建了一個新的application,這個組件跟自己app里面的其它組件并沒有多大關(guān)系捺典。然后ipc就可以起作用了鸟廓,通過binder進行進程間通信。如果兩個屬于不同app的組件襟己,自然是有不同的application和虛擬機了引谜,然后通過簽名文件和uid來進行數(shù)據(jù)共享。普通的資源文件比如string, color這些文件是不需要相同的uid就可以訪問的擎浴,但是data里面的數(shù)據(jù)是需要這樣的员咽。具體怎么做,自己有興趣也可以去查查資料~

7.序列化與反序列化:

首先你要知道贮预,數(shù)據(jù)的傳輸都是要把數(shù)據(jù)轉(zhuǎn)換成字節(jié)碼贝室,不管你是什么類型的數(shù)據(jù)。(嗯仿吞,沒有例外;怠)序列化就是把數(shù)據(jù)轉(zhuǎn)換成字節(jié)碼的過程,而反序列化自然就是在數(shù)據(jù)傳輸?shù)哪康牡匕炎止?jié)碼轉(zhuǎn)換成原始數(shù)據(jù)唤冈。之前自己為了方便峡迷,所有都使用Serializable來做序列化。后來知道Parcelable的效率更高你虹。因為Serializable要做大量的IO操作绘搞。所以以后都要使用安卓里面的Parcelable來做序列化哦~

好了,基本的概念介紹完畢傅物。夯辖!如果我有說的不合理的地方請大家無情給我指出!挟伙!啪啪的打我的臉


Binder的工作原理

我們直接通過aidl文件生成的源碼來理解binder的工作原理楼雹!

首先新建一個aidl的文件包。聲明三個aidl文件尖阔。并且在java包里面聲明我們要傳輸?shù)念怋ook贮缅。

ipc1.png

代碼如下:

/**
 * Created by Zane on 16/3/16.
 */
public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(bookId);
        dest.writeString(bookName);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>(){

        public Book createFromParcel(Parcel in){
            return new Book(in);
        }

        public Book[] newArray(int size){
            return new Book[size];
        }
    };

    private Book(Parcel in){
        bookId = in.readInt();
        bookName = in.readString();
    }

    @Override
    public String toString() {
        return "bookId " + bookId +" bookName " + bookName;
    }
}


// Book.aidl
package com.example.zane.ipc_test;
parcelable Book;


// IBookManager.aidl
package com.example.zane.ipc_test;
import com.example.zane.ipc_test.Book;
import com.example.zane.ipc_test.IOnNewBookArrivedListener;
interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrivedListener listener);
    void unRegisterListener(IOnNewBookArrivedListener listener);
}


// IOnNewBookArrivedListener.aidl
package com.example.zane.ipc_test;
//監(jiān)聽服務(wù)端是否有新書籍,如果有新書籍就立即推送到客戶端,觀察者模式
import com.example.zane.ipc_test.Book;
interface IOnNewBookArrivedListener {
    void newBookArrived(in Book book);
}

好了介却,咱先不管IOnNewBookArrivedListener.aidl這個文件谴供。我們分析IBookManager.aidl的生成源碼。項目包里面的gen目錄下面有一個xxx.aidl包里面會有一個IBookManager.java的文件齿坷。我們就是要分析它桂肌!嗯数焊,搞掂它!

當(dāng)我用sublime打開它之后崎场,老子差點吐掉佩耳。。

ipc2.png

谭跨。干厚。然后我憑借我的強迫癥一個個的給它縮進!我就是這么雷鋒螃宙。蛮瞄。

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: /Users/Zane/編程/AndroidStudioProjects 13-52-23-071/IPC_Test/app/src/main/aidl/com/example/zane/ipc_test/IBookManager.aidl
 */
package com.example.zane.ipc_test;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
  public static abstract class Stub extends android.os.Binder implements com.example.zane.ipc_test.IBookManager
  {

    //binder的唯一標識符
    private static final java.lang.String DESCRIPTOR = "com.example.zane.ipc_test.IBookManager";


/** Construct the stub at attach it to the interface. */
    public Stub()
    {
      this.attachInterface(this, DESCRIPTOR);
    }

/**
 * Cast an IBinder object into an com.example.zane.ipc_test.IBookManager interface,
 * generating a proxy if needed.
 */
    public static com.example.zane.ipc_test.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.zane.ipc_test.IBookManager))) {
        return ((com.example.zane.ipc_test.IBookManager)iin);
      }
      return new com.example.zane.ipc_test.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_getBookList:
        {
          data.enforceInterface(DESCRIPTOR);
          java.util.List<com.example.zane.ipc_test.Book> _result = this.getBookList();
          reply.writeNoException();
          reply.writeTypedList(_result);
          return true;
        }
        case TRANSACTION_addBook:
        {
          data.enforceInterface(DESCRIPTOR);
          com.example.zane.ipc_test.Book _arg0;
          if ((0!=data.readInt())) {
            _arg0 = com.example.zane.ipc_test.Book.CREATOR.createFromParcel(data);
          }
          else {
            _arg0 = null;
          }
          this.addBook(_arg0);
          reply.writeNoException();
          return true;
        }
      }
    return super.onTransact(code, data, reply, flags);
  }

  private static class Proxy implements com.example.zane.ipc_test.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 java.util.List<com.example.zane.ipc_test.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.zane.ipc_test.Book> _result;
      try {
        _data.writeInterfaceToken(DESCRIPTOR);
        mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
        _reply.readException();
        _result = _reply.createTypedArrayList(com.example.zane.ipc_test.Book.CREATOR);
      }
      finally {
        _reply.recycle();
        _data.recycle();
      }
      return _result;
    }

    @Override
    public void addBook(com.example.zane.ipc_test.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();
      }
    }
  }

    static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
  public java.util.List<com.example.zane.ipc_test.Book> getBookList() throws android.os.RemoteException;
  public void addBook(com.example.zane.ipc_test.Book book) throws android.os.RemoteException;
}

如果,你是第一次接觸這個谆扎,或者以前沒看過什么源碼挂捅,內(nèi)心應(yīng)該跟我之前一樣,也是崩潰的堂湖。但是闲先,我們是程序員,在源碼面前千萬不能低頭苗缩!大概看一遍饵蒂,Proxy這個類看到?jīng)]声诸,嗯酱讶,沒錯了,用到了代理彼乌。再看Stub這個這個內(nèi)部類泻肯,繼承了什么?大聲告訴我慰照!嗯灶挟,就是Binder類。沒錯毒租,這個Stub就是后面多次用到的Binder稚铣!其實這么多代碼,只需要理解Stub, Proxy這兩個類就差不多了墅垮。我們來細看代碼:

asInterface(Binder obj):

public static com.example.zane.ipc_test.IBookManager asInterface(android.os.IBinder obj)
    {

      if ((obj==null)) {
        return null;
      }

      //查詢本地的binder
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);

      if (((iin!=null)&&(iin instanceof com.example.zane.ipc_test.IBookManager))) {
        return ((com.example.zane.ipc_test.IBookManager)iin);
      }

      return new com.example.zane.ipc_test.IBookManager.Stub.Proxy(obj);

    }

這個方法在后面使用的部分會多次用到惕医。作用就是將服務(wù)端的Binder對象轉(zhuǎn)換成客戶端所需要的AIDL接口類型的對象。不知道你有沒有感受到算色,這其實就是一種類似接口回調(diào)的過程抬伺。在客戶端使用這個方法去得到服務(wù)端的binder類型的接口,然后調(diào)用服務(wù)端的方法灾梦。

代碼很簡單峡钓,如果客戶端和服務(wù)端在一個進程那么就返回這個binder妓笙,如果是多進程就返回遠程代理類Proxy的實例。

寫到這里能岩,突然感冒加重有點發(fā)燒的感覺寞宫。。拉鹃。orz淆九,我還是堅持下去!

我們再來看Proxy類里面的兩個實現(xiàn)了IBookManager接口的方法:

@Override
    public java.util.List<com.example.zane.ipc_test.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.zane.ipc_test.Book> _result;
      try {
        _data.writeInterfaceToken(DESCRIPTOR);
        //調(diào)用onTransact()方法
        mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
        //讀取異常
        _reply.readException();
        //獲得返回值并返回給客戶端
        _result = _reply.createTypedArrayList(com.example.zane.ipc_test.Book.CREATOR);
      }
      finally {
        _reply.recycle();
        _data.recycle();
      }
      return _result;
    }

data是輸入對象毛俏,reply是輸出對象炭庙,result是最后返回給客戶端的數(shù)據(jù)。這個是getBookList的方法煌寇,沒有參數(shù)焕蹄,只有返回值,所以data寫入token標識符之后就直接調(diào)用了Stub類里面的onTransact()方法阀溶。在onTransact()方法里面把結(jié)果值寫入reply并且返回true表示客戶端與服務(wù)端連接成功腻脏。如果返回false就表示連接失敗银锻!再看addBook(Book book)的方法:

@Override
    public void addBook(com.example.zane.ipc_test.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);
        //防止傳入的參數(shù)為null
        if ((book!=null)) {
          _data.writeInt(1);
          book.writeToParcel(_data, 0);
        }
        else {
          _data.writeInt(0);
        }
        //調(diào)用onTransact()方法
          mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
          _reply.readException();
      }
      finally {
        _reply.recycle();
        _data.recycle();
      }
    }
  }

我想如果你不是特別傻永品,應(yīng)該可以類比上面看得懂這個吧!我在上面寫了一些注釋击纬。然后就來看Stub是來如何響應(yīng)RPC(遠程過程調(diào)用)的鼎姐。

@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_getBookList:
        {
          data.enforceInterface(DESCRIPTOR);
          java.util.List<com.example.zane.ipc_test.Book> _result = this.getBookList();
          reply.writeNoException();
          reply.writeTypedList(_result);
          return true;
        }
        case TRANSACTION_addBook:
        {
          data.enforceInterface(DESCRIPTOR);
          com.example.zane.ipc_test.Book _arg0;
          if ((0!=data.readInt())) {
            _arg0 = com.example.zane.ipc_test.Book.CREATOR.createFromParcel(data);
          }
          else {
            _arg0 = null;
          }
          this.addBook(_arg0);
          reply.writeNoException();
          return true;
        }
      }
    return super.onTransact(code, data, reply, flags);
  }

這個方法通過傳進來的不同code來響應(yīng)客戶端不同的請求。

  1. TRANSACTION_getBookList里面更振,首先調(diào)用獲得服務(wù)端的List炕桨,然后寫入reply。返回true表示連接成功肯腕,并且讓客戶端從reply里面read出來返回過來的數(shù)據(jù)献宫。

  2. TRANSACTION_addBook里面,通過Parcelable去new出了一個新的Book實例实撒。然后調(diào)用服務(wù)端的addBook方法姊途,把這個實例返回給服務(wù)端。你如果第一次使用Parceable可能會不理解怎么new出新的Book的知态。你回頭去看看你的Book類你就懂了捷兰!

嗯!就是這么簡單肴甸!源碼我們基本就分析完了寂殉!我們再來總結(jié)一下流程。

  1. 客戶端發(fā)出請求原在,然后將客戶端發(fā)送請求的線程掛起(這個下篇文章再說)
  2. Binder寫入?yún)?shù)(從客戶端傳入)到data友扰,如果沒有則不寫入彤叉。
  3. 調(diào)用transact()方法
  4. onTransact()響應(yīng),調(diào)用Service(服務(wù)端)的實現(xiàn)方法村怪,并且把客戶端需要的數(shù)據(jù)寫入reply秽浇,如果沒有則不寫入。
  5. result讀出數(shù)據(jù)返回客戶端甚负,喚醒客戶端柬焕,客戶端獲得數(shù)據(jù)。

總結(jié)

開始準備一篇文章直接寫完梭域。斑举。發(fā)現(xiàn)得需要兩篇了!

這篇文章我們解決了一些IPC的基本概念知識and通過aidl學(xué)習(xí)binder的工作原理病涨。

啊啊富玷,,既穆,突然發(fā)燒了臥槽赎懦,好難受,下篇我們接著說AIDL的使用幻工!

未經(jīng)博主同意励两,不得轉(zhuǎn)載該篇文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市囊颅,隨后出現(xiàn)的幾起案子当悔,更是在濱河造成了極大的恐慌,老刑警劉巖迁酸,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件先鱼,死亡現(xiàn)場離奇詭異,居然都是意外死亡奸鬓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門掸读,熙熙樓的掌柜王于貴愁眉苦臉地迎上來串远,“玉大人,你說我怎么就攤上這事儿惫≡璺#” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵肾请,是天一觀的道長留搔。 經(jīng)常有香客問我,道長铛铁,這世上最難降的妖魔是什么隔显? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任却妨,我火速辦了婚禮,結(jié)果婚禮上括眠,老公的妹妹穿的比我還像新娘彪标。我一直安慰自己,他們只是感情好掷豺,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布捞烟。 她就那樣靜靜地躺著,像睡著了一般当船。 火紅的嫁衣襯著肌膚如雪题画。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天德频,我揣著相機與錄音婴程,去河邊找鬼浩村。 笑死礼烈,一個胖子當(dāng)著我的面吹牛彪杉,可吹牛的內(nèi)容都是我干的诈泼。 我是一名探鬼主播绷雏,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼喷市,長吁一口氣:“原來是場噩夢啊……” “哼瞄沙!你這毒婦竟也來了为朋?” 一聲冷哼從身側(cè)響起患亿,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤传蹈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后步藕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惦界,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年咙冗,在試婚紗的時候發(fā)現(xiàn)自己被綠了沾歪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡雾消,死狀恐怖灾搏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情立润,我是刑警寧澤狂窑,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站桑腮,受9級特大地震影響泉哈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一丛晦、第九天 我趴在偏房一處隱蔽的房頂上張望奕纫。 院中可真熱鬧,春花似錦采呐、人聲如沸若锁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽又固。三九已至,卻和暖如春煤率,著一層夾襖步出監(jiān)牢的瞬間仰冠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工蝶糯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洋只,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓昼捍,卻偏偏與公主長得像识虚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妒茬,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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