Android IPC机制

1. IPC基础概念——多进程

IPC是Inter Process Communication的缩写,意为进程间通信或者跨进程通信,是指两个进程之间进行数据交换。

1.1 Android中的多进程

在了解Android多进程之前,得先了解什么是进程?

按照操作系统的描述,线程是CPU调度的最小单元,而进程一般指一个执行单元,在移动设备上指一个程序或应用;一个进程可以包含多个线程。

1.2 Android开启多进程模式

在Android中要开启多进程模式,只要给四大组件指定 android:process 属性。
是不是很简单的就开启了多进程,其实远没有那么简单,这只是挖坑的开始,先看一下开启多进程的实例操作:

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        // 注意进程以:开头
        <activity android:name=".SecondActivity" 
            android:process=":remote"/>

        <activity android:name=".ThirdActivity"
            android:process="com.czj.ipcdemo.remote2"/>

在终端输入adb shell ps | com.czj.ipcdemo查看一个包名中当前应用所存在的进程信息。
会发现一共存在3个进程。
此处划重点:

  • 以:开头的属于当前应用私有进程,其它应用的组件不可以和它跑在同一个进程;
  • 进程名不以:开头的,属于全局进程,其它应用通过shareUID方式可以和它跑在同一个进程中。
App进程.png

1.3 多进程造成的问题

  • 静态成员和单例模式失效
  • 线程同步机制失效
  • SharedPreferences的可靠性降低
  • Application会创建多次

Android虚拟机分配规则
Android会为每一个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址看空间,这就导致在不同的虚拟机(进程)中访问同一个对象会产生多份副本。所以,在多进程环境中,想要依靠内存来共享数据,是不会达到你预期的结果的。

现在来分析一下上述列出的问题:
第1个问题和第2个问题都是因为不同进程的虚拟机是独立的,不同虚拟机内存地址当然就是不一样的,既然不是同一块内存地址,锁对象或者锁类也达不到线程同步。
第3个问题,SharedPreferences不支持两个进程同时做写操作。
第4个问题,一个组件跑在新的进程中,系统要为其分配独立的虚拟机,也就相当于重新启动一个应用。

在进行跨进程通信之前,还得了解Android的序列化(Serializable 和 Parcelable)以及Binder的概念。

2. IPC基础概念——序列化

关于Android序列化,附个传送门:Android序列化基础知识

3. IPC基础概念——Binder

从各个角度看Binder:
IPC:Binder是Android中的跨进程通信方式。
Android Framework:BInder是ServiceManger连接各种Manager和响应MangerService的桥梁。
Android应用层:Binder是客户端和服务端进行通信的媒介。

Binder主要用在Service中,包括AIDL和Messenger(底层是AIDL),普通的Service中的Binder不涉及进程间通信。后面主要对AIDL的使用,进行分析,一个AIDL过程,即是一个RPC的过程。
后面第四部分会对AIDL的使用进行实例分析。

4. Android实现IPC的几种方式

  • Bundle
  • 文件共享
  • Messenger
  • AIDL
  • ContentProvider
  • Socket

4.1 Bundle

Android中的Activity、Service、Receiver都支持使用Intent来传递Bundle数据,Bundle实现了Parcelable接口,所以能够使用Bundle来实现数据的进程间传输。
需要注意的一点是,传输的对象必须能够被序列化(基本类型、Parcelable、Serializable)

4.2 文件共享

两个进程对同一个文件进行读和写,来实现进程间的通信。实现无非就是文件IO操作,实例此处略过。
但是,这里说的文件共享不包括SharedPreferences。
虽然SharedPreferences是XML文件,但是系统会对它的读写有缓存策略,就是说再内存中会有一份SharedPreferences缓存,但是多进程是分配在独立的虚拟机上的,所以SharedPreferences数据在多进程是不可靠的。

4.3 Messenger

Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。

服务端进程:

public class MessengerService extends Service {

    private static final String TAG = "MessengerService";

