Android IPC(二)

Android IPC

Android中的IPC方式

使用Bundle

Android的四大组件都支持在Intent中传递Bundle数据的,并且由于Bundle实现了Parcelable接口,所以它可以很方便地在不同进程中进行传输。
基于这一点,我们在一个进程中启动另外一个进程的Activity、Service、Receiver,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送出去。

除了直接传输数据这种场景外,还可能出现一种特殊情况。比如A进程的组件在进行一个计算,并需要把计算后的结果传递给B进程的一个组件,但是计算结果不能直接放入Bundle中,这个时候假如选择其他IPC方式可能会略显复杂。可以考虑使用以下方式,我们先通过Intent在B进程启动一个组件(比如IntentService),让Service在后台计算,计算完毕后才启动真正要启动的组件,这样一来因为组件运行在B进程中,可以让目标组件通过别的方式直接获取计算结果,解决了上述跨进程的问题。

使用文件共享

通过两个进程读/写同一个文件来进行数据的共享。Android上对并发读/写文件可以没有限制的进行,甚至两个线程同时对同一个文件进行写操作都是允许的。
  而文件共享除了交换信息,也可以通过序列化的方式进行对象的交换。
  文件共享主要的问题是并发读/写问题,因此我们要尽量避免这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。
  通过上述可以得知,文件共享的方式在对数据要求同步要求不高的进程可以进行通信,并且要避免并发读写的问题。
  SharePreferences是一个特例,底层通过XML文件来实现键值对的保存,也属于文件的一种,但是Android系统会对它的读写有一定的缓存,即每一个内存中都有一份SharePreserences文件的缓存,因此在多进程模式中,系统对它的读/写就变得不可靠,在高并发的场景中可能有很大的几率会丢失数据。因为不适宜在多进程中进行使用。

使用Messenger

Messenger底层实现AIDL,通过它可以在不同进程中传递Message对象。在Message中可以放入我们需要传递的数据,就可以轻松地实现数据在进程之间的传递。
  Messenger的构造方法如下

public Messenger(Handler target) {
    mTarget = target.getIMessenger();
}

public Messenger(IBinder target) {
    mTarget = IMessenger.Stub.asInterface(target);
}

Messenger的使用方法简单,它对AIDL进行1了封装,使得我们可以更简便地进行线程间通信。同时又由于它一次处理一个请求,因此在服务端我们可以不同考虑线程同步的问题,因为服务器不存在并发执行的情形。
  而Messenger的创建分为服务端部分和客户端部分。

服务端部分

服务端部分通过注册Service来响应请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在OnBind中返回这个Messenger对象底层的Binder,代码如下

private static class MessengerHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MyConstants.MSG_FROM_CLIENT:
                // 处理请求
                Messenger client = msg.replyTo;
                Message replyMessage = Message.obtain(null, MyConstants.MSG_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("reply", "message");
                replyMessage.setData(bundle);
                
                try {
                    client.send(replyMessage);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                    
                break;
            default:
                super.handleMessage(msg);
        }
    }
}

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

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

上述代码中,MessengerHandler用来处理客户端发送的消息,可以从消息中获取客户端发送的数据。而mMessenger是一个Messenger对象,它和Messenger相关联,并在OnBinder里面返回它里面的Binder对象。
  而这里Messenger的作用是将客户端发送的消息传递给MessengerHandler处理。
  在Hanler中,可以通过Message的reply参数获取客户端发送的Messenger对象,并对它调用send方法发送数据到客户端处理。

客户端部分

客户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务器发送Message类型的消息。
  如果需要服务端也能回应客户端,那么则需要在客户端上创建一个Handler并创建一个新的Messenger,同时将这个Messenger对象通过Message的replyTo参数传递给服务端,代码如下。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

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

    private static class MessengerHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MyConstants.MSG_FROM_SERVICE:
                    Log.i(TAG, "handleMessage: " + msg.getData().get("reply"));
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
            Message msg = Message.obtain(null, MyConstants.MSG_FROM_CLIENT);
            Bundle data = new Bundle();
            data.putString("msg", "hello, this is client");
            msg.setData(data);
            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_main);

        Intent intent = new Intent(this, MessengerService.class);
        bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
    }

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

