Android 关于IPC机制的理解(二)

上篇文章介绍了IPC机制的基本概念以及简单使用,文章链接:Android 关于IPC机制的理解(一) 这篇文章主要是关于IPC的方式,也就是用什么方式来进行进程间通信;
介绍使用前首先我们得了解几个概念,就是序列化和反序列以及Binder。
序列化和反序列化
在Java中序列化是指把java对象转换成字节码的过程,反序列化是指把字节码恢复为Java对象的过程,既然涉及进程间的通信那就必须涉及到序列化了和反序列化,在Android中一共有两个接口可以完成,第一个是Serializable另一个是Parcelable;
Serializable是Java1.1出来的一个特性,它是一个空接口,用它可以为对象提供序列化和反序列化操作,Serializable使用起来不困难,只要在实体类实现这个接口然后自定义添加一个唯一标识就可以了,这里就不在举例了;
值得说一说的是Parceable,Parcelable是Android为我提供的一个实现序列化方法的接口,只要一个对象实现这个接口就可以实现了序列化然后用Intent、Binder来传递这个对象了,可能我们会有疑问,既然有了Serializable为什么Android还要提供这个接口来实现序列化呢?其实是有原因的,Serializable虽然使用简单但是开销很大,因为Serializable是通过I/O操作来实现的,在序列化过程中用到了大量的反射,产生了很多临时变量,Parceable相对来说会更轻便,性能上要优于Serializable,所以Android也推荐使用Parceable来实现序列化。下面用一个例子实现Parcelable然后讲解里面的方法。

public class ParcelableBean implements Parcelable { String name; int year; /** * 返回当前对象的内容描述 * @return / @Override public int describeContents() { return 0;//如果有文件扫描符,返回1,没有返回0 } /* * 将当前对象写入序列化结构中 * * @param dest 序列化对象 * @param flags 标识值 / @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.name); dest.writeInt(this.year); } public ParcelableBean() { } /* * 从序列化后的对象中创建原始对象 * @param in 序列化后的对象 / protected ParcelableBean(Parcel in) { this.name = in.readString();//读取序列化对象中的字符串值 this.year = in.readInt(); } /* * 反序列化功能 / public static final Parcelable.Creator<ParcelableBean> CREATOR = new Parcelable.Creator<ParcelableBean>() { /* * 从序列化后的对象中创建原始对象 * @param source * @return / @Override public ParcelableBean createFromParcel(Parcel source) { return new ParcelableBean(source); } /* * 创建指定长度的原始对象数组 * @param size * @return */ @Override public ParcelableBean[] newArray(int size) { return new ParcelableBean[size]; } };}
上面的对象就可以通过Intent进行数据传递了,其实大多数人不用Parcelable是因为它要写一些模板代码,然后Serializable则不用,所以很多程序员图省事就直接使用了Serializable,其实如果你使用Android Studio进行开发是可以下一个Android Parcelable code generator插件然后就可以自动生成那些模板代码了。

Binder

Binder要研究起来是可以很深入和很复杂得,这里就不在详述,如果有兴趣可以参考这篇文章<<Android Bander设计与实现 - 设计篇>>,本文就大概介绍,Binder是Android中的一个实现了IBinder接口的类,也是一种跨进程通信的方式,从Android应用框架层(Android Framework)来说它是ServiceManager链接各种Manager和相应ManagerService的桥梁;从Android应用层(Android Applications)来说Binder是客户端和服务端进行通信的中介,比如Service中的bindService时服务端会返回一个包含Binder对象,同过这个对象就可以获取服务端提供的数据,在Android中Binder主要用在Service中,下文讲Android中跨进程通信的方式时也会说到Binder。

Android进行跨进程通信的方式
(1).使用Bundle
在Android中,Activity、Service、Receiver都可以用Bundle进行数据传递,这是因为Bundle实现了Parcelable接口,这个我们在日常开发应该是经常使用的,值得注意的是在使用Bundle附加需要传输的数据时要确定传输的数据可以被序列化,也就是传递的对象必须实现了Parcelable或Serializable接口。

