译:Android Interface Definition Language (AIDL)

Android Interface Definition Language (AIDL)

注:本文翻译自https://developer.android.com/guide/components/aidl.html

AIDL(Android Interface Definition Language)跟你可能使用的其他接口定义语言类似。它允许你定义一个编程接口,这个接口是的客户端与服务端都约定一致用来跨进程通信。在Android系统中,一个进程通常不能访问另一个进程的内存。所以他们需要将他们的对象拆解成操作系统所能识别的原语数据(primitives),然后传入到另一个进程之后再替你组装成对象。而这个组装操作的代码写起来十分冗长(tedious ),因此Android通过采用AIDL来替你处理这一冗长的操作。

注意:只有当你允许让来自不同应用的客户端为了跨进程通信(IPC)访问你的服务并且在你的服务中有多个线程需要处理,才有必要使用AIDL。如果没有必要处理并发跨进程(IPC)访问不同的应用,你应该通过实现Binder接来创建接口,或者如果你想执行跨进程通信,但又没有必要处理多线程, 使用Messager 来实现接口。不管怎样,在实现一个AIDL接口之前请确保你已经理解了 绑定服务

在你开始定义你的AIDL接口之前,要意识到对一个AIDL接口的调用时一个直接的函数调用。你不能假定这个调用是在哪一个线程。产生的结果取决于调用该方法的线程是位于本地进程还是远程进程,特别是:

  • 如果调用是来自本地进程并且跟调用执行与同一个线程,比如是你的UI主线程,这个线程执行继续执行AIDL接口。如果是另一个线程,那就是在service中执行的线程。因此,仅当本地线程访问服务的,你才能完全控制AIDL接口在那个线程执行(但是如果是这种情形,你完全不行该使用AIDL,而是通过实现Binder创建接口)
  • 如果对AIDL方法的调用来自远程的进程,并且这个远程的进程采用线程池(这个线程池是在你自己的进程中维护)来分发对该AIDL方法的调用,你必须为即将到来的未知线程做准备,这些线程使得同时有多个调用在进行的。换句话说,你的AIDL的方法的实现必须完全线程安全。
  • 单向(oneway)关键字修改远程调用。一旦使用,远程调用不会阻塞,它只是简单地发送传输数据并且立即返回。接口的实现最终收到一个来于Binder线程池被当做一般的远程调用的常规的调用。如果单向(oneway)采用本地调用,则不会有什么影响并且调用时同步的。

定义AIDL接口

你必须在一个以.aidl为后缀的文件中采用Java的语法来定义AIDL接口,然后将它保存在提供该服务和其他需要绑定该服务的应用的源代码目录src/下面。

当你构造没有应用包含有.aidl格式文件,Android的SDK工具就会根据.aidl文件生成一个 IBinder接口并将它保存在项目的gen/目录下面。服务必须根据需要实现这个 IBinder。然后客户端绑定该服务调用这个 IBinder中的方法来执行跨进程通信。

要创建一个通过AIDL绑定的服务,采用下面的步骤。

  1. 创建.aidl文件
    这个文件定义了带有方法签名的接口
  2. 实现该接口
    Android SDK工具会根据你的.aidl文件以Java编程语言生成一个接口。该接口有一个叫做Stub的内部抽象类, 它继承自Binder并且实现了你在AIDL接口中定义的方法。你必须继承Stub类然后实现它的方法。
  3. 暴露接口给客户端
    实现一个 Service并且覆盖 onBind() 方法,返回你针对Stub类的实现.

注意:在你第一次发布服务之后,每当你的AIDL接口所有改变,你需要保持后向兼容,以避免破坏其他应用使用你的服务。 因为你的.aidl文件必须拷贝到其他应用中以便让其他的应用访问你的服务接口。你必须维护队原始接口的支持。

1、创建.aidl文件

AIDL使用简单的语法让你申明一个带有一个或者多个可以有参数和返回值的方法。参数和返回值可以是任意类型,甚至是AIDL生成的接口。

你必须使用Java编程语言创建.aidl文件。每一个.aidl文件只能定义一个接口,只需要申明该接口的方法(不需要实现)

AIDL默认支持下面几种数据类型:

  • Java编程语言中的基础数据类型(int,long,char,boolean等)
  • String
  • CharSequence
  • List
    List 中的所有元素必须是上面支持的类型或者是其他由AIDL生成的接口,或者你申明的实现了Parcelable接口的类型。List可能被选用为泛型类,比如 List<String>.实际在接受服务一侧生成的类为永远是 ArrayList。尽管生成的方法使用的是 List接口。
  • Map
    Map中的虽有元素必须是上面类型或者是其他由AIDL生成的接口,或者你申明的实现了Parcelable接口的类型。泛型例如 Map<String,Integer>不支持。实际在接受服务一侧生成的类为永远是HashMap。尽管生成的方法使用的是 Map接口。