上述代码中,客户端首先绑定了远程服务进程的MessengerService,绑定成功后,根据服务端返回的binder对象创建Messenger对象,构造Message对象并并通过Messenger对象向服务端发送消息。
  而为了接受服务端的回复信息,客户端也需要准备一个接受消息的Handler和Messenge对象,并在发送消息的时候,通过reply参数将接受服务端回复的Messenger传递给服务端。

总结

Binder工作原理

  上图是Messenger的工作原理,可以通过Messenger实现更复杂的功能。
  需要注意的是,Messenger是使用串行的方式来处理客户端发来的消息,而如果大量的消息同时发送到服务端,服务端仍只能一个个地处理,而可以看出Messenger不适合用于处理大量的并发请求这种场景。同时Messenger的作用主要是为了传递信息,而我们很多时候需要跨进程调用服务端的方,这种情形Messenger就无法做到了,需要使用AIDL。

AIDL

Messenger是通过AIDL实现的,而使用AIDL同样也可以进行进程间通信。这里先介绍使用AIDL进行进程间通信的流程,分为客户端和服务器部分。

服务端

服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口。

客户端

客户端首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中定义的方法了。
  而AIDL实现的过程还要更复杂一些,包含着许多难点和细节,以下将详细介绍。

AIDL接口的创建

IBookManager.aidl
package com.daijie.aldlapp;

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

import com.daijie.aldlapp.Book;

interface IBookManager {

    List<Book> getBookList();

    void addBook(in Book book);
}

在AIDL中,并不是所有的数据类型都可以使用的,AIDL支持的数据类型如下。

  • 基本数据类型(int、long、char、boolean、double等)
  • String和CharSequence;
  • List:只支持ArrayList,并且里面的每个元素都必须能够被AIDL支持
  • Map:只支持HashMap,并且里面的每个元素都能够被AIDL支持,包括key和value
  • Parcelable:所有实现了Parcel接口的对象
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用

在以上的6种类型中,自定义的Parcel对象和AIDL对象不管是否与当前的AIDL文件位于同一个包内,都必须显示的import进来。
  在AIDL文件中如果用到了Parcel对象,则必须新建一个与它同名edAIDL文件,并在其中声明它为Parcel类型。在上面IBookManager.aidl中用到了Book类,所以必须创建Book.aidl,并添加以下的内容。

Book.aidl
package com.daijie.aldlapp;

parcelable Book;

AIDL中每个实现了Parcelable的接口的类都需要按照上面的那种方式去创建响应的AIDL文件并声明那个类为parcelable。除此之外,AIDL除了基本数据类型,其他类型的参数必须标上方面:in、out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。这三个参数需要根据实际情况去指定。不能一概使用out或者inout,因为底层是存在开销的。
  最后,区别于传统的java接口,AIDL只支持方法,不支持声明静态常量。

为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样子做的原因是,当客户端是另外一个应用的时候,可以直接将整个包复制到那个客户端工程中,而避免麻烦和出错。
  需要注意的是,AIDL的包结构在服务端和客户端要保持一致,否则会运行出错。因为在客户端需要反序列化服务端中和AIDL相关的所有类,如果类的路径不一样,就会造成反序列化失败。

远程服务端Service的实现

public class BookManagerService extends Service {

    private CopyOnWriteArrayList<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(1, "Android"));
        mBookList.add(new Book(2, "iOS"));
    }

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

以上是一个远程Service的实现,在onCreate中初始化添加了两本书,然后创建了一个Binder对象并在onBind中返回,而这个Binder对象继承了IBookManager.Stub并实现了它内部的AIDL方法。
  而这里管理书籍内容的List是通过CopyOnWriteArrayList进行管理的,它支持并发的读/写,因为AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以需要在AIDL方法中处理线程同步。

虽然AIDL中能使用的List只有ArrayList,但是这里却使用了CopyOnWriteArrayList(CopyOnWriteArrayList并非是ArrayList的子类)。这里的原因是因为AIDL中支持的是一个抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList,但是在Binder中会按照List的规范去访问数据并最终形成一个ArrayList给客户端。
  与此同时,ConcurrentMap也是可以被支持的。

客户端的实现

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;
            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list: " + list.toString());
                Book newBook = new Book(3, "Android开发艺术探索");
                bookManager.addBook(newBook);
                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list: " + newList.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };

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

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