(2).文件共享
就是两个进程读/写同一个文件来进行数据交互但是在使用这种方式进行通信的同时要注意并发的问题,避免两个进程同时并发写当前文件,这种方式适合在对数据同步性要求不高的进程间进行通信。

(3).使用Messenger
通过在Messenger中存放需要进行交互的数据,然后就可以在进程间进行传递,下面用一个例子来说明如何使用Messenger在不同进程中进行数据传递,例子很简单,就是Activity中通过Messenger给Service发送段话,首先是Activity的代码。

public class MainActivity extends Activity { MyServiceConnection con; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent = new Intent(this, MyService.class); con = new MyServiceConnection(); bindService(intent, con, BIND_AUTO_CREATE);//绑定服务 } /** * 服务连接 / class MyServiceConnection implements ServiceConnection { /* * 已经建立连接 * * @param name * @param service / @Override public void onServiceConnected(ComponentName name, IBinder service) { Messenger msgr = new Messenger(service); Message msg = new Message(); msg.what = 1;//标示 Bundle data = new Bundle(); data.putString("msg", "哈喽"); msg.setData(data); try { msgr.send(msg);//发送消息 } catch (RemoteException e) { e.printStackTrace(); } } /* * 连接失败 * * @param name */ @Override public void onServiceDisconnected(ComponentName name) { } } @Override protected void onDestroy() { super.onDestroy(); unbindService(con);//解绑服务 }}
接着是Service,这里我已经给Service设置了不同的进程

<service android:name=".MyService" android:process=":messenger" />

Service代码为

public class MyService extends Service { private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { Log.d("print", msg.getData().getString("msg")); } } }; private final Messenger msgr = new Messenger(handler); @Nullable @Override public IBinder onBind(Intent intent) { return msgr.getBinder(); }}
Log为:



通过上面的小例子我们就完成了一次使用Messenger在不同进程间通信了。但是用Messenger是有一定局限性的,因为Messenger是用串行的方式来处理消息,如果数据大量的话,也只能是一个个来发送和处理,这样应对数据并发显然不大可能,而且这只是数据交互,如果想调用不同进程方法显然更不可能。

(4)使用AIDL
AIDL(Android interface definition language)安卓接口定义语言,它的作用就是进行跨进程通信,下面用一个示例来演示两个进程间通信, 在Activity中调用另一个进程中Service的方法,我用的是Android studio,在这个工具中使用AIDL跟Eclipse略有不同,下面我一步来说明怎么实现功能:
首先右键新建一个aidl文件,这里我命名为IMyAidlInterface .aidl,这时as会自动帮我生成一个.aidl的文件,如图:



然后修改.aidl文件代码如下

// IMyAidlInterface.aidlpackage com.sjr.ipcdemo;// Declare any non-default types here with import statements/** * 注意AIDL只支持下面几种类型:基本数据类型,String、CharSequence、ArrayList、Map、 * 实现了Parcelable的接口对象、其他AIDL接口, /interface IMyAidlInterface { /* * Demonstrates some basic types that you can use as parameters * and return values in AIDL.\ * */ void show();}

上面的void show()方法是我想要共享的方法,这时gen目录并没有生成对应的接口文件(跟Eclipse不同),我们在AS中点击Build->Make Project(快捷键Ctrl+F9),这样gen目录就生成了对应的接口文件了,如图



生成的接口文件为(这里没兴趣的可以跳过,只是我加了点注释便于理解Android为我们生成的是什么,有点深入了)

