Android多进程间通信笔记

一. 什么是多进程?


多进程就是多个进程的意思,那么什么是进程呢?

当一个应用在开始运行时,系统会为它创建一个进程,一个应用默认只有一个进程,这个进程(主进程)的名称就是应用的包名。

进程的特点:

  • 进程是系统资源和分配的基本单位,而线程是调度的基本单位。

  • 每个进程都有自己独立的资源和内存空间

  • 其它进程不能任意访问当前进程的内存和资源

  • 系统给每个进程分配的内存会有限制

根据上边的引言和进程的特点可以看出,使用多进程的场景为:需要使apk所使用的内存限制扩大。

二. 进程的等级


按优先级可以分为五类,优先级从高到低排列:

image
  1. 前台进程:该进程包含正在与用户进行交互的界面组件,比如一个Activity。在接收关键生命周期方法时会让一个进程临时提升为前台进程,包括任何服务的生命周期方法onCreate()和onDestroy()和任何广播接收器onReceive()方法。这样做确保了这些组件的操作是有效的原子操作,每个组件都能执行完成而不被杀掉。

  2. 可见进程:该进程中的组件虽然没有和用户交互,但是仍然可以被看到。activity可见的时候不一定在前台。一个简单的例子是前台的 activity 使用对话框启动了一个新的 activity 或者一个透明 activity 。另一个例子是当调用运行时权限对话框时(事实上它就是一个 activity!)。

  3. 服务进程:该进程包含在执行后台操作的服务组件,比如播放音乐的Service。对于许多在后台做处理(如加载数据)而没有立即成为前台服务的应用都属于这种情况。
    请特别注意从onStartCommand()返回的常量,如果服务由于内存压力被杀掉,它表示控制什么发生什么:
    START_STICKY表示希望系统可用的时候自动重启服务,但不关心是否能获得最后一次的 Intent (例如,可以重建自己的状态或者控制自己的 start/stop 生命周期)。
    START_REDELIVER_INTENT是为那些在被杀死之后重启时重新获得 Intent 的服务的,直到用传递给 onStartCommand() 方法的 startId 参数调用stopSelf()为止。这里会使用 Intent 和 startId 作为队列完成工作。
    START_NOT_STICKY用于那些杀掉也没关系的服务。这适合那些管理周期性任务的服务,它们只是等待下一个时间窗口工作。

  4. 后台进程:该进程包含的组件没有与用户交互,用户也看不到 Service。在一般操作场景下,设备上的许多内存就是用在这上面的,使可以重新回到之前打开过的某个 activity 。

  5. 空进程:没有任何界面组件、服务组件,或触发器组件,只是出于缓存的目的而被保留(为了更加有效地使用内存而不是完全释放掉),只要 Android 需要可以随时杀掉它们。

三. 多进程的创建


Android多进程创建很简单,只需要在AndroidManifest.xml的声明四大组件的标签中增加”android:process”属性即可。命名之后,就成了一个单独的进程。

process分私有进程和全局进程:

  • 私有进程的名称前面有冒号,例如:

    <service android:name=".MusicService"   
             android:process=":musicservice"/>
    
  • 全局进程的名称前面没有冒号,例如:

    <service android:name=".MusicService"   
             android:process="com.trampcr.musicdemo.service"/>
    

为了节省系统内存,在退出该Activity的时候可以将其杀掉(如果没有人为杀掉该进程,在程序完全退出时该进程会被系统杀掉)。

多进程被创建好了,应用运行时就会对进程进行初始化,如果一个application中有多个进程,在进行全局初始化时,多进程会被初始化多次。

解决办法:判断当前进程,然后做相应的初始化操作。

四. 多进程间的通信IPC


IPC:InterProcess Communication,即进程间通信。

我们知道,同一个进程的多个线程是共享该进程的所有资源,但多个进程间内存是不可见的,也就是说多个进程间内存是不共享的。那么进程间是如何进行通信的呢?