客户端的代码比较简单,首先要绑定远程服务,绑定成功后将服务端返回的Binder对象转化为AIDL接口,然后就可以通过这个接口去调用服务端的远程方法。
  需要注意的是,服务端的方法可能会比较耗时,而在UI线程中直接运行可能会导致ANR。因为客户端线程会在调用远程方法后挂起直至方法返回。

观察者模式

我们可以设计出一个需求,每个感兴趣的用户都在观察着新书,而图书馆可以在新书到来的时候通知这些用户,这就可以利用观察者模式来实现。
  而要实现这个功能,需要定义一个AIDL接口,然后让客户端需要实现这个接口并且向图书馆申请新书的提醒功能,当然也可以随时取消订阅。这里使用AIDL接口而并非普通接口的原因是,在AIDL文件中无法使用普通接口。
  这里创建一个IOnNewBookArrivedListener文件,当有新书来的时候去通知每一个订阅的用户,从程序上来说就是调用一个方法,并把新书的参数传递进去,代码如下

IOnNewBookArrivedListener
package com.daijie.aldlapp;

import com.daijie.aldlapp.Book;

interface IOnNewBookArrivedListener {
    void OnNewBookArrived(in Book newBook);
}
IBookManager.aidl
package com.daijie.aldlapp;

import com.daijie.aldlapp.Book;
import com.daijie.aldlapp.IOnNewBookArrivedListener;

interface IBookManager {
    List<Book> getBookList();

    void addBook(in Book book);

    void registerListener(IOnNewBookArrivedListener listener);

    void unregisterListener(IOnNewBookArrivedListener listener);
}

这里除了要新加一个AIDL接口,还需要在原有的接口上添加两个新的方法。
  而服务端的Service的Binder也要去实现这两个新加的接口,同时在服务端开启一个新的线程,每隔5s就添加一本书并通知订阅更新的客户端,代码如下。

