Android跨进程通信IPC之19——AIDL

移步系列Android跨进程通信IPC系列

  • 1 AIDL简介
  • 2 为什么要设置AIDL
  • 3 AIDL的注意事项
  • 4 AIDL的使用
  • 5 源码跟踪
  • 6 AIDL的设计给我们的思考
  • 7 总结

1 AIDL简介

  • AIDL是一个缩写,全程是Android Interface Definition Language,也是android接口定义语言。
  • 准确的来说,它是用于定义客户端/服务器通信接口的一种描述语言。它其实一种IDL语言,可以拿来生成用于IPC的代码。从某种意义上说它其实是一个模板

2 为什么要设置AIDL

2.1 IPC的角度

  • 设计这门语言的目的是为了实现进程间通信,尤其是在涉及多进程并发情况的下的进程间通信IPC。
  • 通过AIDL,可以让本地调用远程服务器的接口就像调用本地接口那么简单,让用户无需关注内部细节,只需要实现自己的业务逻辑接口,内部复杂的参数序列化发送、接收、客户端调用服务端的逻辑,你都不需要去关心了。

2.2 方便角度

在Android process 之间不能用通常的方式去访问彼此的内存数据。他们把需要传递的数据解析成基础对象,使得系统能够识别并处理这些对象。因为这个处理过程很难写,所以Android使用AIDL来解决这个问题

3 使用场景

  • 多个客户端,多个线程并发的情况下要使用AIDL
  • 如果你的IPC不需要适用多个客户端,就用Binder。
  • 如果你想要IPC,但是不需要多线程,那就选择Messager

4 AIDL支持以下数据类型

  • Java基本类型,即int、long、char等;
  • String;
  • CharSequence;
  • List
    • List中的所有元素都必须是AIDL支持的数据类型、其他AIDL接口或你之前声明的Parcelable实现类。
  • Map
    • Map中的所有元素都必须是AIDL支持的数据类型、其他AIDL接口或你之前声明的Parcelable实现类。
  • 其他类型,必须要有import语句,即使它跟.aidl是同一个包下。
  • 关于复杂对象
  • 传递的复杂对象必须要实现Parcelable接口,这是因为Parcelable允许Android系统将复杂对象分解成基本类型以便在进程间传输.
  • 若客户端组件和服务分开在不同APP,必须要把该Parcelable实现类.java文件拷贝到客户端所在的APP,包路径要一致。
  • 另外,需要为这个Parcelable实现类定义一个相应的.aidl文件。与AIDL服务接口.aidl同理,客户端所在APP的/src/<SourceSet>/aidl目录下也要有这份副本。
TIM截图20180821173719.png

5 AIDL中的方法和变量

  • 方法可有零、一或多个参数,可有返回值或void。
  • 所有非基本类型的参数都需要标签来表明这个数据的去向
    • in,表示此变量由客户端设置;
    • out,表示此变量由服务端设置;
    • inout,表示此变量可由客户端和服务端设置;
    • 基本类型只能是in。
  • AIDL中的定向tag表示在跨进程通信中数据 流向
  • in表示数据只能由客户端流向服务端,out表示数据只能由服务端流行客户端,而inout则表示数据可在服务端与客户端之间双向流通。
  • 其中,数据流向是针对客户端中的那个传入方法的对象而言。
  • in为定向tag的话,表现为服务端将会接受到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端传参修改而发生变动;
  • out的话表现为服务端将会接收到那个对象的参数为空的对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;
  • inout为定向tag的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务对该对象的任何变动。

6 AIDL的使用(复杂对象为例)

6.1 服务端

我们以在Android Studio为例进行讲解

6.1.1 创建复杂对象

public class MessageData implements Parcelable {

    public long id;  //消息的id
    public String content; //消息的内容
    public long time;  //时间

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", content='" + content + '\'' +
                ", time=" + time +
                '}';
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(id);
        dest.writeString(content);
        dest.writeLong(time);
    }

    public MessageData(Parcel source) {
        id = source.readLong();
        content = source.readString();
        time = source.readLong();
    }

    public MessageData() {

    }

    public void readFromParcel(Parcel in) {
        id = in.readLong();
        content = in.readString();
        time = in.readLong();
    }


    public static final Creator<MessageData> CREATOR = new Creator<MessageData>() {
        @Override
        public MessageData createFromParcel(Parcel source) {
            return new MessageData(source);
        }

        @Override
        public MessageData[] newArray(int size) {
            return new MessageData[size];
        }
    };
}

6.1.2 创建AIDL文件夹

TIM截图20180821160304.png

6.1.3 创建AIDL文件(复杂对象及服务)

