学习笔记| (二)IPC机制

一.多进程

1.创建多进程:
  • 在android 中通过 android:process为四大组件创建新的进程
  • 在特殊情况下也可以通过JNI去native层fork一个新的进程,但是一般不会这么做
  • android:process = ":remote" 是私有进程(其他应用的组件不会和他跑在同一个进程中)
  • android:process = "com.test.remote"是全局进程(其他应用通过ShareUID和它跑在同一个进程中)
2.Share UID:
  • android 系统会为每一个应用都分配一个UID(钥匙)
  • 只有UID相同的才能共享数据(房间)
  • 注意:要想在同一个进程中运行,除了UID要一样,签名也得一样;这种情况下的两个应用,不光能访问私有数据(比如data目录),还能共享内存数据
3.多进程的运行机制
  • 系统会为每个应用分配一个虚拟机,或者说,会为每一个进程分配一个虚拟机
  • 同一个应用内不同进程直接就像是复制品一样,每个进程内的东西是完全一样的,一个进程内变量或其他东西的改变,不影响其他进程,他们都有自己的内存空间
4.多进程的影响:
  • 静态成员变量和单例模式无效(原因就是3中所说的,每一个进程都有自己的内存空间了)
  • SharePreference无效(SharePreference不允许多个进程同时进行读写操作)
  • 线程同步机制无效(因为不同进程锁的都不是同一个对象)
  • 会创建多个Application(创建一个新的进程,就要创建一个新的虚拟机,就相当于打开了一个新的应用,会重新创建一个application。可以这么理解,在同一个进程中的应用,用同一个虚拟机和同一个Application)

二.IPC机制

1.Serizable接口
  • 使用:
    • 写一个类实现Serializable接口
    • 自己写一个serialVersionUID或者是系统自动生成,自己写的话可以保证序列化和反序列化正确
  • 不参与序列化的有:
    • 对于静态的成员变量
    • transient关键字标记的不参与
String fname = Environment.getExternalStorageDirectory() + "/test.txt";
        //序列化
        User user = new User(1,"lili",true);
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(fname));
            outputStream.writeObject(user);
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            Log.e("user","存入失败:"+e.getMessage());
        }
        //反序列化
        try {
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(fname));
            User user = (User) inputStream.readObject();
            Log.e("user","读取结果:"+user.toString());
            inputStream.close();
            textEdit.setText(user.toString());
        } catch (IOException e) {
            e.printStackTrace();
            Log.e("user","读取失败:"+e.getMessage());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            Log.e("user","读取失败222:"+e.getMessage());
        }
2.Parceable接口
Parcelable方法.png
  • 写一个类实现Parcelable接口
  • parceable方法说明.png
  • 在序列化对象中有一个是实现了Parcelable的类,序列化时需要:
    //序列化
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(book,0);
    }
    //反序列化 
    protected User(Parcel in) {
       //需要上传当前上下文的线程
        book = in.readParcelable(Thread.currentThread().getContextClassLoader());
    }
3.这两个接口的区别
区别 Serializable Parcelable
平台 java的序列化接口 android的序列化接口
原理 将一个对象转成可存储或可传输的状态 将一个对象分解成若干个支持传输的类型
使用场景 将序列化对象存储到设备上或者是用于网络传输(推荐) 内存序列化
优缺点 用到了大量的I/O操作(ObjectOutputStream/ObjectInoutStream),使用简单,但是消耗过大 高效,但是使用复杂
4.Binder
4.1 理解
  • Binder是android的一个类,实现了IBinder接口
  • 从IPC角度:是Android中一种跨进程通信方式,可以理解为一种虚拟的物理设备,它的驱动是/dev/binder
  • 从Framework层,是ServiceManager连接各种Manager和相应ManagerService的桥梁
  • 从应用层,是连接客户端和服务端的桥梁,bindService的时候,服务端会返回一个服务端的Binder对象给客户端,客户端可以通过这个Binder可以获取服务端提供的服务和数据
4.2 创建aidl

①创建Book.java实现Parceable接口
②创建Book.aidl(先取别的名字,之后再重命名)

// Book.aidl
package ly.com.artres.aidl;
parcelable Book;

③创建IBookManager.aidl

// IBookManager.aidl
package ly.com.artres.aidl;
//手动导包
import ly.com.artres.aidl.Book;

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