服务端代码
public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private AtomicBoolean mIsServiceDestroy = new AtomicBoolean(false);

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private RemoteCallbackList<IOnNewBookArrivedListener> mListeners =
            new RemoteCallbackList<>();

    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 registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListeners.register(listener);
            Log.i(TAG, "registerListener: " + mListeners.beginBroadcast());
            mListeners.finishBroadcast();
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListeners.unregister(listener);
            Log.i(TAG, "unregisterListener: " + mListeners.beginBroadcast());
            mListeners.finishBroadcast();
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "iOS"));
        new Thread(new ServiceWorker()).start();

    }


    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestroy.set(true);
    }

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

    private void onNewBookArrived(Book book) throws RemoteException {
        mBookList.add(book);
        final int N = mListeners.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListeners.getBroadcastItem(i);
            if (l != null) {
                try {
                    l.OnNewBookArrived(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListeners.finishBroadcast();
    }

    private class ServiceWorker implements Runnable {

        @Override
        public void run() {
            while (!mIsServiceDestroy.get()) {
                try {
                    Thread.sleep(5 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = mBookList.size() + 1;
                Book newBook = new Book(bookId, "new Book#" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

而修改完服务端代码后,客户端代码也要进行修改,主要在两方面:客户端需要注册OnNewBookArrivedListener到远程服务端,这样当有新书的时,服务端才能通知当前客户端,同时我们需要在Activity销毁;另一方面,当有新书的时候,服务端会回调客户单的IOnNewBookArrivedListener对象的onNewBookArrived方法,但是这个方法是在客户端的Binder线程池中执行的,因此为了方便UI操作,需要有一个Handler可以将其切换到客户端的主线程中去执行。

客户端代码
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    public static final int MESSAGE_NEW_BOOK_ARRIVED = 1;

    private IBookManager mRemoteBookManager;

    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_NEW_BOOK_ARRIVED:
                    Log.d(TAG, "receive new book: " + msg.obj);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    };

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;
            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list: " + list.toString());
                Book newBook = new Book(3, "Android开发艺术探索");
                bookManager.addBook(newBook);
                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list: " + newList.toString());
                bookManager.registerListener(mIOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

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

    private IOnNewBookArrivedListener mIOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {

        @Override
        public void OnNewBookArrived(Book newBook) throws RemoteException {
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
        }
    };


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

    @Override
    protected void onDestroy() {
        if (mRemoteBookManager != null && mRemoteBookManager.asBinder().isBinderAlive()) {
            try {
                Log.i(TAG, "unregister listener: " + mIOnNewBookArrivedListener);
                mRemoteBookManager.unregisterListener(mIOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
        super.onDestroy();
    }
}

以上代码可以正确运行,并且每隔5s,客户端就收到了来自服务端的新书推送。
但在Service的代码中使用的是RemoteCallbackList而并非CopyOnWriteArrayList,原因如下。
  假如这里使用到了CopyOnWriteArrayList,我们则需要通过客户端传入的listener来判断是否在CopyOnWriteArrayList中是否存在该对象后进行移除,尽管在订阅和取消订阅的时候使用的都是同一个客户端对象,但是由于Binder的机制,在客户端传输进来的对象会重新转化并生成一个新的对象,而无法进行匹配。对象的跨进程传输本就是一个序列化和反序列化的团,所以自定义对象才需要实现Parcelable接口。
  RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。RemoteCallbackList是一个泛型类,支持管理任意的AIDL接口,这个从它的声明可以看出,因为所有的AIDL接口都继承于IInterface,以下是它的声明。

public class RemoteCallbackList<E extends IInterface> 

它的工作原理很简单,在它内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型,如下所示

ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

其中Callback属性封装了真正的远程listener。当客户端注册listener的时候,它就将这个listener的信息存入mCallbacks中,其中key和value即分别通过以下方式获取。

IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);

虽然多次跨进程传输客户端的同一个对象在服务端会生成不同的对象,但是这些对象都有一个共同点,那么就是它们的底层Binder对象都是同一个,而利用这个特性,客户端取消订阅的时候,只要遍历服务端所有的listener,找出那个和取消订阅的listener具有相同对象的服务端listener并把它删掉即可。
  以上就是RemoteCallbackList为我们做的事情,同时它可以在客户端进程终止的时候,它能够自动移除客户单进程所注册的listener。另外,它内部还自动实现了线程同步的功能,所以不用做额外的线程同步工作,
  使用RemoteCallbackList要注意的是,它虽然名字里面有List,但是我们不能像操作一个List那样操作它,遍历RemoteCallbackList,必须按照下面方式进行,而其中必须beginBroadcast和finishBroadcast配对使用,哪怕只是获取RemoteCallbackList的元素个数。

final int N = mListeners.beginBroadcast();
for (int i = 0; i < N; i++) {
    IOnNewBookArrivedListener l = mListeners.getBroadcastItem(i);
    if (l != null) {
        //TODO hander l
    }
}
mListeners.finishBroadcast();

AIDL基本使用方法已经介绍完了,但是还是有几点需要注意。
  客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法比较耗时,就会导致客户端长时间的阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致UI线程ANR。因此如果这个远程方法是耗时的话,就要避免在客户端的UI线程去访问远程方法。由于客户端的onServiceConnected和onServiceDisconnected都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法。
  另外,由于服务端的方法本身就是运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时工作,这个时候就切记不要在服务端中开线程去执行异步任务,除非明确是要干什么,否则不建议。

Binder重连

为了程序的健壮性考虑,Binder是可能会出现意外死亡的,这往往是由于服务端进程意外终止了,这个时候则需要重新连接服务,有两种办法。
  第一种方法是给Binder设置DeathRecipient监听,当Binder死亡时,我们会收到binderDied方法的回调,在binderDied中重连远程服务。
  另一种方法是在onServiceDisconneced中重连远程服务。
  它们的区别在于,onServiceDisconneced是在客户端的UI线程被回调,而bindDied在客户端的Binder线程池被回调。

权限验证

Binder中验证

第一种办法,我们可以在onBind中验证,验证不通过就返回null,这样验证失败的客户端就无法绑定服务,而验证方式可以有多种,比如使用permission进行验证。

在onTransact方法中做权限验证

第二种方法,在onTransact方法中做权限验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL的方法达到保护服务端的掉过,验证的方式也很多,比如permission验证。,还可以通过getCallingUid和getCallingPid获取客户端所属的Uid和Pid进行验证。

其他方法

除了以上两种比较常用的方法外,还有其他方法,比如在Service中指定android:permission属性等。

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

推荐阅读更多精彩内容