Android中提供了三种方法:

  • 系统实现。

  • AIDL(Android Interface Definition Language,Android接口定义语言):大部分应用程序不应该使用AIDL去创建一个绑定服务,因为它需要多线程能力,并可能导致一个更复杂的实现。

  • Messenger:利用Handler实现。(适用于多进程、单线程,不需要考虑线程安全),其底层基于AIDL。

使用Messenger

如需让服务与远程进程通信,则可使用Messenger为服务提供接口。
定义一个MessengerService继承自Service,并在AndroidManifest.xml中声明并给一个进程名,使该服务成为一个单独的进程。代码如下:

MessengerService.java

public class MessengerService extends Service{

    class IncomingHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 0:
                    Toast.makeText(getApplicationContext(), "hello, trampcr", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    }

    Messenger mMessenger = new Messenger(new IncomingHandler());

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Toast.makeText(getApplicationContext(), "binding", Toast.LENGTH_SHORT).show();
        return mMessenger.getBinder();
    }
}

AndroidManifest.xml文件的配置如下:

<service android:name=".MessengerService"  
         android:process="com.trampcr.messenger.service"/>

MessengerActivity.java

public class MessengerActivity extends Activity{

    private boolean mBound;
    private Messenger mMessenger;
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mMessenger = new Messenger(service);
            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mMessenger = null;
            mBound = false;
        }
    };

    public void sayHello(View v){
        if(!mBound){
            return;
        }
        Message msg = Message.obtain(null, 0 , 0, 0);
        try {
            mMessenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(MessengerActivity.this, MessengerService.class);
        bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(mBound){
            unbindService(mServiceConnection);
            mBound = false;
        }
    }
}

通过以上代码,可以看到Messenger的使用方法:

  1. 服务实现一个Handler,由其接收来自客户端的每个调用的回调。

  2. Handler用于创建Messenger对象(对Handler的引用)。

  3. Messenger创建一个IBinder,服务通过onBind()使其返回客户端。

  4. 客户端使用IBinder将Messenger(引用服务的Handler)实例化,然后使用后者将Message对象发送给服务。

  5. 服务在其Handler中(具体地讲,是在handleMessage()方法中)接收每个Message。

这样,客户端并没有调用服务的“方法”。而客户端传递的“消息”(Message对象)是服务在其Handler中接收的。

以上代码实现的应用,刚打开会弹出一个binding,binding表示打开应用Activity就通过Messenger连接了一个服务进程,然后点击say hello会弹出hello,trampcr,这表示了Activity通过Messenger将Message发送给了服务进程。如下图:

image

使用AIDL

AIDL是一种接口描述语言,通常用于进程间通信。

使用AIDL的步骤:

  1. 创建AIDL,在main下新建一个文件夹aidl,然后在aidl下新建AIDL文件,这时系统会自动为该文件创建一个包名。
    aidl文件中会有一个默认的basicType方法,我们为它增加一个getName方法。代码如下:

    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);
    
     String getName(String nickName);
    }
    

    以上是我们自己创建的aidl文件,系统还会自动生成aidl代码,所在位置为:build/generated/source/aidl下debug和release,但是此时debug下没有任何东西,可以rebuild或运行一下程序,再次打开debug,发现生成了一个包和一个aidl文件。

  2. 在java下新建一个类AIDLService继承自Service。代码如下:

    public class AIDLService extends Service {
    
     IMyAidlInterface.Stub mStub = new IMyAidlInterface.Stub() {
         @Override
         public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
    
         }
    
         @Override
         public String getName(String nickName) throws RemoteException {
             return "aidl " + nickName;
         }
     };
    
     @Nullable
     @Override
     public IBinder onBind(Intent intent) {
         return mStub;
     }
    }
    
  3. 在AndroidManifest.xml中注册,并给一个进程名,是该服务成为一个独立的进程。

    <service android:name=".AIDLService"   
             android:process="com.aidl.test.service"/>
    
  4. 在MainActivity中进行与AIDLService之间的进程间通信。代码如下:

    public class MainActivity extends AppCompatActivity {
    
     private Button mBtnAidl;
     private IMyAidlInterface mIMyAidlInterface;
    
     ServiceConnection mServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             mIMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
         }
    
         @Override
         public void onServiceDisconnected(ComponentName name) {
    
         }
     };
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         mBtnAidl = (Button) findViewById(R.id.btn_aidl);
    
         bindService(new Intent(MainActivity.this, AIDLService.class), mServiceConnection, BIND_AUTO_CREATE);
    
         mBtnAidl.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 if(mIMyAidlInterface != null){
                     try {
                         String name = mIMyAidlInterface.getName("I'm nick");
                         Toast.makeText(MainActivity.this, "name = " + name, Toast.LENGTH_SHORT).show();
                     } catch (RemoteException e) {
                         e.printStackTrace();
                     }
                 }
             }
         });
     }
    }
    