interface IBookManager {
    //获取所有图书列表
    List<Book> getBookList();

    //添加图书:AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out、inout,in表示输入型参数,
    //out输出型参数,inout表示输入输出型参数,AIDL中只支持方法,不支持声明静态常量,是不同于一般接口的
    void addBook(in Book book);
}

④Build-->Make project之后在

build/generate/source/aidl中可以看到编译后的IBookManager .java
4.3 手写aidl

所有通过Binder传输的接口都要继承IInterface接口

aidl内部实现.png

binder机制.png

/**
 * 手写aidl实现
 * 所有通过Binder传输的接口都要继承IInterface接口
 */
public interface BookManager extends IInterface{
    //1.1创建文件描述类
    static final String DESCRIPTION = "ly.com.artres.aidl.BookManager";

    //1.2创建两个方法的标识
    static final int TRANSACT_GETBOOKLIST = IBinder.FIRST_CALL_TRANSACTION + 0;
    static final int TRANSACT_ADDBOOK = IBinder.FIRST_CALL_TRANSACTION + 1;


    //1.3创建客户端要调用的几个方法
    List<Book> getBookList() throws RemoteException;
    void addBook(Book book) throws RemoteException;

    //1.4创建内部类Stub,实现外部接口,实际上是一个Binder
    public class BookManageImpl extends Binder implements BookManager{

        //2.1要有一个构造函数和外部接口进行绑定
        public BookManageImpl(){
            this.attachInterface(this,DESCRIPTION);
        }

        //2.2 将服务端的Binder转化为客户端需要的接口(BookManager)
        public static BookManager asInterface(Binder binder){
            if (binder == null){
                //客户端与服务端连接失败
                return null;
            }

            //根据描述,取出当前进程的接口
            IInterface iin = binder.queryLocalInterface(DESCRIPTION);
            if (iin != null && iin instanceof BookManager){
                //相同进程,取出的接口和客户端的接口相同,则返回服务端binder
                return (BookManager) binder;
            }

            //不同进程
            return new BookManageImpl.Proxy(binder);
        }

        //2.3 返回当前的binder对象
        @Override
        public IBinder asBinder() {
            return this;
        }

        /**
         * 2.4 客户端发起RPC(远程过程调用)请求--->运行在服务端的Binder线程池中,所以所有的Binder请求都要是同步的
         * @param code : 请求的方法标识
         * @param data : 请求参数
         * @param reply : 请求的返回结果
         * @param flags : 参数flags只有0和FLAG_ONEWAY两种,默认的跨进程操作是同步的,所以transact()方法的执行会阻塞,
         *              调用以同步的形式传递到远程的transact(),等待远端的transact()返回后继续执行——最好理解的方式就是把
         *              两端的transact()看作一个方法,Binder机制的目标也就是这样。指定FLAG_ONEWAY时,表示Client的transact()
         *              是单向调用,执行后立即返回,无需等待Server端transact()返回
         * @return 返回false,则客户端请求失败
         * @throws RemoteException
         */
        @Override
        public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
            //判断请求的哪个方法
            switch (code){
                case TRANSACT_GETBOOKLIST:
                    //获取图书列表
                    //有请求参数,读取请求参数(没有)
                    data.enforceInterface(DESCRIPTION);

                    //调用请求方法
                    List<Book> list = this.getBookList();

                    //请求完成后,需要返回值,则写到reply中
                    reply.writeNoException();
                    reply.writeTypedList(list);

                    return true;

                case TRANSACT_ADDBOOK:
                    //添加图书
                    //有请求参数,读取请求参数(有)
                    data.enforceInterface(DESCRIPTION);

                    Book _arg0;
                    if (0 != data.readInt()){
                        //将之前我们所存入的数据按照顺序进行获取,反序列化
                        _arg0 = Book.CREATOR.createFromParcel(data);
                    }else {
                        _arg0 = null;
                    }

                    //调用方法
                    this.addBook(_arg0);

                    //请求完成后,需要返回值,则写到reply中(没有返回值)
                    reply.writeNoException();

                    return true;

                case INTERFACE_TRANSACTION:
                    reply.writeString(DESCRIPTION);
                    return true;

                default:
                    break;
            }

            return super.onTransact(code, data, reply, flags);
        }

        //2.4创建代理类
        private static class Proxy implements BookManager{
            IBinder mRemote;

