Android探索之旅 | AIDL原理和实例讲解

-- 作者 谢恩铭 转载请注明出处

前言


为使应用程序之间能够彼此通信,Android提供了IPC (Inter Process Communication,进程间通信)的一种独特实现: AIDL (Android Interface Definition Language, Android接口定义语言)。

网上有不少关于AIDL的文章,写得都很不错。不过例子构造大多略微复杂: 建立两个Android项目,一个是client(客户端),一个是server(服务端,提供service(服务))。

这篇文章将首先介绍AIDL的原理,再通过一个Android项目来介绍AIDL用法。服务端和客户端包含在这同一个项目中,原理和分别在两个项目中是一样的,不过轻省许多。

源码在我的Github上,文末有放出。

这篇博文包含以下四个部分:


  1. AIDL介绍
  2. 实现步骤
  3. 实例: HelloSumAIDL
    3.1 创建工程
    3.2 定义AIDL文件
    3.3 实现远程服务(Service)
    3.4 “暴露”服务
    3.5 相关代码
  4. 后记和源码

1. AIDL介绍


在Android中,默认每个应用(application)执行在它自己的进程中,无法直接调用到其他应用的资源,这也符合“沙箱”(SandBox)的理念。所谓沙箱原理,一般来说用在移动电话业务中,简单地说旨在部分地或全部地隔离应用程序。

Android沙箱技术:
Android“沙箱”的本质是为了实现不同应用程序和进程之间的互相隔离,即在默认情况 下,应用程序没有权限访问系统资源或其它应用程序的资源。
每个APP和系统进程都被分配唯一并且固定的User Id(用户身份标识),这个uid与内核层进程的uid对应。
每个APP在各自独立的Dalvik虚拟机中运行,拥有独立的地址空间和资源。
运行于Dalvik虚拟机中的进程必须依托内核层Linux进程而存在,因此Android使用Dalvik虚拟机和Linux的文件访问控制来实现沙箱机制,任何应用程序如果想要访问系统资源或者其它应用程序的资源必须在自己的manifest文件中进行声明权限或者共享uid。
本段关于沙箱的解释转载自:Android的权限机制之—— “沙箱”机制sharedUserId和签名

因此,在Android中,当一个应用被执行时,有一些操作是被限制的,比如访问内存,访问传感器,等等。这样做可以最大化地保护系统,免得应用程序“为所欲为”。

那我们有时需要在应用间交互,怎么办呢?于是,Android需要实现IPC协议。

关于IPC协议,可以参看下面摘自维基百科的内容:

进程间通信(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法。
进程是计算机系统分配资源的最小单位(严格说来是线程)。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。
为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。举一个典型的例子,使用进程间通信的两个应用可以被分类为客户端和服务器(主从式架构),客户端进程请求数据,服务端回复客户端的数据请求。有一些应用本身既是服务器又是客户端,这在分布式计算中,时常可以见到。这些进程可以运行在同一计算机上或网络连接的不同计算机上。
进程间通信技术包括消息传递、同步、共享内存和远程过程调用(Remote Procedure Call,缩写是RPC)。IPC是一种标准的Unix通信机制。

使用IPC 的理由:

  • 信息共享:Web服务器,通过网页浏览器使用进程间通信来共享web文件(网页等)和多媒体。
  • 加速:维基百科使用通过进程间通信进行交流的多服务器来满足用户的请求。
  • 模块化。
  • 私有权分离。

与直接共享内存地址空间的多线程编程相比,IPC的缺点:

  • 采用了某种形式的内核开销,降低了性能;
  • 几乎大部分IPC都不是程序设计的自然扩展,往往会大大地增加程序的复杂度。

对于进程和线程的联系和区别,可以参看阮一峰老师的这篇图文:进程与线程的一个简单解释,非常形象生动。

关于Android中的进程和线程,可以参看官方开发文档:
https://developer.android.com/guide/components/processes-and-threads.html
(国内的朋友也可以去这里:https://developer.android.google.cn/guide/components/processes-and-threads.html


我们知道Android中要实现IPC,有好多种方式:

  1. 在Intent中附加extras来传递信息。
  2. 共享文件。
  3. SharedPreferences(不建议在进程间通信中使用,因为在多进程模式下,系统对SharedPreferences的读/写会变得不可靠,面对高并发的读/写访问,有很大几率会丢失数据)。
  4. 基于Binder的AIDL。
  5. 基于Binder的Messenger(翻译为“信使”,其实Messenger本质上也是AIDL,只不过系统做了封装以方便上层调用)。
  6. Socket。
  7. 天生支持跨进程访问的ContentProvider。

然而,如果我们要在Android中自己来实现IPC这个协议,还是有点复杂的,主要因为需要实现数据管理系统(在进程或线程间传递数据)。为了暂时减缓这个“会呼吸的痛”,Android为我们实现了一种定制的IPC,也就是梁静茹,oh,sorry,是AIDL。

不要把AIDL和JNI及NDK混淆起来,这几个的功用是这样的:

  • AIDL:是Android中IPC(进程间通信)的一种方式, 因为Android中不同应用一般是位于不同进程中的,而即使同一个应用中的组件(component。参看Android四大组件:Activity,Service,ContentProvider,BroadcastReceiver)也可以位于不同进程(通过在AndroidManifest.xml中为组件设置android:process属性来实现)。例如,同一个应用中,如果Activity和Service两者处于不同进程,但Activity 需要给Service传递一些信息,就可以用到AIDL这种机制。
  • JNI:Java Native Interface的缩写,表示“Java原生接口”。为了方便Java调用Native(原生)代码(比如C和C++,等等)所封装的一层接口。JNI是Java语言的东西,并不专属于Android。
  • NDK:Native Development Kit的缩写,表示“原生开发工具集”。NDK是Google为Android开发的工具集,专属于Android。利用NDK,我们可以在Android中更加方便地通过JNI来调用原生代码(比如C和C++,等等)。NDK还提供了交叉编译器,我们只需要简单修改.mk文件就可以生成指定CPU平台的动态库。NDK还有其他一些优势。

2. 实现步骤


在Android官方开发文档中有这么一段话,是关于IPC的:

Android offers a mechanism for interprocess communication (IPC) using remote procedure calls (RPCs), in which a method is called by an activity or other application component, but executed remotely (in another process), with any result returned back to the caller. This entails decomposing a method call and its data to a level the operating system can understand, transmitting it from the local process and address space to the remote process and address space, then reassembling and reenacting the call there. Return values are then transmitted in the opposite direction. Android provides all the code to perform these IPC transactions, so you can focus on defining and implementing the RPC programming interface.

To perform IPC, your application must bind to a service, using bindService(). For more information, see the Services developer guide.

翻译如下
Android利用远程过程调用(Remote Procedure Call,简称RPC)提供了一种进程间通信(Inter-Process Communication,简称IPC)机制,通过这种机制,被Activity或其他应用程序组件调用的方法将(在其他进程中)被远程执行,而所有的结果将被返回给调用者。这就要求把方法调用及其数据分解到操作系统可以理解的程度,并将其从本地的进程和地址空间传输至远程的进程和地址空间,然后在远程进程中重新组装并执行这个调用。执行后的返回值将被反向传输回来。Android提供了执行IPC事务所需的全部代码,因此只要把注意力放在定义和实现RPC编程接口上即可。

要执行IPC,应用程序必须用bindService()绑定到服务上。详情请参阅服务Services开发指南

AIDL是IPC的一个轻量级实现,用到了Java开发者很熟悉的语法。Android也提供了一个工具,可以自动创建Stub。

问:"Stub又是什么呢?"
答:"Stub在英语中是“树桩”的意思,这个stub的概念并不是Android专有的,其他编程开发中也会用到,根据维基百科的解释:

Stub(桩)指用来替换一部分功能的程序段。桩程序可以用来模拟已有程序的行为(比如一个远端机器的过程)或是对将要开发的代码的一种临时替代。因此,打桩技术在程序移植、分布式计算、通用软件开发和测试中用处很大。

因此,简单的说,Android中的Stub是一个类,实现了远程服务的接口,以便你能使用它,就好像此服务是在本地一样。好比在本地打了一个远程服务的“桩”,你就可以用来造房子什么的。"

当我们要在应用间用AIDL来通信时,我们需要按以下几步走:

  1. 定义一个AIDL接口。
  2. 为远程服务(Service)实现对应Stub。
  3. 将服务“暴露”给客户程序使用。

3. 实例: HelloSumAIDL


AIDL的语法很类似Java的接口(Interface),只需要定义方法的签名。

AIDL支持的数据类型与Java接口支持的数据类型有些不同:

  1. 所有基础类型(int, char, 等)
  2. String,List,Map,CharSequence等类
  3. 其他AIDL接口类型
  4. 所有Parcelable的类

为了更好地展示AIDL的用法,我们来看一个很简单的例子: 两数相加。

3.1 创建工程


事不宜迟,我们就用Android Studio创建一个Android项目。

以下是项目的基本信息(不一定要一样):

  • 项目(project)名称: HelloSumAIDL
  • 包(package)名: com.android.hellosumaidl
  • Activity名称: HelloSumAidlActivity
新建项目

点击Next(下一步):

选择平台:手机和平板,及最小SDK

默认配置即可,点击Next(下一步):

选择空白Activity

点击Next(下一步):

填写Activity信息

点击Finish(完成),Android Studio就会开始帮你创建新项目。稍等片刻,即可看到如下图所示的项目:

3.2 创建AIDL文件


此时的项目视图是默认的Android。

鼠标左键选中 HelloSumAIDL/app/src/main/java这个路径,如下图所示:

点击鼠标右键,新建一个AIDL文件(依次选择New->AIDL->AIDL File),取名为 IAdditionService。

填入AIDL文件名 IAdditionService

点击Finish。

Android Studio就会为你新建一个IAdditionService.aidl文件,位于新建的路径 HelloSumAIDL/app/src/main/aidl 中,它的包名也是 com.android.hellosumaidl,因为包名在我们创建项目时已经定了,可以在AndroidManifest.xml文件中可以看到

项目的包名是 com.android.hellosumaidl

在新建的这个IAdditionService.aidl文件中将已有代码替换为如下代码:

package com.android.hellosumaidl;

// Interface declaration (接口声明)
interface IAdditionService {
    // You can pass the value of in, out or inout
    // The primitive types (int, boolean, etc) are only passed by in
    int add(in int value1, in int value2);
}

add是英语“加”的动词。addition是“加”的名词。
AIDL也有一些格式规范,主要是in和out关键字,in代表传入的参数,out代表输出的参数,inout代表传入和输出的参数。Java语言内置的类型(比如int,boolean,等等)只能通过in来传入。

IAdditionService.aidl文件

一旦文件被保存,Android Studio会自动在 HelloSumAIDL/app/build/generated/source/aidl/debug/com/android/hellosumaidl 这个路径(如果你的Favorites是release,那么debug会是release)里自动生成对应的IAdditionService.java这个文件。

为了能看到app/build/generated/中的文件,需要把项目视图从默认的Android改选为Project Files。

从默认的Android改选为Project Files

然后,你就能找到IAdditionService.java这个文件了,如下图所示:

IAdditionService.java文件

在这个文件里,我们可以看到add方法也被自动添加了:

因为IAdditionService.java这个文件是自动生成的,所以无需改动。这个文件里就包含了Stub,可以看到就是

public static abstract class Stub extends android.os.Binder implements com.android.hellosumaidl.IAdditionService

那一行。

我们接下来要为我们的远程服务实现这个Stub。

3.3 实现远程服务


首先我们来理清一下思路,现在我们的项目有两个主要的文件:

  • HelloSumAIDL/app/src/main/java/com/android/hellosumaidl/HelloSumAidlActivity.java :这个HelloSumAidlActivity.java是我们的客户端(client)。

  • HelloSumAIDL/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :这个是AIDL。客户端通过AIDL实现与服务端的通信。

注意:使用AIDL进行客户端和服务端的通信有一个条件需要满足,那就是服务器端的各个AIDL文件(因为aidl目录下也许不止一个文件,我们项目中只创建了一个而已)须要被拷贝到客户端的相同包名下,不然会不成功。例如:

  • HelloSumAIDLServer/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :假如HelloSumAIDLServer是一个表示AIDL服务端的Android项目。
  • HelloSumAIDLClient/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl :假如HelloSumAIDLClient是一个表示AIDL客户端的Android项目。

那么,HelloSumAIDLClient/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl 就要和 HelloSumAIDLServer/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl一样。

我们这篇文章中,因为客户端和服务端是在同一项目中,因此存在一份AIDL文件就够了,就是 HelloSumAIDL/app/src/main/aidl/com/android/hellosumaidl/IAdditionService.aidl。

我们还没有写远程服务端的代码,因此我们来实现之:

在HelloSumAIDL/app/src/main/java/com/android/hellosumaidl 这个路径中新建一个Service,取名叫AdditionService.java。这个就是我们的服务端了。

创建AdditionService.java

为了实现我们的服务,我们需要让这个类中的onBind方法返回一个IBinder类的对象。这个IBinder类的对象就代表了远程服务的实现。

我们要用到自动生成的子类IAdditionService.Stub。在其中,我们也必须实现我们之前在AIDL文件中定义的add()函数。下面是我们远程服务的代码:

package com.android.hellosumaidl;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;

/*
 * This class exposes the service to client
 * 服务端,将服务(service)"暴露"给客户端(client)
 */
public class AdditionService extends Service {
  public AdditionService() {
  }

  @Override
  public IBinder onBind(Intent intent) {
    return new IAdditionService.Stub() {
      /*
       * Implement com.android.hellosumaidl.IAdditionService.add(int, int)
       * 实现了add方法
       */
      @Override
      public int add(int value1, int value2) throws RemoteException {
        return value1 + value2;
      }
    };
  }
}

AdditionService.java(服务端)和HelloSumAidlActivity.java(客户端)被放在同一个路径下:

3.4 “暴露”服务


一旦实现了服务中的onBind方法,我们就可以把客户端程序(在我们的项目里是HelloSumAidlActivity.java)与服务连接起来了。

为了建立这样的一个链接,我们需要实现ServiceConnection类。

我们在HelloSumAidlActivity.java中创建一个内部类 AdditionServiceConnection,这个类继承ServiceConnection类,并且重写了它的两个方法:onServiceConnected和onServiceDisconnected。

下面给出内部类的代码:

  /*
   * This inner class is used to connect to the service
   * 这个内部类用于连接到服务(service)
   */
  class AdditionServiceConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder boundService) {
      service = IAdditionService.Stub.asInterface(boundService);
      Toast.makeText(HelloSumAidlActivity.this, "Service connected", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
      service = null;
      Toast.makeText(HelloSumAidlActivity.this, "Service disconnected", Toast.LENGTH_LONG).show();
    }
  }

3.5 相关代码


为了完成我们的测试项目,我们需要首先改写activity_hello_sum_aidl.xml(主界面的布局文件)和string.xml (字符串定义文件):

布局文件 activity_hello_sum_aidl.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello"
        android:textSize="22sp" />

    <EditText
        android:id="@+id/value1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="@string/hint1" >
    </EditText>

    <TextView
        android:id="@+id/TextView01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/plus"
        android:textSize="36sp" />

    <EditText
        android:id="@+id/value2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="@string/hint2" >
    </EditText>

    <Button
        android:id="@+id/buttonCalc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="@string/equal" >
    </Button>

    <TextView
        android:id="@+id/result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/result"
        android:textSize="36sp" />

</LinearLayout>

string.xml

<resources>
    <string name="app_name">HelloSumAIDL</string>
    <string name="hello">Hello Sum AIDL</string>
    <string name="result">Result</string>

    <string name="plus">+</string>
    <string name="equal">=</string>

    <string name="hint1">Value 1</string>
    <string name="hint2">Value 2</string>
</resources>

最后,我们的HelloSumAidlActivity.java如下:

package com.android.hellosumaidl;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class HelloSumAidlActivity extends AppCompatActivity {
  IAdditionService service;
  AdditionServiceConnection connection;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_hello_sum_aidl);

    initService();

    Button buttonCalc = (Button)findViewById(R.id.buttonCalc);
    buttonCalc.setOnClickListener(new View.OnClickListener() {
      EditText value1 = (EditText)findViewById(R.id.value1);
      EditText value2= (EditText)findViewById(R.id.value2);
      TextView result = (TextView)findViewById(R.id.result);
      @Override
      public void onClick(View v) {
        int v1, v2, res = -1;
        v1 = Integer.parseInt(value1.getText().toString());
        v2 = Integer.parseInt(value2.getText().toString());

        try {
          res = service.add(v1, v2);
        } catch (RemoteException e) {
          e.printStackTrace();
        }

        result.setText(Integer.valueOf(res).toString());
      }
    });
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    releaseService();
  }

  /*
     * This inner class is used to connect to the service
     * 这个内部类用于连接到服务(service)
     */
  class AdditionServiceConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder boundService) {
      service = IAdditionService.Stub.asInterface(boundService);
      Toast.makeText(HelloSumAidlActivity.this, "Service connected", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
      service = null;
      Toast.makeText(HelloSumAidlActivity.this, "Service disconnected", Toast.LENGTH_LONG).show();
    }
  }

  /*
   * This method connects the Activity to the service
   * 这个方法使Activity(客户端)连接到服务(service)
   */
  private void initService() {
    connection = new AdditionServiceConnection();
    Intent i = new Intent();
    i.setClassName("com.android.hellosumaidl", com.android.hellosumaidl.AdditionService.class.getName());
    bindService(i, connection, Context.BIND_AUTO_CREATE);
  }

  /*
   * This method disconnects the Activity from the service
   * 这个方法使Activity(客户端)从服务(service)断开
   */
  private void releaseService() {
    unbindService(connection);
    connection = null;
  }
}

将此项目运行起来,得到的两个截图如下:

Fig 1 : 填写数字前
Fig 2 : 按下计算按钮(等号)后

4. 后记和源码


  1. 光是一个AIDL,就涉及到很多Android知识点。所以说:Android是一个“庞然大物”,要学习的东西很多。“少年,路漫漫其修远兮”,要成为Android大牛必须付出努力!

  2. 可以看到AIDL的原理还是著名的客户端和服务端原理。其底层实现用到了Android的Binder。关于Binder的实现原理,可以去看《Android开发艺术探索》一书。

  3. 网上一般的AIDL实例是将服务端(Server)和客户端(Client)分开放到两个Android项目中,我们的这个项目,将服务端和客户端放在同一个项目中,原理是类似的。

  4. 以上项目的源码我放到自己的Github上了,欢迎查看、fork、下载。https://github.com/frogoscar/HelloSumAIDL

  5. 欢迎留言补充、指正,谢谢。


我是谢恩铭,在巴黎奋斗的软件工程师。
热爱生活,喜欢游泳,略懂烹饪。
人生格言:「向着标杆直跑」

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容