你必须要为每一个上面未列出类型添加import申明,尽管他们是作为接口定义在同一个包里面。

当你定义的服务接口,要意识到:

  • 方法可以带有0个或者多个参数,带有返回值或者没有返回值
  • 所有的非基础数据类型参数需要一个额外的用于标明参数去向的标记。可以是in、out或者inout(参见下面的例子)。基础数据类型默认是in.而不能选择其他方式。

注意:你需要限制确实需要的方向,因为组装参数的开销很大。

  • 所有包含在.aidl文件中的代码注释也就会包生成的 IBinder 接口中,除了导包语句之前的声明的注释。
  • 在AIDL中仅支持申明方法,不能申明静态域。

下面是一个.aidl文件示例:

// IRemoteService.aidl
package com.example.android;

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

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

只需要将.aidl文件保存到你项目的src/目录下面当构建项目的时候SDK工具会生成在项目的gen/目录下IBinder接口文件,生成的文件名与.aidl文件名相匹配,但是是以.java扩展名结尾(例如IRemoteService.aidl会生成IRemoteService.java

如果你使用的是Android Studio,增量构建几乎会立即生成Binder类。如果你没有使用Android Studio,Gradle 工具会在你下次构建应用时生成的binder类。一旦你写完了.aidl文件,你应该用gradle assembleDebug或者gradle assembleRelease构建项目,这样才能让你的代码链接到生成的类上。

2、实现该接口

当你构建你的应用的时候,Android SDK工具生成一个根据.aidl文件生成.java接口,生成的接口包含一个叫做的Stub子类,这个类是父接口(比如YourInterface.Stub)的抽象实现,并且申明了所有来自.aidl文件的方法。

注意Stub也定义了一些帮助方法,比较常用的是asInterface(),这个方法传入一个 IBinder (这个参数通常传入到客户端的的onServiceConnected()回调方法中)并且返回一个Stub的实例。参见 调用跨进程方法获取如何转换的更多信息。

为了实现由.aidl生成的接口,继承生成的Binder接口,例如YourInterface.Stub,然后实现继承自.aidl文件中的方法。

下面是一个调用名为IRemoteService 接口的实现。(在上面的例子 IRemoteService.aidl中定义),使用匿名实例:

private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
};

现在mBinderStub 类的实例,它定义了该服务的跨进程通信接口,在下一步,这个实例会暴露给客户端这样客户端便能与服务端交互。

下面是在实现AIDL接口是应当注意的规则:

  1. 传入的调用不一定保证会在主线程中执行,因此你需要考虑多线程从开始到构建服务保证线程安全

  2. 默认情况下,远程过程调用(Remote Procedure Call,缩写为 RPC)是同步的,如果知道服务会花费超过数秒来完成一个请求,你不应当在activity 的主线中调用该方法,因为这可能会导致应用挂起(Android应用可能会显示应用无法响应的对话框),通常你应该在客户端的一个单独的线程中调用该方法。

  3. 不会有异常回传给调用者

3、暴露接口给客户端

一旦你实现了服务接口,你需要将它暴露给客户端这样客户端就能绑定服务。为了暴露接口给服务,继承服务类Service 并实现 onBind()以返回一个实现了生成的Stub(前面所讨论).

下面是一个暴露IRemoteService实例接口给服务端的示例:

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}

现在,当客户端例如Activity调用[bindService()](https://developer.android.com/reference/android/content/Context.html#bindService(android.content.Intent, android.content.ServiceConnection, int))链接到该服务,客户端的 [onServiceConnected()](https://developer.android.com/reference/android/content/ServiceConnection.html#onServiceConnected(android.content.ComponentName, android.os.IBinder)) 回调方法接受一个service的 onBind()方法返回的mBinder实例。

客户端必须访问接口类,如果客户端和服务端处在不同的应用中,客户端必须具有一个应用必须具有一份.aidl的拷贝并将其存放于src/目录下面。(Android SDK)会生成android.os.Binder接口。提供客户端访问AIDL的方法。

当客户端接收到 [onServiceConnected()](https://developer.android.com/reference/android/content/ServiceConnection.html#onServiceConnected(android.content.ComponentName, android.os.IBinder)) 回调方法中 IBinder .它必须调用YourServiceInterface.Stub.asInterface(service)将其转换成YourServiceInterface 类型。示例如下:

IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        mIRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        mIRemoteService = null;
    }
};

了解更多的示例代码参考 ApiDemos中的 RemoteService.java
类.

通过跨进程通信传递对象

如果你有一个想通过跨进程通信接口从一个进程向另一个进程发送数据的类,你可以这样做。但是你必须确定你的类中的代码对跨进程通信通道另一端的是可用的,你必须支持 Parcelable 接口。支持 Parcelable 接口非常重要,因为它允许Android将对象拆解成可以跨进程组装的原语(primitives ).