            /**
             * 服务端的binder
             * @param binder
             */
            Proxy(IBinder binder){
                this.mRemote = binder;
            }

            /**
             * 运行在客户端
             * @return
             * @throws RemoteException
             */
            @Override
            public List<Book> getBookList() throws RemoteException {
                Parcel _data = Parcel.obtain();//创建输入型对象
                Parcel _reply = Parcel.obtain();//创建输出型对象
                List<Book> _result = null;//创建结果对象

                try {
                    //写入请求参数(为空)
                    _data.writeInterfaceToken(DESCRIPTION);

                    //发起RPC请求
                    mRemote.transact(TRANSACT_GETBOOKLIST,_data,_reply,0);

                    //返回结果(list)
                    _reply.readException();
                    _result = _reply.createTypedArrayList(Book.CREATOR);
                }catch (Exception e){
                    e.getMessage();
                }finally {
                    //回收
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            /**
             * 运行在客户端
             * @param book
             * @throws RemoteException
             */
            @Override
            public void addBook(Book book) throws RemoteException {
                Parcel _data = Parcel.obtain();//创建输入型对象
                Parcel _reply = Parcel.obtain();//创建输出型对象

                try {
                    //写入请求参数
                    _data.writeInterfaceToken(DESCRIPTION);
                    if (book != null){
                        //序列化
                        _data.writeInt(1);
                        book.writeToParcel(_data,0);
                    }else {
                        _data.writeInt(0);
                    }

                    //发起RPC请求
                    mRemote.transact(TRANSACT_ADDBOOK,_data,_reply,0);

                    //返回结果(为空)
                    _reply.readException();
                }catch (Exception e){
                    e.getMessage();
                }finally {
                    //回收
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public IBinder asBinder() {
                return mRemote;
            }

            public String getInterfaceDescription(){
                return DESCRIPTION;
            }
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            return null;
        }

        @Override
        public void addBook(Book book) throws RemoteException {

        }
    }

}

4.4 两个重要方法
  • unlinkToDeath()
private IBinder.DeathRecipient mRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if (mManage == null){
                //断开之前的连接
                mManage.asBinder().unlinkToDeath(mRecipient,0);
                mManage = null;
            }
        }
    };
  • linkToDeath():
binder.linkToDeath(mRecipient,0);

三、Android中的IPC方式:

  • Bundle
  • ContentProvider
  • AIDL
  • Messenger
  • 文件共享
  • Socket
3.1 Bundle
  • Bundle实现了Parcelable接口,可以通过Intent传递
  • Bundle可以放基本数据类型,实现了Parcelale或Serializable接口的类,还有特殊数据类型
       Intent intent = new Intent(this,BActivity.class);
        Bundle bundle = new Bundle();
        bundle.putString("key1","你在干啥");
        intent.putExtra("intentkey",bundle);
        startActivity(intent);
  • 特殊使用场景:
    进程A中的计算结果要在进程B中展示,且这个计算结果不支持Bundle传输,可以通过Intent启动B的一个Service组件,然后B在后台进行计算,计算完成后在B中显示
3.2 Messenger(信使)
  • 是一种轻量级的ipc方案
  • 底层是AIDL
  • 串行的方式处理消息,客户端发一个消息,服务端处理一个,当客户端发了大量的消息的时候,服务端也只能一个一个处理,效率低,不适合用Messenger处理。
  • Messenger主要是用来传递消息,当要调用服务端的方法时就不适用了。
    messenger流程图.jpg

原理分析:
①client和server绑定后
②client在onServiceConnected中通过IBinder生成一个Messenger发送Message消息
③server接收到client发来的消息后,会在Handler中进行处理
④server回复消息给client,也是通过Messenger,他发送的消息需要客户端处理,所以这个Messenger得是client的,怎么创建客户端的Messenger呢?只需要在②中,发送消息的时候,通过msg.replyTo = xx,就可以把client的messenger传过来了

  • 注意:
    在跨进程中不能传递自定义的Parceable对象
3.3 AIDL
  • (1)支持的数据类型:

    aidl支持的数据类型.png

    自定义的Parcelable对象和AIDL都要手动import

