LeakCanary的原理

为了使文章尽量通俗易懂。在探究LeakCanary之前,有必要补充些Java引用的知识。

软引用、弱引用、虚引用-他们的特点及应用场景

为什么会有这4种引用

Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。 这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。 很多系统的缓存功能都符合这样的应用场景。

说白了传统的两种应用没法描述对象生命周期中的多种状态,对象有哪些状态呢。

Java中,对象的生命周期包括以下几个阶段:

  • 1.创建阶段(Created)
  • 2.应用阶段(In Use)
  • 3.不可见阶段(Invisible)
  • 4.不可达阶段(Unreachable)
  • 5.收集阶段(Collected)
  • 6.终结阶段(Finalized)
  • 7.对象空间重分配阶段(De-allocated)

创建阶段(Created)
在创建阶段系统通过下面的几个步骤来完成对象的创建过程

为对象分配存储空间
开始构造对象
从超类到子类对static成员进行初始化
超类成员变量按顺序初始化,递归调用超类的构造方法
子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段

应用阶段(In Use)
对象至少被一个强引用持有着。

不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。
简单说就是程序的执行已经超出了该对象的作用域了。

不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为GC roo。存在着这些GC root会导致对象的内存泄露情况,无法被回收。

收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
这里要特别说明一下:不要重载finazlie()方法!原因有两点:

  • 会影响JVM的对象分配与回收速度
    在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC
  • 可能造成该对象的再次“复活”
    finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。

终结阶段
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。

对象空间重新分配阶段
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

哪4种,各有什么特点

  1. 强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfM moryError错误,使程序异常终止,也不会靠随意回收具有强引用 对象来解决内存不足的问题。

  1. 软引用

软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

/**
 * 软引用何时被收集
 * 运行参数 -Xmx200m -XX:+PrintGC
 * Created by ccr at 2018/7/14.
 */
public class SoftReferenceDemo {

    public static void main(String[] args) throws InterruptedException {
        //100M的缓存数据
        byte[] cacheData = new byte[100 * 1024 * 1024];
        //将缓存数据用软引用持有
        SoftReference<byte[]> cacheRef = new SoftReference<>(cacheData);
        //将缓存数据的强引用去除
        cacheData = null;
        System.out.println("第一次GC前" + cacheData);
        System.out.println("第一次GC前" + cacheRef.get());
        //进行一次GC后查看对象的回收情况
        System.gc();
        //等待GC
        Thread.sleep(500);
        System.out.println("第一次GC后" + cacheData);
        System.out.println("第一次GC后" + cacheRef.get());

        //在分配一个120M的对象,看看缓存对象的回收情况
        byte[] newData = new byte[120 * 1024 * 1024];
        System.out.println("分配后" + cacheData);
        System.out.println("分配后" + cacheRef.get());
    }

}