在Activity中利用bindService与AIDLService进行连接,通过IMyAidlInterface实例与AIDLService进程进行通信,如下图所示:

image

五.序列化插件


Parcelable code generate:自动生成实现了Parcelable接口的对象。

六 .Android使用场景介绍

0.前言

在Android中,默认情况下,同一应用的所有组件均运行在同一进程中,且大多数应用都不会改变这一点。不过,单进程开发并不是Android应用的全部,今天我们就来说说Android中的多进程开发以及多进程的使用场景。

1.进程

我们都知道Android系统是基于Linux改造而来的,进程系统也是一脉相承,进程其实就是程序的具体实现。当程序第一次启动,Android会启动一个Linux进程(具体由Zygote fork出来)以及一个主线程,默认的情况下,所有组件都将运行在该进程内。同一个应用由系统分配一个独立的Linux账户,该应用产生的所有进程,都会是这同一个Linux账户。

2.使用多进程

在开发中,我们通常会使用修改清单文件的android:process来达到多进程的目的。如果android:process的value值以冒号开头的话,那么该进程就是私有进程,如果是以其他字符开头,那么就是公有进程,这样拥有相同ShareUID的不同应用可以跑在同一进程里。至于创建进程的具体源码分析,网上有一篇很详细的文章,在这就不重复造轮子了,有需要的朋友可以前往理解Android进程创建流程

还有一种方法开启进程,是通过JNI利用C/C++,调用fork()方法来生成子进程,一般开发者会利用这种方法来做一些daemon进程,来实现防杀保活等效果。

3.进程优先级

Android利用重要性层次结构,就是将最重要的保留,杀掉不重要的进程。

Android将重要性层次结构分为5个层级,具体可以查看Android开发——Android进程保活招式大全中1.1部分的内容,这里就不赘述了。

根据进程中当前活动组件的重要程度,Android会将进程评定为它可能达到的最高级别。例如,如果某进程托管着服务和可见Activity,则会将此进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对它的依赖而有所提高。例如进程A中的内容提供程序为进程B中的客户端提供服务,或者进程A中的服务绑定到进程B中的组件,则进程A始终被视为至少与进程B同样重要。

由于运行服务的进程其级别高于托管后台Activity的进程,因此启动长时间运行操作的Activity最好为该操作启动服务,而不是简单地创建工作线程。例如,正在将图片上传到网站的Activity应该启动服务来执行上传,这样一来,即使用户退出Activity仍可在后台继续执行上传操作。使用服务可以保证无论Activity发生什么情况该操作至少具备“服务进程”优先级。同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

4. Low Memory Killer

Android的Low Memory Killer基于Linux的OOM机制,Low Memory Killer会根据进程的adj级别以及所占的内存,来决定是否杀掉该进程,adj越大,占用内存越多,进程越容易被杀掉。

关于adj的分级,我们可以参考ProcessList.java,这里的常量定义了ADJ的分级。

UNKNOWN_ADJ = 16  
//级别最低级的进程,通常是被缓存的进程,但是系统也不清楚缓存的内容  

CACHED_APP_MAX_ADJ = 15  
//这是一个只托管不可见的活动的进程,因此可以在没有任何中断的情况下被杀死  