复杂对象
[MessageData.aidl]

// MessageData.aidl
package com.kai.ling.myapplication;

// Declare any non-default types here with import statements
parcelable MessageData;

服务

// IMyAidlInterface.aidl
package com.kai.ling.myapplication;
// Declare any non-default types here with import statements
import com.kai.ling.myapplication.MessageData;

interface IMyAidlInterface {
    /**
     * 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);

    void connect();

    void sendMessage(inout MessageData message);
}

编译后会生成对应的aidl文件


TIM截图20180821174715.png

6.1.4 书写服务端server继承IMyAidlInterface.Stub

public class MyServer extends IMyAidlInterface.Stub {

    private static final String TAG = "GEBILAOLITOU";


    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

    }

    @Override
    public void connect() throws RemoteException {
        Log.i(TAG, "connect");
    }

    @Override
    public void sendMessage(MessageData message) throws RemoteException {
        Log.i(TAG, "MyServer ** sendInMessage **" + message.toString());
    }

}

6.1.5 service中返回server 并设置多进程

public class PushService extends Service {

    private MyServer myServer=new MyServer();


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return myServer;
    }
}
        <service
            android:name=".server.PushService"
            android:enabled="true"
            android:exported="true"
            android:process=":push"/>

6.2 客户端

管理类

public class PushManager {

    private static final String TAG = "GEBILAOLITOU";
    
    private PushManager() {
    }

    private IMyAidlInterface iMyAidlInterface;
    
    private static PushManager instance = new PushManager();
    
    //单例
    public static PushManager getInstance() {
        return instance;
    }


    //绑定服务
    public void init(Context context) {
        //定义intent
        Intent intent = new Intent(context, PushService.class);
        //绑定服务
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //成功连接
            Log.d(TAG, "pushManager ***************成功连接***************");
            //通过asInterface获取
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //断开连接调用
            Log.d(TAG, "pushManager ***************连接已经断开***************");
        }
    };

    //远程调用connect()方法
    public void connect() {
        try {
            Log.d(TAG, "pushManager ***************start Remote***************");
            iMyAidlInterface.connect();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    //远程调用sendMessage()方法
    public void sendMessage(String content) {
        MessageData messageData = new MessageData();
        messageData.content = content;
        try {
            iMyAidlInterface.sendMessage(messageData);
        } catch (RemoteException e) {
            e.printStackTrace();
            Log.d(TAG, "pushManager ***************RemoteException***************");
        }
    }
}
public class MainActivity extends AppCompatActivity {

    private boolean isConnected;
    private EditText content;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化
        PushManager.getInstance().init(this);

        content = findViewById(R.id.content);

        findViewById(R.id.connect).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PushManager.getInstance().connect();
                isConnected = true;
            }
        });

        findViewById(R.id.send).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!isConnected) {
                    Toast.makeText(MainActivity.this, "请连接", Toast.LENGTH_LONG).show();
                }
                if (TextUtils.isEmpty(content.getText().toString().trim())) {
                    Toast.makeText(MainActivity.this, "请输入", Toast.LENGTH_LONG).show();
                }
                PushManager.getInstance().sendMessage(content.getText().toString().trim());
            }
        });
    }
}

7 源码跟踪

  • 在写完AIDL文件后,编译器会帮我们自动生成一个同名的.java文件。
  • 在我们实际编写客户端和服务端代码的过程中,真正协助我们工作的其实就是这个文件,而.aidl文件从头到尾都没有出现过。
  • 这个.aidl文件就是为了生成这个对应的.java文件。事实上,就算我们不写AIDL文件,直接按照它生成的.java文件这样写一个.java文件出来。在服务端和客户端也可以照常使用这个.java类进行跨进程通信。
  • AIDL语言只是在简化我们写这个.java文件而已,而要研究AIDL是符合帮助我们进行跨境进程通信的,其实就是研究这个生成的.java文件是如何工作的

7.1 .java文件位置

TIM截图20180822155143.png
  • 它的完整路径是:app->build->generated->source->aidl->debug->com->gebilaolitou->android->aidl->IMyAidlInterface.java(其中 com.gebilaolitou.android.aidl
    是包名,相对应的AIDL文件为 IMyAidlInterface.aidl )。

7.2 IMyAidlInterface .java类分析

结构图

TIM截图20180822155826.png

  • 编译的后IMyAidlInterface.java文件是一个接口,继承自android.os.IInterface
  • IMyAidlInterface内部代码主要分成两部分,一个是抽象类Stub 和 原来aidl声明的basicTypes()、connect()和sendInMessage()方法

7.3 Stub类分析

TIM截图20180822160212.png
  • Stub类基本结构
  • 静态方法 asInterface(android.os.IBinder obj)
  • 静态内部类 Proxy
  • 方法 onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
  • 方法 asBinder()
  • private的String类型常量DESCRIPTOR
  • private的int类型常量TRANSACTION_connect
  • private的int类型常量TRANSACTION_sendInMessage

7.3.1 静态方法 asInterface(android.os.IBinder obj)

public static com.kai.ling.myapplication.IMyAidlInterface asInterface(android.os.IBinder obj) {
    //非空判断
    if ((obj == null)) {
        return null;
    }
    // DESCRIPTOR是常量为"com.gebilaolitou.android.aidl.IMyAidlInterface"
    // queryLocalInterface是Binder的方法,搜索本地是否有可用的对象
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    //如果有,则强制类型转换并返回
    if (((iin != null) && (iin instanceof com.kai.ling.myapplication.IMyAidlInterface))) {
        return ((com.kai.ling.myapplication.IMyAidlInterface) iin);
    }
    //如果没有,则构造一个IMyAidlInterface.Stub.Proxy对象
    return new com.kai.ling.myapplication.IMyAidlInterface.Stub.Proxy(obj);
}
  • 主要的作用就是根据传入的Binder对象转换成客户端需要的IMyAidlInterface接口。
    • 如果客户端和服务端处于同一进程,那么queryLocalInterface()方法返回就是服务端Stub对象本身;
    • 如果是跨进程,则返回一个封装过的Stub.Proxy,也是一个代理类,在这个代理中实现跨进程通信。那么让我们来看下Stub.Proxy类

7.3.2 onTransact()方法解析

onTransact()方法是根据code参数来处理,这里面会调用真正的业务实现类

  • 在onTransact()方法中,根据传入的code值回去执行服务端相应的方法。其中常量TRANSACTION_connect和常量TRANSACTION_sendInMessage就是code值(在AIDL文件中声明了多少个方法就有多少个对应的code)。
  • 其中data就是服务端方法需要的的参数,执行完,最后把方法的返回结果放入reply中传递给客户端。如果该方法返回false,那么客户端请求失败。

7.3.3 静态类Stub.Proxy

private static class Proxy implements com.kai.ling.myapplication.IMyAidlInterface {
    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;
    }

    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            _data.writeInt(anInt);
            _data.writeLong(aLong);
            _data.writeInt(((aBoolean) ? (1) : (0)));
            _data.writeFloat(aFloat);
            _data.writeDouble(aDouble);
            _data.writeString(aString);
            mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
            _reply.readException();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }

    @Override
    public void connect() throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            mRemote.transact(Stub.TRANSACTION_connect, _data, _reply, 0);
            _reply.readException();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }
    //发送消息  客户端——> 服务器
    @Override
    public void sendMessage(com.kai.ling.myapplication.MessageData message) 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 ((message != null)) {
                _data.writeInt(1);
                message.writeToParcel(_data, 0);
            } else {
                _data.writeInt(0);
            }
            mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply, 0);
            _reply.readException();
            if ((0 != _reply.readInt())) {
                message.readFromParcel(_reply);
            }
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }
}

  • 1、Proxy 实现了 com.gebilaolitou.android.aidl.IMyAidlInterfac接口,所以他内部有IMyAidlInterface接口的两个抽象方法
  • 2、Proxy的asBinder()方法返回的mRemote,而这个mRemote是什么时候被赋值的?是在构造函数里面被赋值的。

7.3.3.1 静态类Stub.Proxy的connect()方法

  • 1、 通过阅读静态类Stub.Proxy的connect()方法,我们容易分析出来里面的两个android.os.Parcel_data_reply是用来进行跨进程传输的"载体"。而且通过字面的意思,很容易猜到,*_data用来存储 客户端流向服务端 的数据,_reply用来存储 服务端流向客户端 的数据
  • 2、通过mRemote. transact()方法,将_data和_reply传过去
  • 3、通过_reply.readException()来读取服务端执行方法的结果。
  • 4、最后通过finally回收l_data和_reply

7.3.3.2 connect() 相关参数

  • 关于 transact()方法:这是客户端和和服务端通信的核心方法,也是IMyAidlInterface.Stub继承android.os.Binder而重写的一个方法。
  • 调起这个方法之后,客户端将会挂起当前线程,等候服务端执行完相关任务后,通知并接收返回的_reply数据流。
  • 1 方法ID:transact()方法第一个参数是一个方法ID,这个是客户端和服务端约定好的给方法的编码,彼此一一对应。在AIDL文件转话为.java时候,系统会自动给AIDL里面的每一个方法自动分配一个方法ID。而这个ID就是咱们说的常量TRANSACTION_connect和TRANSACTION_sendInMessage这些常量生成了递增的ID,是根据你在aidl文件的方法顺序而来,然后在IMyAidlInterface.Stub中的onTransact()方法里面switch根据第一个参数code即我们说的ID而来。
  • 2最后的一个参数:transact()方法最后一个参数是一个int值,代表是单向的还是双向的。具体大家请参考我们前面的文章Android跨进程通信IPC之8——Binder的三大接口中关于IBinder部分。我这里直接说结论:0表示双向流通,即_reply可以正常的携带数据回来。如果为1的话,那么数据只能单向流程,从服务端回来的数据_reply不携带任何数据。注意:AIDL生成的.java文件,这个参数均为0

7.3.4 asBinder()方法

该方法就是返回当前的Binder方法

8 IMyAidlInterface .java流程分析

以上面的例子为例,那么先从客户端开始

8.1 客户端

8.1.1 获取IMyAidlInterface对象

public void init(Context context){
        //定义intent
        Intent intent = new Intent(context,PushService.class);
        //绑定服务
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //成功连接
            Log.i(TAG,"PushManager ***************成功连接***************");
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //断开连接调用
            Log.i(TAG,"PushManager ***************连接已经断开***************");
        }
    };
  • 客户端中通过Intent去绑定一个服务端的Service。
  • onServiceConnected(ComponentName name, IBinder service)方法中通过返回service可以得到AIDL接口的实例
  • 这是调用了asInterface(android.os.IBinder) 方法完成的。在asInterface(android.os.IBinder)我们知道是调用的new com.gebilaolitou.android.aidl.IMyAidlInterface.Stub.Proxy(obj)构造的一个Proxy对象。
  • 所以可以这么说在PushManager中的变量IMyAidlInterface其实是一个IMyAidlInterface.Stub.Proxy对象。

8.1.2 调用connect()方法

PushManager类中的iMyAidlInterface其实IMyAidlInterface.Stub.Proxy对象,所以调用connect()方法其实是IMyAidlInterface.Stub.Proxy的connect()方法。

  • 1、这里面主要是生成了_data和_reply数据流,并向_data中存入客户端的数据。
  • 2、通过 transact()方法将他们传递给服务端,并请求服务指定的方法
  • 3、接收_reply数据,并且从中读取服务端传回的数据。

通过上面客户端的所有行为,我们会发现,其实通过ServiceConnection类中onServiceConnected(ComponentName name, IBinder service)中第二个参数service很重要,因为我们最后是用它的transact() 方法,将客户端的数据和请求发送给服务端去。从这个角度来看,这个service就像是服务端在客户端的代理一样,而IMyAidlInterface.Stub.Proxy对象更像一个二级代理,我们在外部通过调用这个二级代理来间接调用service这个一级代理

8.2 服务端流程

在前面几篇文章中我们知道Binder传输中,客户端调用transact()对应的是服务端的onTransact()函数,我们在IMyAidlInterface.java中看到
查看onTransact()

       @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_basicTypes: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    long _arg1;
                    _arg1 = data.readLong();
                    boolean _arg2;
                    _arg2 = (0 != data.readInt());
                    float _arg3;
                    _arg3 = data.readFloat();
                    double _arg4;
                    _arg4 = data.readDouble();
                    java.lang.String _arg5;
                    _arg5 = data.readString();
                    this.basicTypes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_connect: {
                    data.enforceInterface(DESCRIPTOR);
                    this.connect();
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_sendMessage: {
                    data.enforceInterface(DESCRIPTOR);
                    com.kai.ling.myapplication.MessageData _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.kai.ling.myapplication.MessageData.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.sendMessage(_arg0);
                    reply.writeNoException();
                    if ((_arg0 != null)) {
                        reply.writeInt(1);
                        _arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
                    } else {
                        reply.writeInt(0);
                    }
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

在收到客户端的 transact()方法后,直接调用了switch选择,根据ID执行不同操作,因为我们知道是调用的connect()方法,所以对应的code是TRANSACTION_connect,所以我们下case TRANSACTION_connect:的内容,如下:

case TRANSACTION_connect: {
    data.enforceInterface(DESCRIPTOR);
    this.connect();
    reply.writeNoException();
    return true;
}

这里面十分简单了,就是直接调用服务端的connect()方法。

9 整体流程如下:

5713484-298c59e740ccf7e3.png

5713484-2c51ce26ad1e82a9.png

参考

Android跨进程通信IPC之11——AIDL

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

推荐阅读更多精彩内容