    private Messenger mMessenger = new Messenger(new MessengerHandler());

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constants.MSG_CLIENT:
                    // 收到客户端消息
                    Log.e(TAG, "收到客户端进程消息:" + msg.getData().getString("msg"));
                    // 回复客户端消息
                    Messenger client = msg.replyTo;
                    Message replyMessage = Message.obtain(null, Constants.MSG_SERVER);
                    Bundle bundle = new Bundle();
                    bundle.putString("reply", "来自Service的回复");
                    replyMessage.setData(bundle);
                    try {
                        client.send(replyMessage);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}

创建一个Service来处理客户端的连接请求,同时创建一个Handler内部类来处理消息,并用来实例化Messenger对象。最后在onBind返回Messenger对象的Binder。日志打印如下:

10-24 14:08:47.542 31636-31636/com.czj.ipcdemo:remote3 E/MessengerService: 收到客户端进程消息:client msg

客户端进程:

public class MessengerActivity extends AppCompatActivity {

    private static final String TAG = "MessengerActivity";

    private Messenger mService;

    private Messenger mMessenger = new Messenger(new MessengerHandler());

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
            Message msg = Message.obtain(null, Constants.MSG_CLIENT);
            Bundle bundle = new Bundle();
            bundle.putString("msg", "client msg");
            msg.setData(bundle);
            // 将客户端的 Messenger 传递给服务端
            msg.replyTo = mMessenger;
            try {
                mService.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
        Intent intent = new Intent(this, MessengerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

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

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constants.MSG_SERVER:
                    Log.e(TAG, "收到服务端回信:" + msg.getData().get("reply"));
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}

客户端进程,首先要绑定服务端的Service,然后用服务端给的IBinder去创建Messenger来与服务端进行通信。
如果需要服务端能够回复客户端,则客户端要定义个Messenger,然后赋值给Message.replyTo传递给服务端。
日志打印如下:

10-24 14:08:47.548 31568-31568/com.czj.ipcdemo E/MessengerActivity: 收到服务端回信:来自Service的回复

使用Messenger进行进程间的通信,使用方法简单,但它也有些局限性。

  1. Messenger是以串行的方式处理客户端消息,服务端只能一个个处理,不适合有大量并发请求的情况
  2. Messenger的主要作用是传递消息,无法跨进程调用服务端的方法(也是RPC过程)

4.4 AIDL

AIDL(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端实现间接调用服务端对象的方法。

4.4.1 AIDL文件定义

1. AIDL接口创建
AIDL接口就是暴露给客户端调用的方法,此AIDL文件编译后在build目录下会生成对应的Java代码,所以从本质上来说AIDL是系统提供了一套可快速实现Binder的工具。示例代码如下:

package com.czj.ipcdemo;
// 这里要注意,即使在同一个包下,也要把引用的类import进来
import com.czj.ipcdemo.Book;

interface IBookManager {

    List<Book> getBookList();

    void addBook(in Book book);

}

AIDL文件中的数据类型要求:

  • 基本数据类型:byte,int,long,float,double,boolean,char
  • String类型
  • CharSequence类型
  • ArrayList、HashMap且里面的每个元素都能被AIDL支持
  • 实现Parcelable接口的对象
  • 所有AIDL接口本身

需要注意的是,AIDL中除了基本类型外,其它类型的参数必须标明传递方向:

  • in:输入型参数
    表示数据只能由客户端流向服务端。
    服务端将会接收到这个对象的完整数据,但在服务端修改它不会对客户端输入的对象产生影响。
  • out: 输出型参数
    表示数据只能由服务端流向客户端。
    服务端将会接收到这个对象的的空对象,但在服务端对接收到的空对象有任何修改之后客户端将会同步变动。
  • inout:输入输出型参数
    表示数据可在服务端与客户端之间双向流通。
    服务端将会接收到客户端传来对象的完整信息,且客户端将会同步服务端对该对象的任何变动。

2. 自定义Parcelable类对应AIDL文件
如果AIDL文件中用到了自定义的Parcelable对象,则必须新建一个和它同名的AIDL文件:

package com.czj.ipcdemo;

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

parcelable Book;

3. 服务端Service实现

public class BookManagerService extends Service {

    private static final String TAG = "BMS";

    // CopyOnWriteArrayList支持并发,当多个客户端同时访问时
    // 前面提到AIDL只支持ArrayList
    // AIDL中使用的是List接口去访问,此处服务端返回CopyOnWriteArrayList实例,但是Binder会转成ArrayList给客户端,所以通过AIDL接口去获取的时候已经是ArrayList
    private List<Book> mBookList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book("忒修斯之船"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

4. 客户端实现

public class BookManagerActivity extends AppCompatActivity {

    private final static String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 将服务端Binder转成AIDL接口类型
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                // 获取服务端数据
                List<Book> list = bookManager.getBookList();
                Log.e(TAG, "list type : " + list.getClass().getCanonicalName());
                Log.e(TAG, "book list : " + list);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "onServiceDisconnected");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_book_manager);
        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

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

输出(注意此处的List输出类型):
10-24 09:51:26.122 13500-13500/com.czj.ipcdemo E/BookManagerActivity: list type : java.util.ArrayList
10-24 09:51:26.123 13500-13500/com.czj.ipcdemo E/BookManagerActivity: book list : [[bookName:忒修斯之船]]

AIDL变量和方法解释(存在于AIDL文件编译转换成的Java类中)

  • DESCRIPTOR
    Binder的唯一标识,用当前Binder的类名表示,如:com.czj.ipcdemo.IBookManager
  • asInterface(android.os.IBinder obj)
    客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。如果客户端和服务端同属一个进程,此方法返回的是服务端对象本身,否则返回系统封装后的Stub.proxy对象。
  • asBinder()
    返回当前Binder对象
  • onTransact()
    运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
  • transact()
    运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

Binder死亡的处理

如果服务端进程意外终止,则客户端到服务端的Binder连接也就终止了,这会导致客户端远程调用失败。

面对这种情况,有两种解决方案:

  • 给Binder设置死亡监听:DeathRecipient
  • 在onServiceDisconnected中重新绑定远程服务

下面给出为Binder设置死亡监听的示例:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
                if (bookManager == null) return;
                bookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
                bookManager = null;
                // TODO 此处重新绑定远程Service
        }
 }

然后在客户端绑定Service的回调中,设置Binder的死亡代理:

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            // 设置死亡代理            
            service.linkToDeath(mDeathRecipient, 0);
            ......
        }
        ......
    };

Binder的跨进程传输

不知道有没有发现一个问题,就是服务端和客户端是运行在两个进程上的,那么服务端的Binder是如何传递给客户端的ServiceConnection中,显然不是通过内存直接传递Binder对象。
Binder的传递:将Binder打包进Parcel来传输,发现如果是同进程的,则收到的是原始对象,而不是对象的拷贝。如果是跨进程的,则收到的BinderProxy。

Service注册到Service Manager时,将Binder实体跨进程传输给ServiceManager,由于是跨进程的,所以必须先经过Binder驱动,驱动会保存这个Binder实体,并生成一个Binder引用返给ServiceManager。Client端如果向Service Manager请求该Service,ServiceManager会将该Service的Binder引用返回,经过Binder驱动时,如果Client和Service在同一个进程,那么Binder驱动会直接返回该Service的Binder实体,如果不在同一个进程,则Binder驱动会返回Binder的引用。

推荐参考 关于Binder的跨进程传输

4.5 ContentProvider

ContentProvider用于在多个应用程序之间共享数据,所以适合多进程通信的场景,它的底层实现也是Binder。