要想创建一个支持 Parcelable协议的类,你可以采用下面的方式:

  1. 创建一个类并实现 Parcelable接口.
  2. 实现 [writeToParcel](https://developer.android.com/reference/android/os/Parcelable.html#writeToParcel(android.os.Parcel, int))方法,这个方法将当前对象的状态写入到 Parcel中。
  3. 添加一个名为CREATOR的静态域到你的类里面,这个域实现了 Parcelable.Creator接口。
  4. 最后,创建一个.aidl文件,申明你的parcelable类(如下面的 Rect.aidl文件所示)。
    如果你正在使用一个自定义构建进程,不需要添加.aidl文件到你的构建.跟C语言的头文件很相似,.aidl文件不会编译。

AIDL使用这些所生成的代码方法和域来组装和拆解你的对象。

例如。下面是一个Rect.aidl文件来创建一个可组装和拆解的Rect类.


package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;

下面是 Rect 类如何实现 Parcelable协议的:


package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;

下面是Rect类如何实现 Parcelable 协议的
import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new
Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

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

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }
}

Rect类的组装非常简单,查看 Parcel 上的其他方法可以知道你能向Parcel中写入其他类型的值。

警告:不要忘记接受来自其他进程数据的安全性。在该例子中,Rect读取来自 Parcel的四个数字。但是这取决于你确保这些值是在一个可接受的范围类,无论客户端尝试做什么。参考 安全和权限了解更多有关如何保证你的应用免于恶意软件破坏的信息。

调用一个跨进程通信方法

下面是一个类想调用由AIDL定义的远程接口所必须经历的步骤:

  1. .aidl文件放入项目的src/目录下面。
  2. 申明IBinder接口实例(该示例基于AIDL生成)
  3. 实现 ServiceConnection.
  4. 调用 [Context.bindService()](https://developer.android.com/reference/android/content/Context.html#bindService(android.content.Intent, android.content.ServiceConnection, int)),传入你 ServiceConnection 的实现。
  5. 在 [onServiceConnected()](https://developer.android.com/reference/android/content/ServiceConnection.html#onServiceConnected(android.content.ComponentName, android.os.IBinder))的实现中,你会收到一个IBinder的实例(也叫service),调用YourInterfaceName.Stub.asInterface((IBinder)service)将 [onServiceConnected()](https://developer.android.com/reference/android/content/ServiceConnection.html#onServiceConnected(android.content.ComponentName, android.os.IBinder))回调方法中的IBinder参数转换成YourInterface 类型。
  6. 调用你在接口中定义的方法。你始终要要捕获 DeadObjectException 异常,这个异常会在链接断开时抛出。这个异常只会有远程方法抛出.
  7. 如果想断开链接,用你接口的实例调用 Context.unbindService()

关于调用跨进程服务的一点说明:

  • 对象跨进程采取引用计数方式
  • 可以发送匿名对象作为方法的参数

想了解更多有关绑定服务的内容,参考绑定服务相关文档。

下面是摘自ApiDemos项目中远程服务范例部分代码展示如何调用AIDL创建的服务。

public static class Binding extends Activity {
    /** The primary interface we will be calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary mSecondaryService = null;

    Button mKillButton;
    TextView mCallbackText;

    private boolean mIsBound;

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button clicks.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(mUnbindListener);
        mKillButton = (Button)findViewById(R.id.kill);
        mKillButton.setOnClickListener(mKillListener);
        mKillButton.setEnabled(false);

        mCallbackText = (TextView)findViewById(R.id.callback);
        mCallbackText.setText("Not attached.");
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            mKillButton.setEnabled(true);
            mCallbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            mKillButton.setEnabled(false);
            mCallbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection mSecondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            mSecondaryService = ISecondary.Stub.asInterface(service);
            mKillButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            mSecondaryService = null;
            mKillButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names.  This allows other applications to be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, mSecondaryConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
            mCallbackText.setText("Binding.");
        }
    };

    private OnClickListener mUnbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (mIsBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // has crashed.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(mSecondaryConnection);
                mKillButton.setEnabled(false);
                mIsBound = false;
                mCallbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener mKillListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently our service has a call that will return
            // to us that information.
            if (mSecondaryService != null) {
                try {
                    int pid = mSecondaryService.getPid();
                    // Note that, though this API allows us to request to
                    // kill any process based on its PID, the kernel will
                    // still impose standard restrictions on which PIDs you
                    // are actually able to kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here; packages
                    // sharing a common UID will also be able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    mCallbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // Just for purposes of the sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    mCallbackText.setText("Received from service: " + msg.arg1);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }

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

推荐阅读更多精彩内容