CACHED_APP_MIN_ADJ = 9  
//缓存进程,没有英文解释  

SERVICE_B_ADJ = 8  
//不活跃的服务,不像adj=5的服务那么活跃  
//在root以后,有的系统优化大师会把所有服务都调成adj=8来达到内存优化的目的  
//因为当所有人adj都比较高时,这样才能保证名正言顺的杀进程  

PREVIOUS_APP_ADJ = 7  
//被切换的进程,一般是用户前一个使用的进程。两个应用来回切换,那么前一个应用一般adj设置为7  

HOME_APP_ADJ = 6  
//与主应用程序有交互的进程  

SERVICE_ADJ = 5  
//活跃的服务进程  

HEAVY_WEIGHT_APP_ADJ = 4  
//高权重进程  

BACKUP_APP_ADJ = 3  
//正在备份的进程  

PERCEPTIBLE_APP_ADJ = 2  
//可感知进程,通常是前台Service进程  

VISIBLE_APP_ADJ = 1  
//可见进程  

FOREGROUND_APP_ADJ = 0  
//前台进程  

剩下的就是adj值为负数的进程,基本上都是系统集成,不在本文的讨论范围内。负数进程是不会被lmk杀掉的。

5.如何查看进程优先级和设备的内存临界值

首先通过adb shell ps指令查找对应进程的pid。

然后通过adb shell cat /proc/${pid}/oom_adj(设备需要root)返回对应进程的adj值。

我们可以通过adb shell cat查看下面两个文件,cat之前请先用chmod赋予权限,adj代表的是oom_score_adj的值,对应的minfree则代表内存临界值。

/sys/module/lowmemorykiller/parameters/adj

/sys/module/lowmemorykiller/parameters/minfree

比如我的测试机小米4C测试机对应的值就是:

adj: 0,58,117,176,529,1000

这个值其实是oom_score_adj的值,用这个值17再除1000四舍五入取整数,就是对应的adj的值,例如第二个值58即为5817/1000 = 1,对应的adj也就是1,1000默认就是15。所以这6个值对应的adj是0,1,2,3,9,15。

minfree: 18432,23040,27648,32256,56250,81250

这个值是页值,一页等于4KB,换算成MB大概是72,90,108,126,220,318

当可用内存小于318MB的时候,系统开始杀adj=15的进程,以此类推。

6.什么情况需要使用多进程

举个例子,现在要做一款音乐播放器,现在有以下几种方案:

A.在Activity中直接播放音乐。

B.启动后台Service,播放音乐。

C.启动前台Service,播放音乐。

D.在新的进程中,启动后台Service,播放音乐。

E.在新的进程中,启动前台Service,播放音乐。

A方案

我们的播放器是直接在activity中启动的。首先这么做肯定是不对的,我们需要在后台播放音乐,所以当activity退出后就播不了了,之所以给出这个例子是为了控制变量作对比。

音乐播放器无非是打开app,选歌,播放,退到桌面,切其他应用。我们选取了三个场景,打开、按home、按back退回桌面。让我们看一下A的相对应的oom_adj、oom_score、oom_score_adj的值。

image
image
image

从上述三个场景的结果来看,当我们应用在前台的时候,无论adj还是score还是score_adj,他们的值都非常的小,基本不会被LMK所杀掉,但是当我们按了Home之后,进程的adj就会急剧增大,变为7,相应的score和score_adj也会增大。在上篇文章中我们得知,adj=7即为被切换的进程,两个进程来回切换,上一个进程就会被设为7。当我们按Back键的时候,adj就会被设为9,也就是缓存进程,优先级比较低,有很大的几率被杀掉。

B方案

B直接启动一个后台service并播放音乐,让我们来看下B的对应的打开、按下Home切换、按下Back退出相应的adj、score、score_adj的值。

image
image
image