  • 除了onCreate是在UI线程,query、update、insert、delete都运行在Binder线程
  • SQLiteDatabase内部有同步处理,但是多个SQLiteDatabase的情况,要做好同步处理

4.6 Socket

Socket:套接字,能够建立网络长连接。不仅支持进程间通信,还能进行跨设备通信。

在Service进程中创建一个Socket服务,客户端去连接该服务,也能够实现进程间通信。直接看实例:

  • 首先需要声明网络访问权限
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  • 服务端代码
public class TCPServerService extends Service {

    private final static String TAG = "TCPServerService";

    private boolean mIsDestory = false;

    private String[] mReturnMsgAry = { "你好傻猫威~", "买了否冷?", "鬼刀一开,看不见,走位走位", "学猫叫" };

    @Override
    public void onCreate() {
        Log.e(TAG, "开启服务");
        new Thread(new TcpServer()).start();
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        mIsDestory = true;
        super.onDestroy();
    }

    private class TcpServer implements Runnable {

        @Override
        public void run() {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(9527);
                Log.e(TAG, "服务已开启,正在监听客户端消息");
            } catch (IOException e) {
                Log.e(TAG, "run socket server failed, port:8688");
                e.printStackTrace();
                return;
            }
            while (!mIsDestory) {
                try {
                    final Socket client = serverSocket.accept();
                    new Thread() {
                        @Override
                        public void run() {
                            // 处理客户端请求
                            try {
                                responseClient(client);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void responseClient(Socket client) throws IOException {
        // 接收客户端消息
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        // 向客户端发送消息
        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
        out.println("成功连接服务端");
        while (!mIsDestory) {
            String str = in.readLine();
            if(str == null)
                break;//客户端断开连接
            int i = new Random().nextInt(mReturnMsgAry.length);
            String msg = mReturnMsgAry[i];
            out.println(msg);
        }
        in.close();
        out.close();
        client.close();
    }

}
  • 客户端代码:
public class TCPClientActivity extends AppCompatActivity implements View.OnClickListener{

    private final static int MSG_RECEIVE_MSG = 1;

    private final static int MSG_SOCKET_CONNECTED = 2;

    private Button mSendBtn;

    private TextView mMsgTextView;

    private EditText mMsgEditText;

    private PrintWriter mPrintWriter;

    private Socket mClient;

    private Handler mHander = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_RECEIVE_MSG:
                    mMsgTextView.setText(mMsgTextView.getText() + (String)msg.obj);
                    break;
                case MSG_SOCKET_CONNECTED:
                    mSendBtn.setEnabled(true);
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tcpclient);
        mMsgEditText = findViewById(R.id.et_msg);
        mMsgTextView = findViewById(R.id.tv_content);
        mSendBtn = findViewById(R.id.btn_send);
        mSendBtn.setOnClickListener(this);
        // 启动服务进程
        Intent intent = new Intent(this, TCPServerService.class);
        startService(intent);
        new Thread() {
            @Override
            public void run() {
                connectTCPServer();
            }
        }.start();

    }

    @Override
    protected void onDestroy() {
        if(mClient != null) {
            try {
                mClient.shutdownInput();
                mClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }

    private void connectTCPServer() {
        Socket socket = null;
        while (socket == null) {
            try {
                socket = new Socket("localhost", 9527);
                mClient = socket;
                mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
                // 发送连接成功的消息
                mHander.sendEmptyMessage(MSG_SOCKET_CONNECTED);
            } catch (IOException e) {
                SystemClock.sleep(1000);
                System.out.println("连接失败,重连中...");
            }
        }

        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (!TCPClientActivity.this.isFinishing()) {
                String msg = br.readLine();
                System.out.println("receive: " + msg);
                if (msg != null) {
                    String time = formateTime(System.currentTimeMillis());
                    // 追加消息
                    final String showMsg = "server(" + time + "):" + msg + "\n";
                    mHander.obtainMessage(MSG_RECEIVE_MSG, showMsg).sendToTarget();
                }
            }
            System.out.println("断开连接...");
            mPrintWriter.close();
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String formateTime(long time) {
        return new SimpleDateFormat("HH:mm:ss").format(new Date(time));
    }


    @Override
    public void onClick(View v) {
        if(v.getId() == mSendBtn.getId()) {
            final String msg = mMsgEditText.getText().toString();
            if(!TextUtils.isEmpty(msg) && mPrintWriter != null) {
                new Thread() {
                    @Override
                    public void run() {
                        mPrintWriter.println(msg);
                    }
                }.start();
                mMsgEditText.setText("");
                String time = formateTime(System.currentTimeMillis());
                String showMsg = "client(" + time + "):" + msg + "\n";
                mMsgTextView.setText(mMsgTextView.getText() + showMsg);
            }
        }
    }
}
  • 程序截图


    Screenshot_2018-10-26-13-39-35-286_IPCDemo.png

IPC方式的优缺点和适用场景

名称 优点 缺点 适用场景
Bundle 简单易用 只能传输Bundle支持的数据 四大组件之间的进程通信
文件共享 简单易用 不适合高并发场景,并且无法做到进程间的即时通信 无并发访问,交换简单的数据,且实时性不高的场景
AIDL 功能强大,支持一对多并发通信,支持实时通信 使用较复杂,需要处理好线程同步 一对多通信且有RPC需求
Messenger 支持一对多串行通信,实时通讯 不能很好处理高并发情形,不支持RPC,数据通过Message进行传输,因此只能传输Bundle数据 低并发的一对多即时通信,无RPC需求,或者无须返回结果的RPC需求
ContentProvider 在数据源访问方面功能强大,支持一对多并发共享数据,可通过Call方法扩展其他操作 可以理解为受约束的AIDL,主要提供数据源的CRUD操作 一对多的进程间的数据共享
Socket 功能强大,可以通过网络传输数据,支持一对多并发实时通信 实现细节繁琐,不支持直接的RPC 网络数据交换
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335