/* * This file is auto-generated. DO NOT MODIFY.(这个文件是自动生成的,不要修改!!!!) * Original file: D:\sjr\lianyiwork\MyApplication\ipcdemo\src\main\aidl\com\sjr\ipcdemo\IMyAidlInterface.aidl /package com.sjr.ipcdemo;// Declare any non-default types here with import statements(声明非默认的类型和导入语句)public interface IMyAidlInterface extends android.os.IInterface { /* * Local-side IPC implementation stub class. / public static abstract class Stub extends android.os.Binder implements com.sjr.ipcdemo.IMyAidlInterface { /* * BInder的唯一标识,一般用当前Binder的类名来表示 / private static final java.lang.String DESCRIPTOR = "com.sjr.ipcdemo.IMyAidlInterface"; /* * Construct the stub at attach it to the interface. / public Stub() { this.attachInterface(this, DESCRIPTOR); } /* * Cast an IBinder object into an com.sjr.ipcdemo.IMyAidlInterface interface, * generating a proxy if needed. * 将被请求共享进程的Binder对象转换成请求共享进程所需的AIDL接口类型对象 * (区分进程,如果在同一进程返回的是被请求进程的Stub本身,否则返回的是系统封装之后的Stub.proxy对象) / public static com.sjr.ipcdemo.IMyAidlInterface asInterface(android.os.IBinder obj) { if ((obj == null)) { return null; } android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof com.sjr.ipcdemo.IMyAidlInterface))) { return ((com.sjr.ipcdemo.IMyAidlInterface) iin); } return new com.sjr.ipcdemo.IMyAidlInterface.Stub.Proxy(obj); } /* * 返回当前的Binder对象 * @return / @Override public android.os.IBinder asBinder() { return this; } /* * 运行在Service中的Binder线程池中,当有跨进程请求时,远程请求会通过系统底层封装后交由此方法处理 * @param code 请求码 * @param data 数据 * @param reply 写入返回值 * @param flags 标示 * @return 返回false的共享请求会失败 * @throws android.os.RemoteException / @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_show: { data.enforceInterface(DESCRIPTOR); this.show(); reply.writeNoException(); return true; } } return super.onTransact(code, data, reply, flags); } /* * 要请求共享的进程请求调用时执行 / private static class Proxy implements com.sjr.ipcdemo.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 show() 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_show, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); } } } static final int TRANSACTION_show = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); } /* * Demonstrates some basic types that you can use as parameters * and return values in AIDL.\ * 方法名跟调用的方法名一样 */ public void show() throws android.os.RemoteException;}
然后是Service的方法,然后更换为其他进程,这里给Service加了个action,是考虑到如果是不同应用时Intent跳转不能用this,XXX.class,只能根据action来跳,单个应用可用不用设置action,一样是可行的。

<service android:name=".TestAIDLService" android:process=":hello"> <intent-filter> <action android:name="com.sjr.ipcdemo.hello" /> </intent-filter> </service>

Service代码为:

public class TestAIDLService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new Intermediary(); } /** * 用一个中介类去继承自己写好的接口 / class Intermediary extends IMyAidlInterface.Stub { /* * 然后在这个回调方法里调用Service方法 * * @throws RemoteException */ @Override public void show() throws RemoteException { TestAIDLService.this.show(); } } public void show() { Log.d("print", "哈喽这是Service的方法"); }}
然后就是在MainActivity里调用了,代码为:

public class MainActivity extends Activity { MyServiceConnection con; IMyAidlInterface mAidl; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); con = new MyServiceConnection(); //5.0以后Service必须显示启动 // 否则异常 java.lang.IllegalArgumentException: Service Intent must be explicit: Intent { act=com.sjr.ipcdemo.hello } Intent intent = new Intent("com.sjr.ipcdemo.hello").setPackage("com.sjr.ipcdemo"); bindService(intent, con, BIND_AUTO_CREATE); Button btn1 = (Button) findViewById(R.id.btn1); btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { mAidl.show(); } catch (RemoteException e) { e.printStackTrace(); } } }); } class MyServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { mAidl = IMyAidlInterface.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { } } @Override protected void onDestroy() { super.onDestroy(); unbindService(con); }}
当我点击按钮就调用Service的方法打出Log,Log为:


这样就完成了一次使用AIDL进行简单的跨进程通信了,有点需要注意的是在5.0以后Service必须是显示启动。

(5).使用ContentProvider(内容提供者)

ContentProvider是Android四大组件之一,也是Android提供的专门用于不同应用数据共享的方式,所以ContentProvider也是跨进程通信的方式之一,ContentProvider底层也是Binder,也同样是因为它是Android系统提供的共享数据方式之一,所以Android对ContentProvider进行了良好的封装,我们不用关心底层实现就可以实现跨进程通信,系统也提供了很多ContentProvider,比如通讯录、短信等,要操作这些数据使用的是ContentResolver。下面用一个示例来演示ContentProvider实现多进程通信,例子比较简单,就是利用ContentProvider给一个数据库插入一条语句,然后提供给其他进程的Activity操作
首先是先创建数据库:

public class MyOpenHelper extends SQLiteOpenHelper { public MyOpenHelper(Context context) { super(context, "people.db", null, 1); } /** * 创建数据库表 * * @param db */ @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table person(_id integer primary key, name char(10), age integer(20))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { }}
接着是创建ContentProvider类:

public class PersonProvider extends ContentProvider { private MyOpenHelper mHelper; private SQLiteDatabase mDatabase; //创建uri匹配器对象 private static UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH); //匹配uri static { mMatcher.addURI("com.sjr.ipcdmo.people", "person", 1); } @Override public boolean onCreate() { mHelper = new MyOpenHelper(getContext()); mDatabase = mHelper.getWritableDatabase();//获取一个可写的数据库 //开启新线程插入一条数据 new Thread(new Runnable() { @Override public void run() { mDatabase.execSQL("delete from person"); mDatabase.execSQL("insert into person values(1,'李子明',2);"); } }).start(); return false; } /** * 查询 * @param uri * @param projection * @param selection * @param selectionArgs * @param sortOrder * @return / @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Cursor cursor = null; if (mMatcher.match(uri) == 1) cursor = mDatabase.query("person", projection, selection, selectionArgs, null, null, sortOrder, null); else throw new IllegalArgumentException("uri不对"); return cursor; } @Nullable @Override public String getType(Uri uri) { if (mMatcher.match(uri) == 1) return "van.android.cursor.dir/person"; return null; } /* * 其他应用调用,用于忘数据库里插入数据 * * @param uri 内容提供者的地址 * @param values 由其他应用传入,用于封装要插入的数据 * @return / @Nullable @Override public Uri insert(Uri uri, ContentValues values) { if (mMatcher.match(uri) == 1) { mDatabase.insert("person", null, values); //发送数据改变通知,所有注册在这个uri的内容观察者都可以收到这个通知 getContext().getContentResolver().notifyChange(uri, null); } else throw new IllegalArgumentException("uri不对"); return uri; } /* * 删除 * @param uri * @param selection * @param selectionArgs * @return / @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } /* * 更新 * @param uri * @param values * @param selection * @param selectionArgs * @return */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; }}
同时在清单文件注册Provider和开启新进程:

<provider android:name=".provider.PersonProvider" android:authorities="com.sjr.ipcdmo.people" android:process=":provider" />
然后在Activity中使用ContentResolver来对数据库操作:

public class ContentProviderActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //在Activity中也插入一条数据,这时应该有两条了 ContentResolver resolver = getContentResolver(); ContentValues values = new ContentValues(); values.put("_id", 2); values.put("name", "李大哥"); values.put("age", 5); resolver.insert(Uri.parse("content://com.sjr.ipcdmo.people/person"), values); //查询所有 Cursor query = resolver.query(Uri.parse("content://com.sjr.ipcdmo.people/person"), new String[]{"name", "age"}, null, null, null); while (query.moveToNext()){ Log.d("print","姓名:"+query.getString(0)+"年龄:"+query.getString(1)); } }}
这时运行Log为:


上面就是一个使用ContentProvider进行跨进程通信的一个小示例;有一个注意的是Provider的query、update、insert、delete存在多线程并发访问时方法内部要做好线程同步。
(6).使用Socket
Socket也叫"套接字",是网络中的概念,下面用Socket来演示一个跨进程通信的示例:
首先是服务端:

public class TCPServerService extends Service { private boolean mIsServiceDestoryed = false; private String[] mMessages = new String[]{ "哈喽", "嗨", "你好", "吃饭了吗" }; @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { new Thread(new Runnable() { @Override public void run() { ServerSocket socket = null; try { socket = new ServerSocket(8068); } catch (IOException e) { e.printStackTrace(); return; } while (!mIsServiceDestoryed) { try { final Socket client = socket.accept();//接受客户端请求 Log.d("print","accept"); new Thread(new Runnable() { @Override public void run() { try { responseClient(client); } catch (IOException e) { e.printStackTrace(); } } }).start(); } catch (IOException e) { e.printStackTrace(); } } } }).start(); } @Override public void onDestroy() { super.onDestroy(); mIsServiceDestoryed = true; } 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("welcom"); while (!mIsServiceDestoryed) { String str = in.readLine(); Log.d("print","msg from client:" + str); if (str == null) { break; } int i = new Random().nextInt(mMessages.length); String msg = mMessages[i]; out.println(msg); Log.d("print","send :" + msg); } Log.d("print","client quit."); // 关闭流 if (out != null) out.close(); if (in != null) in.close(); client.close(); }}
当然服务端也要开启新进程:

<service android:name=".socket.TCPServerService" android:process=":service" />
以及使用Socket是需要如下权限的:

<pre name="code" class="html"><uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

接着是客户端Activity:

public class TCPClentActivity extends Activity implements View.OnClickListener { private static final int MESSAGE_RECEIVE_NEW_MSG = 1; private static final int MESSAGE_SOCKET_CONNECTED = 2; private Button mSendButton; private TextView mMessageTextView; private EditText mMessageEditText; private PrintWriter mPrintWriter; private Socket mClientSocket; @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_RECEIVE_NEW_MSG: { mMessageTextView.setText(mMessageTextView.getText() + (String) msg.obj); break; } case MESSAGE_SOCKET_CONNECTED: { mSendButton.setEnabled(true); break; } default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tcpclient); mMessageTextView = (TextView) findViewById(R.id.msg_container); mSendButton = (Button) findViewById(R.id.send); mSendButton.setOnClickListener(this); mMessageEditText = (EditText) findViewById(R.id.msg); Intent service = new Intent(this, TCPServerService.class); startService(service); new Thread() { @Override public void run() { connectTCPServer(); } }.start(); } @Override protected void onDestroy() { if (mClientSocket != null) { try { mClientSocket.shutdownInput(); mClientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } super.onDestroy(); } @Override public void onClick(View v) { if (v == mSendButton) { final String msg = mMessageEditText.getText().toString(); if (!TextUtils.isEmpty(msg) && mPrintWriter != null) { mPrintWriter.println(msg); mMessageEditText.setText(""); String time = formatDateTime(System.currentTimeMillis()); final String showedMsg = "self " + time + ":" + msg + "\n"; mMessageTextView.setText(mMessageTextView.getText() + showedMsg); } } } @SuppressLint("SimpleDateFormat") private String formatDateTime(long time) { return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time)); } private void connectTCPServer() { Socket socket = null; while (socket == null) { try { socket = new Socket("localhost", 8068); mClientSocket = socket; mPrintWriter = new PrintWriter(new BufferedWriter( new OutputStreamWriter(socket.getOutputStream())), true); mHandler.sendEmptyMessage(MESSAGE_SOCKET_CONNECTED); System.out.println("connect server success"); } catch (IOException e) { SystemClock.sleep(1000); Log.d("print","connect tcp server failed, retry..."); } } try { // 接收服务器端的消息 BufferedReader br = new BufferedReader(new InputStreamReader( socket.getInputStream())); while (!TCPClentActivity.this.isFinishing()) { String msg = br.readLine(); System.out.println("receive :" + msg); if (msg != null) { String time = formatDateTime(System.currentTimeMillis()); final String showedMsg = "server " + time + ":" + msg + "\n"; mHandler.obtainMessage(MESSAGE_RECEIVE_NEW_MSG, showedMsg) .sendToTarget(); } } System.out.println("quit..."); if (mPrintWriter!=null) mPrintWriter.close(); if (br!=null) br.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } }}

当Activity启动时会连接服务端Socket,因为主线程不能联网,所以新开一个线程去连接,然后为了确定连接成功,采用了超时重连策略,重试时间为1秒,重上面的两个类就可以完成一次使用Socket进行通信的示例了,效果为:



总结
跨进程通信就是上述几个了,他们的优缺点为:


源码地址:http://download.csdn.net/detail/lxzmmd/9527343

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

推荐阅读更多精彩内容