三种状态的adj、score_adj的值和A都是一样的,只有score有一点出入,其实分析源码得知,LMK杀进程的时候,score的影响其实并不大,所以我们暂时忽略它。所以adj和score_adj的值都相同却内存不足的情况下,这两个应用谁占得内存更大,谁就会被杀掉。不过鉴于A实在activity中播放音乐,所以B还是比A略好的方案。

这里有朋友肯定要问了,为什么切到后台后,adj的值是7而不是5,后台不是还有service在跑吗?

我们通过查看源码可以找出来,当切换Home的时候,会调用ActivityStack.java的finishCurrentActivityLocked函数,然后调用到了ActivityManagerService.java的computeOomAdjLocked函数,在这里对进程的ADJ值进行重新计算。当进程为PreviousProcess情况,则ADJ=7。具体的计算流程请看computeOomAdjLocked计算流程

if (app == mPreviousProcess && app.activities.size() > 0) {  
  if (adj > ProcessList.PREVIOUS_APP_ADJ) {  
      adj = ProcessList.PREVIOUS_APP_ADJ;  
      schedGroup = Process.THREAD_GROUP_BG_NONINTERACTIVE;  
      app.cached = false;  
      app.adjType = "previous";  
  }  
  if (procState > ActivityManager.PROCESS_STATE_LAST_ACTIVITY) {  
      procState = ActivityManager.PROCESS_STATE_LAST_ACTIVITY;  
  }  
}  

C方案

C的话是启动一个前台Service来播放音乐。让我们来看一下对应的值。

image
image
image

在前台的时候,和AB是一样的,adj都是0,当切到后台,或者back结束时,C对应的adj都是2,也就是可感知进程。adj=2可以说是很高优先级了。一般adj<5的应用不会被杀掉。因此总的来说,C方案相对于B来说更稳定,用户体验更好。不过有一点不足是必须启动一个前台service。不过现在大部分的音乐类软件都会提供一个前台service,也就不是什么缺点了。

D****方案

D把应用进行了拆分,把用于播放音乐的service放到了新的进程内,让我们看一下对应的值。

image
image
image

上面三张图对应的是D应用主进程的ADJ相关值,我们可以看出来,跟A类似,adj都是0,7,9。由于少了service部分,内存使用变少,最后计算出的oom_score_adj也更低了,意味着主进程部分也更不容易被杀死。下面我们看下拆分出的service的相关值。

image

因为是service进程,所以不受打开,关闭,切换所影响,我们可以看到service的adj值一直会是5,也就是活跃的服务进程,相比于B来说,优先级高了不少。不过对于C来说,其实这个方案反倒不如C的adj=2的前台进程更稳定。但是D可以自主释放主进程,使D实际所占用的内存很小,从而不容易被杀掉。C、D各有利弊。

E方案

E也是使用了多进程,并且在新进程中,使用了前台service,先来看下对应的值。

image
image
image

这个不多解释,和ABD基本差不多,都是0,7,9。我们看下拆分出来的进程的值。

image

我们可以看到,这个进程的值是2,像C方案,非常小,非常稳定,而且我们还可以在系统进入后台后,手动杀掉主进程,使整个应用的内存消耗降到最低。内存低,优先级又高,E获得了今天的“最稳定方案奖”。

7.使用多进程的其他场景补充

多进程还有一种非常有用的场景,就是多模块应用。比如我做的应用大而全,里面肯定会有很多模块,假如有地图模块、大图浏览、自定义WebView等等(这些都是吃内存大户),一个成熟的应用一定是多模块化的。首先多进程开发能为应用解决了OOM问题,因为Android对内存的限制是针对于进程的,所以,当我们需要加载大图之类的操作,可以在新的进程中去执行,避免主进程OOM。而且假如图片浏览进程打开了一个过大的图片,java heap申请内存失败,该进程崩溃并不影响我主进程的使用。


Android开发中怎样用多进程、用多进程的好处、多进程的缺陷、解决方法

1.怎样用多进程

Android多进程概念:一般情况下,一个应用程序就是一个进程,这个进程名称就是应用程序包名。我们知道进程是系统分配资源和调度的基本单位,所以每个进程都有自己独立的