  • (2)解注册跨进程的接口时:

@Override
    protected void onDestroy() {
        if (mRemoteManager != null && mRemoteManager.asBinder().isBinderAlive()){
            //注销
            Log.e("testaidl","onDestroy unregister listener:"+mNewBookListener);
            try {
                mRemoteManager.unregisterListener(mNewBookListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        unbindService(mConnection);
        super.onDestroy();
    }

这样写的时候会解注册失败,因为这时候的manager和service端的都不是同一个了,aidl不能传输对象,这里的manager相当于两个新对象。
要删除这个跨进程的接口,要用到RemoteCallBackListener;
使用方法:

 //用于删除跨进程的接口
private RemoteCallbackList<IOnNewBookArrivedLisneer> mListennerist = new RemoteCallbackList<>();

@Override
public void registerListener(IOnNewBookArrivedLisneer listener) throws RemoteException { 
            mListennerist.register(listener);
        }

@Override
public void unregisterListener(IOnNewBookArrivedLisneer listener) throws RemoteException {
            mListennerist.unregister(listener);
        }

注意:
在使用RemoteCallbackList时要注意:

      //获取元素个数
       int N = mListennerist.beginBroadcast();
        for (int i=0;i<N;i++){
            IOnNewBookArrivedLisneer lisneer = mListennerist.getBroadcastItem(i);
            if (lisneer != null){
                try {
                    lisneer.onNewBookArrived(book);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }

        //要和beginBroadcast配套使用
        mListennerist.finishBroadcast();

(3)在UI线程中调用远程端的耗时方法(互调):

  • 服务端中的方法都运行在Binder线程池中,客户端调用服务端中的耗时方法时,会造成ANR,所以要在客户端开启一个子线程才能调用服务端的方法;
  • 服务端的调用客户端Binder线程池中的某一个耗时方法时,也要开始一个线程才行;

(4)Binder意外死亡的时候:

  • 可以在onServiceDisconnected中重新连接
  • 可以在DeathRecipient 中断开之前的连接,再重新绑定
IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
        }
    };
  • 他们二者的区别:
    onServiceDisconnected是运行在UI线程中,而DeathRecipient 是运行在客户端的Binder线程池中的

(5)权限验证:
声明权限

<!--权限验证,验证通过才能连接服务器-->
<permission android:name="ly.com.artres.aidl.ACCESS_BOOK_SERVICE" android:protectionLevel="normal"/>

使用权限:

<uses-permission android:name="ly.com.artres.aidl.ACCESS_BOOK_SERVICE"/>
  • 可以在onBinder()中验证权限
@Override
    public IBinder onBind(Intent intent) {
        //权限验证
        int permission = checkCallingOrSelfPermission("ly.com.artres.aidl.ACCESS_BOOK_SERVICE");
        if (permission == PackageManager.PERMISSION_DENIED){
            return null;
        }
        return mBinder;
    }
  • 可以在onTransact()中通过权限验证或者Uid,Pid进行验证

  • 总结:创建一个service和AIDL接口,创建一个类实现AIDL接口中的Stub类并实现里面的抽象方法,在service的onBind()中返回这个类的对象,然后在客户端就可以绑定服务端Service,继而访问远程接口

3.4 ContentProvider
  • 底层是Binder
  • 写一个类继承ContentProvider
    要实现6个方法:
    ①boolen onCreate():创建,做初始化工作,运行在UI线程中;
    ②String getType(Uri uri):根据uri获取对应的MIME(媒体)类型:文字/图片/视频,在主线程;
    ③Uri insert(@NonNull Uri uri, @Nullable ContentValues values):插入数据,在binder线程池中运行;
    ④int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs):删除数据,在binder线程池中运行;
    ⑤int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs):更新数据,在binder线程池中运行;
    ⑥Cursor query(...):查询数据,在binder线程池中运行;

实现步骤:

  • 在manifest中定义这个provider
<provider
            android:name=".contentproviderdemo.BookProvider"
            android:authorities="ly.com.artres.provider"//唯一标识
            android:permission="ly.com.PROVIDER"//权限
            android:process=":process" />//这个可以不写
  • 根据Uri-->得到UriCode--->再判断是哪一张表
  • 将uri和uricode进行绑定
   //创建Uri和UriCode
    private static String AUTHORITY = "ly.com.artres.provider";
    private static Uri BOOK_URI = Uri.parse("content://"+AUTHORITY+"/book");
    private static Uri USER_URI = Uri.parse("content://"+AUTHORITY+"/user");
    public static final int BOOK_CODE = 0;
    private static final int USER_CODE = 1;

   //创建urimatch
    private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        uriMatcher.addURI(AUTHORITY,"book",BOOK_CODE);
        uriMatcher.addURI(AUTHORITY,"user",USER_CODE);
    }