第一次GC前null
第一次GC前[B@7d4991ad
[GC (System.gc())  105728K->103248K(175104K), 0.0009623 secs]
[Full GC (System.gc())  103248K->103139K(175104K), 0.0049909 secs]
第一次GC后null
第一次GC后[B@7d4991ad
[GC (Allocation Failure)  103805K->103171K(175104K), 0.0027889 secs]
[GC (Allocation Failure)  103171K->103171K(175104K), 0.0016018 secs]
[Full GC (Allocation Failure)  103171K->103136K(175104K), 0.0089988 secs]
[GC (Allocation Failure)  103136K->103136K(199680K), 0.0009408 secs]
[Full GC (Allocation Failure)  103136K->719K(128512K), 0.0082685 secs]
分配后null
分配后null

从上面的示例中就能看出,软引用关联的对象不会被GC回收。JVM在分配空间时,若果Heap空间不足,就会进行相应的GC,但是这次GC并不会收集软引用关联的对象,但是在JVM发现就算进行了一次回收后还是不足(Allocation Failure),JVM会尝试第二次GC,回收软引用关联的对象。

像这种如果内存充足,GC时就保留,内存不够,GC再来收集的功能很适合用在缓存的引用场景中。在使用缓存时有一个原则,如果缓存中有就从缓存获取,如果没有就从数据库中获取,缓存的存在是为了加快计算速度,如果因为缓存导致了内存不足进而整个程序崩溃,那就得不偿失了。

  1. 弱引用

弱引用也是用来描述非必须对象的,他的强度比软引用更弱一些,被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。

/**
 * 弱引用关联对象何时被回收
 * Created by ccr at 2018/7/14.
 */
public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        //100M的缓存数据
        byte[] cacheData = new byte[100 * 1024 * 1024];
        //将缓存数据用软引用持有
        WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
        System.out.println("第一次GC前" + cacheData);
        System.out.println("第一次GC前" + cacheRef.get());
        //进行一次GC后查看对象的回收情况
        System.gc();
        //等待GC
        Thread.sleep(500);
        System.out.println("第一次GC后" + cacheData);
        System.out.println("第一次GC后" + cacheRef.get());

        //将缓存数据的强引用去除
        cacheData = null;
        System.gc();
        //等待GC
        Thread.sleep(500);
        System.out.println("第二次GC后" + cacheData);
        System.out.println("第二次GC后" + cacheRef.get());
    }
}
第一次GC前[B@7d4991ad
第一次GC前[B@7d4991ad
第一次GC后[B@7d4991ad
第一次GC后[B@7d4991ad
第二次GC后null
第二次GC后null

从上面的代码中可以看出,弱引用关联的对象是否回收取决于这个对象有没有其他强引用指向它。这个确实很难理解,既然弱引用关联对象的存活周期和强引用差不多,那直接用强引用好了,干嘛费用弄出个弱引用呢?其实弱引用存在必然有他的应用场景。

static Map<Object,Object> container = new HashMap<>();
public static void putToContainer(Object key,Object value){
    container.put(key,value);
}

public static void main(String[] args) {
    //某个类中有这样一段代码
    Object key = new Object();
    Object value = new Object();
    putToContainer(key,value);

    //..........
    /**
     * 若干调用层次后程序员发现这个key指向的对象没有用了,
     * 为了节省内存打算把这个对象抛弃,然而下面这个方式真的能把对象回收掉吗?
     * 由于container对象中包含了这个对象的引用,所以这个对象不能按照程序员的意向进行回收.
     * 并且由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。
     * 很可能会造成内存泄漏。
     */
    key = null;
}

下面一段话摘自《Java核心技术卷1》:

设计 WeakHashMap类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再 使用了, 将会出现什么情况呢? 假定对某个键的最后一次引用已经消亡,不再有任何途径引 用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键 / 值 对无法从映射中删除。为什么垃圾回收器不能够删除它呢? 难道删除无用的对象不是垃圾回 收器的工作吗?

遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象。只要映射对象是活动的, 其中的所有桶也是活动的, 它们不能被回收。因此,需要由程序负责从长期存活的映射表中 删除那些无用的值。 或者使用 WeakHashMap完成这件事情。当对键的唯一引用来自散列条
目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对。

下面是这种机制的内部运行情况。WeakHashMap 使用弱引用(weak references) 保存键。 WeakReference 对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的 对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象 已经没有他人引用了,就将其回收。然而, 如果某个对象只能由 WeakReference 引用, 垃圾 回收器仍然回收它,但要将引用这个对象的弱引用放人队列中。WeakHashMap将周期性地检 查队列, 以便找出新添加的弱引用。一个弱引用进人队列意味着这个键不再被他人使用, 并 且已经被收集起来。于是, WeakHashMap将删除对应的条目。

除了WeakHashMap使用了弱引用,ThreadLocal类中也是用了弱引用。

  1. 虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null,弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。

jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。

软引用、弱引用、虚引用的构造方法均可以传入一个ReferenceQueue与之关联。在引用所指的对象被回收后,引用(reference)本身将会被加入到ReferenceQueue之中,此时引用所引用的对象reference.get()已被回收 (reference此时不为nullreference.get()此时为null)。
所以,在一个非强引用所引用的对象回收时,如果引用reference没有被加入到被关联的ReferenceQueue中,则表示还有引用所引用的对象还没有被回收。如果判断一个对象的非强引用本该出现在ReferenceQueue中,实际上却没有出现,则表示该对象发送内存泄漏。

LeakCanary

理论依据

当一个ActivityonDestory方法被执行后,说明该Activity的生命周期已经走完,在下次GC发生时,该Activity对象应将被回收。

  • 通过上面对引用的学习,可以考虑在onDestory发生时创建一个弱引用指向Activity,并关联一个RefrenceQuence,当Activity被正常回收,弱引用实例本身应该出现在该RefrenceQuence中,否则便可以判断该Activity存在内存泄漏。
  • 通过Application.registerActivityLifecycleCallbacks()方法可以注册Activity生命周期的监听,每当一个Activity调用onDestroy进行页面销毁时,去获取到这个Activity的弱引用并关联一个ReferenceQuence,通过检测ReferenceQuence中是否存在该弱引用判断这个Activity对象是否正常回收。
  • onDestory被调用后,初步观察到Activity未被GC正常回收时,手动触发一次GC,由于手动发起GC请求后并不会立即执行垃圾回收,所以需要在一定时延后再二次确认Activity是否已经回收,如果再次判断Activity对象未被回收,则表示Activity存在内存泄漏。

源码解析

1.在导入依赖后使用如下方法便可以使用LeakCanary进行Activity内存泄漏分析:
MyApp.java
public class MyApp extends Application {
   @Override
   public void onCreate() {
       super.onCreate();
       //判断是否在主进程中
       if (LeakCanary.isInAnalyzerProcess(this)) {
           // This process is dedicated to LeakCanary for heap analysis.
           // You should not init your app in this process.
           return;
       }
       //使用LeakCanary
       LeakCanary.install(this);
   }
}

LeakCanary 2.0 的初始化放在了自带的ContentProvider中:ContentProvider的onCreate的调用时机介于Application的attachBaseContext和onCreate之间,LeakCanary 2.0LeakCanary的初始化放在了自带的ContentProvideronCreate函数中,将multiprocess设为false可以保证ContentProvider只初始化一次,LeakCanary也只初始化一次

2.进入LeakCanary#install(Application application)
LeakCanary.java
public final class LeakCanary {

 /**
  * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
  * references (on ICS+).
  */
 public static RefWatcher install(Application application) {
   return refWatcher(application)
       .listenerServiceClass(DisplayLeakService.class)//内存泄漏后用于显示的线上泄漏信息
       .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())//白名单
       .buildAndInstall();
 }

 /** Builder to create a customized {@link RefWatcher} with appropriate Android defaults. */
 public static AndroidRefWatcherBuilder refWatcher(Context context) {
   return new AndroidRefWatcherBuilder(context);
 }
}

install(Application application)内部,通过refWatcher创建了一个AndroidRefWatcherBuilder对象。
由命名可以看出这是个builder模式,在阅读这种设计模式的代码时,有一些小技巧:在build()方法调用前基本进行的就是一些变量赋值的操作,只需要留意build()返回的对象即可,所以这里重点关注buildAndInstall()的调用。

3 深入AndroidRefWatcherBuilder#buildAndInstall()
  • AndroidRefWatcherBuilder继承RefWatcherBuilder,在buildAndInstall()中创建一个RefWatcher(引用勘探者)实例,并使用静态方法ActivityRefWatcher#installOnIcsPlus()Application.ActivityLifecycleCallbacks对象注册到Application对象当中,每当有Activity调用onActivityDestroyed方法时,程序将回调RefWatcherwatch(Activity activity)方法。
  • 内存泄漏的判断、分析以及泄漏信息的显示均在RefWatcher#watch(Activity activity)完成。
AndroidRefWatcherBuilder.java
  /**
  * Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
  */
 public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();//构建引用勘探者
    ActivityRefWatcher.installOnIcsPlus((Application) context, refWatcher);
   }
   return refWatcher;
 }
 //: ActivityRefWatcher.java
@TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher {

 public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
   if (SDK_INT < ICE_CREAM_SANDWICH) {
     // If you need to support Android < ICS, override onDestroy() in your base activity.
     return;
   }
   ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
   //完成Activity生命周期的注册工作
   activityRefWatcher.watchActivities();
 }

 private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
     new Application.ActivityLifecycleCallbacks() {
       @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
       }

       @Override public void onActivityStarted(Activity activity) {
       }

       @Override public void onActivityResumed(Activity activity) {
       }

       @Override public void onActivityPaused(Activity activity) {
       }

       @Override public void onActivityStopped(Activity activity) {
       }

       @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
       }

       @Override public void onActivityDestroyed(Activity activity) {
         ActivityRefWatcher.this.onActivityDestroyed(activity);
       }
     };
       private final Application application;
       private final RefWatcher refWatcher;

       /**
        * Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking
        * after they have been destroyed.
        */
       public ActivityRefWatcher(Application application, final RefWatcher refWatcher) {
         this.application = checkNotNull(application, "application");
         this.refWatcher = checkNotNull(refWatcher, "refWatcher");
       }
       void onActivityDestroyed(Activity activity) {
         refWatcher.watch(activity);
        } 
       
        public void watchActivities() {
        // Make sure you don't get installed twice.
        stopWatchingActivities();
        application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
        }

        public void stopWatchingActivities() {
         application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
        }
 }