资源和内存空间,别的进程是不能任意访问其他进程的内存和资源的。

如何让自己的应用拥有多个进程:

四大组件在AndroidManifest文件中注册的时候,有个属性android:process这里可以指定组件的所处的进程。

默认就是应用的主进程。指定为别的进程之后,系统在启动这个组件的时候,就先创建(如果还没创建的话)这个进程,然后再创建该组件。打印出它的进程名称:重

载Application类的onCreate方法即可。

设置android:process属性,要注意:如果是android:process=”:deamon”,以:开头的名字,表示这是一个应用程序的私有进程,否则它是一个全局进程。私有进程的进程名称是

会在冒号前自动加上包名,而全局进程则不会。一般我们都是有私有进程,很少使用全局进程。

2.用多进程的好处

好处:

(1)分担主进程的内存压力。

当应用越做越大,内存越来越多,将一些独立的组件放到不同的进程,它就不占用主进程的内存空间了。当然还有其他好处,有心人会发现

(2)使应用常驻后台,防止主进程被杀守护进程,守护进程和主进程之间相互监视,有一方被杀就重新启动它。

Android后台进程里有很多应用是多个进程的,因为它们要常驻后台,特别是即时通讯或者社交应用,不过现在多进程已经被用烂了。

典型用法是在启动一个不可见的轻量级私有进程,在后台收发消息,或者做一些耗时的事情,或者开机启动这个进程,然后做监听等。

坏处:消耗用户的电量。

多占用了系统的空间,若所有应用都这样占用,系统内存很容易占满而导致卡顿。

应用程序架构会变得复杂,因为要处理多进程之间的通信。这里又是另外一个问题了。

3.多进程的缺陷

进程间的内存空间是不可见的。开启多进程后,会引发以下问题:

1)Application的多次重建。

2)静态成员的失效。

3)文件共享问题。

4)断点调试问题。

4.解决方法

1)针对Application的多次重建:

在Application的onCreate中获取进程Id来判断不同进程,然后做不同的事情。

public class MyApplication extends Application {

@Override

public void onCreate() {

super.onCreate();

//获取进程Id

int pid = android.os.Process.myPid();

Log.e("m_tag", "MyApplication onCreate pid is " + pid); //根据进程id获取进程名称

String pName = getProcessName(this,pid);

if("com.xyy.processtest".equals(pName)){

//处理该进程的业务

}

}

}

public String getProcessName(Context cxt, int pid) {

ActivityManager am = (ActivityManager)

cxt.getSystemService(Context.ACTIVITY_SERVICE);

List runningApps = am.getRunningAppProcesses();

if (runningApps == null) {

return null;

}

for (RunningAppProcessInfo procInfo : runningApps) {

if (procInfo.pid == pid) {

return procInfo.processName;

}

}

return null;

}

2)针对静态成员的失效:

使用Intent或者aidl等进程通讯方式传递内容,不能用静态或单例模式。

3)针对文件共享问题:

多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。在多线程的情况下我们有锁机制控制资源的共享,但是在多进程中比较难,虽然有文件锁、排队等机制,但是在Android里很难实现。解决办法就是多进程的时候不并发访问同一个文件,比如子进程涉及到操作数据库,就可以考虑调用主进程进行数据库的操作。

4)针对断点调试问题:

调试就是跟踪程序运行过程中的堆栈信息,由于每个进程都有自己独立的内存空间和各自的堆栈,无法实现在不同的进程间调试。因此要改为同一进程:调试时去掉AndroidManifest.xml中android:process标签,这样保证调试状态下是在同一进程中,堆栈信息是连贯的。待调试完成后,再将标签复原。

参考:http://www.jianshu.com/p/ce1e35c84134
http://blog.csdn.net/seu_calvin/article/details/5393217
http://blog.csdn.net/SPENCER_HALE/article/details/54968092 http://blog.csdn.net/Simon_Crystin/article/details/70315106

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