     /**
     * 根据Uri得到Uricode,再判断是哪一张表
     * @param uri
     * @return
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (uriMatcher.match(uri)){
            case BOOK_CODE:
                tableName = DBOpenHelper.BOOK_TABLE_NAME;
                break;
            case USER_CODE:
                tableName = DBOpenHelper.USER_TABLE_NAME;
                break;
            default:
                break;
        }
        return tableName;
    }


   @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        Log.e("testcp","insert():"+Thread.currentThread().getName());

        String tableName = getTableName(uri);
        if (tableName == null){
            throw new IllegalArgumentException("insert unsupport uri:"+uri);
        }

        mDb.insert(tableName,null,values);
        mContext.getContentResolver().notifyChange(uri,null);
        return null;
    }

SQLite数据库:

  • 底部实现了线程同步,如果provider不使用sqlite数据库,要注意在CRUD中使用线程同步;
  • 自定义一个类继承SQLiteOpenHelper,必须要有一个构造方法;
  • SQLiteOpenHelper用于创建升级数据库;
public class DBOpenHelper extends SQLiteOpenHelper {
    public static final String DB_NAME = "book.db";
    public static final String BOOK_TABLE_NAME = "booktb";
    public static final String USER_TABLE_NAME = "usertb";
    public static final int DB_VERSION = 1;

    private String CREATE_BOOK = "create table if not exists "+BOOK_TABLE_NAME+"(_id integer primary key,name text)";
    private String CREATE_USER = "create table if not exists "+USER_TABLE_NAME+"(_id integer primary key,name text)";


    /**
     * 必须要有一个构造函数
     * @param context
     */
    public DBOpenHelper(Context context) {
        super(context,DB_NAME,null,DB_VERSION);
    }


    @Override
    public void onCreate(SQLiteDatabase db) {
       //创建表
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_USER);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

根据sqliteopenhelprt可以创建sqlitedatabase,然后在contentprovider的CRUD方法中调用sqlitedatabase的增删改查的方法;

SQLiteDatabase mDb = new DBOpenHelper(mContext).getWritableDatabase();

在Activity中往provider中插入数据时:

        Uri uri = Uri.parse("content://ly.com.artres.provider/book");
        ContentValues values = new ContentValues();
        values.put("_id",4);
        values.put("name","语文");
        getContentResolver().insert(uri,values);

在Activity中从provider中查询数据时:

        Uri uri = Uri.parse("content://ly.com.artres.provider/book");
        Cursor bookCursor = getContentResolver().query(uri,new String[]{"_id","name"},null,null,null);
        while (bookCursor.moveToNext()){
            int id = bookCursor.getInt(0);
            String name = bookCursor.getString(1);
            Log.e("testcp","query book结果:"+id+",name:"+name+"\n");
        }

        bookCursor.close();
3.5 文件共享
  • 实现Serializable接口
  • 通过ObjectOutputStream()和ObjectInputStream()实现
3.6 Socket(套接字)

1.分为两种:

  • 流式套接字-->对应TCP(面向连接,提供稳定的双方通信功能,要建立三次握手,本身具有超时重连机制)
  • 用户数据报套接字-->对应UDP(也能实现双方通信功能,性能会更高,但是数据不一定能传输成功,)

2.可以传输任意字节流

3.使用:

  • 要有权限(不能在主线程访问网络
    <!-- socket要的权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  • 步骤:
    服务端:
①服务端创建Socket
ServerSocket serverSocket = new ServerSocket(8688);

②接收客户端请求:
final Socket client = serverSocket.accept();

③接收客户端请求
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
String str = in.readLine();

④向客户端发送消息
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())),true);
out.println("欢迎来到聊天室");

客户端:

①连接服务端
Socket socket = new Socket("localhost",8688);

②向服务端发送请求:
PrintWriter mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
mPrintWriter.println(“hello”);

③接收服务端的消息:
 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = in.readLine();

四、Binder连接池:

五、选择合适的IPC方式:

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

推荐阅读更多精彩内容