4 RefWatcher#watch(Activity activity)分析

上文讲到内存泄漏的判断、分析以及泄漏信息的显示均在RefWatcher#watch(Activity activity)完成,因此不得不去此方法中一探究竟。

在正式开始源码之前有必要交代一下:

KeyedWeakReferenceWeakReference的一个子类,它的作用是持有一个Activity的弱引用并为每一个Activity实例绑定一个全局唯一的Key(具体采用的方法是调用UUID.randomUUID().toString();)ReferenceQueue是一个队列

public final class RefWatcher {

 public static final RefWatcher DISABLED = new RefWatcherBuilder<>().build();

 private final WatchExecutor watchExecutor;
 private final DebuggerControl debuggerControl;
 //GC触发器,手动发起GC
 private final GcTrigger gcTrigger;
 private final HeapDumper heapDumper;
 //与各个Activity关联的唯一UUID容器
 private final Set<String> retainedKeys;
 //引用队列,当Activity被正常回收时,该Activity的弱引用将被放入其中
 private final ReferenceQueue<Object> queue;
 private final HeapDump.Listener heapdumpListener;
 //白名单,白名单内的对象可以有效持有Activity,避开内存检测
 private final ExcludedRefs excludedRefs;

 RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger,
     HeapDumper heapDumper, HeapDump.Listener heapdumpListener, ExcludedRefs excludedRefs) {
   this.watchExecutor = checkNotNull(watchExecutor, "watchExecutor");
   this.debuggerControl = checkNotNull(debuggerControl, "debuggerControl");
   this.gcTrigger = checkNotNull(gcTrigger, "gcTrigger");
   this.heapDumper = checkNotNull(heapDumper, "heapDumper");
   this.heapdumpListener = checkNotNull(heapdumpListener, "heapdumpListener");
   this.excludedRefs = checkNotNull(excludedRefs, "excludedRefs");
   retainedKeys = new CopyOnWriteArraySet<>();
   queue = new ReferenceQueue<>();
 }

 /**
  * Identical to {@link #watch(Object, String)} with an empty string reference name.
  *
  * @see #watch(Object, String)
  */
 public void watch(Object watchedReference) {
   watch(watchedReference, "");
 }

 /**
  * Watches the provided references and checks if it can be GCed. This method is non blocking,
  * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
  * with.
  *
  * @param referenceName An logical identifier for the watched object.
  */
 public void watch(Object watchedReference, String referenceName) {
   if (this == DISABLED) {
     return;
   }
   checkNotNull(watchedReference, "watchedReference");
   checkNotNull(referenceName, "referenceName");
   final long watchStartNanoTime = System.nanoTime();
   String key = UUID.randomUUID().toString();
   retainedKeys.add(key);
   final KeyedWeakReference reference =
       new KeyedWeakReference(watchedReference, key, referenceName, queue);

   ensureGoneAsync(watchStartNanoTime, reference);
 }

 private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
   watchExecutor.execute(new Retryable() {
     @Override public Retryable.Result run() {
       return ensureGone(reference, watchStartNanoTime);
     }
   });
 }

 Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
   long gcStartNanoTime = System.nanoTime();
   long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//在retainedKeys中移除进入quene中的KeyedWeakReference所对应的UUID
   removeWeaklyReachableReferences();

   if (debuggerControl.isDebuggerAttached()) {
     // The debugger can create false leaks.
     return RETRY;
   }
   //若retainedKeys中没有对应的UUID
   if (gone(reference)) {
     return DONE;
   }
   //若retainedKeys中存在对应的UUID,发起手动GC,并睡眠100ms
   gcTrigger.runGc();
   //再次在retainedKeys中移除进入quene中的KeyedWeakReference所对应的UUID
   removeWeaklyReachableReferences();
   //若retainedKeys中仍然存在对应的UUID,则开始内存泄漏和展示信息的工作
   if (!gone(reference)) {
     long startDumpHeap = System.nanoTime();
     long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

     File heapDumpFile = heapDumper.dumpHeap();
     if (heapDumpFile == RETRY_LATER) {
       // Could not dump the heap.
       return RETRY;
     }
     long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
     //内存泄漏后的信息分享,最终在DisplayLeakService#onHeapAnalyzed中通过Notification显示处理
     heapdumpListener.analyze(
         new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
             gcDurationMs, heapDumpDurationMs));
   }
   return DONE;
 }
//弱引用对应的UUID是否已被移除
 private boolean gone(KeyedWeakReference reference) {
   return !retainedKeys.contains(reference.key);
 }

 private void removeWeaklyReachableReferences() {
   // WeakReferences are enqueued as soon as the object to which they >point to becomes weakly
   // reachable. This is before finalization or garbage collection has >actually happened.
   KeyedWeakReference ref;
   //从被回收的对象的弱引用队列中取出一个弱引用对象,并在retainedKeys中移除对应的UUID
   while ((ref = (KeyedWeakReference) queue.poll()) != null) {
     retainedKeys.remove(ref.key);
   }
 }
}

ActivityonDestory方法被调用后,LeakCanary将在RefWatcherretainedKeys加入一条全局唯一的UUID,同时创建一个该Activityd的弱引用对象KeyedWeakReference,并将UUID写入KeyedWeakReference实例中,同时KeyedWeakReference与引用队列queue进行关联,这样当Activity对象正常回收时,该弱引用对象将进入队列当中。
循环遍历获取queue队列中的KeyedWeakReference对象ref,将ref中的UUID取出,在retainedKeys中移除该UUID。如果遍历完成后retainedKeys中仍然存在该弱引用的UUID,则说明该Activity对象在onDestory调用后没有被正常回收。此时通过GcTrigger手动发起一次GC,再等待100ms,然后再次判断Activity是否被正常回收,如果没有被回收,则开始内存泄漏和展示信息的工作。

拓展

RefWatch可以用作监控任意的普通对象,如下:
demoInstacne需要被回收的位置监控其是否被正常回收。

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

推荐阅读更多精彩内容