leakCanary 分析

一、leakCanary概念了解

1、leakCanary工作流程

LeakCannary 的主要原理,其实很简单,大概可以分为以下几步:

  • (1) 监测Activity 的生命周期的 onDestroy() 的调用。
  • (2) 当某个 Activity 的 onDestroy() 调用后,便对这个 activity 创建一个带 ReferenceQueue 的弱引用,并且给这个弱引用创建了一个 key 保存在 Set集合 中。
  • (3) 如果这个 activity 可以被回收,那么弱引用就会被添加到 ReferenceQueue 中。
  • (4) 等待主线程进入 idle(即空闲)后,通过一次遍历,在 ReferenceQueue 中的弱引用所对应的 key 将从 retainedKeys 中移除,说明其没有内存泄漏。
  • (5) 如果 activity 没有被回收,先强制进行一次 gc,再来检查,如果 key 还存在 retainedKeys 中,说明 activity 不可回收,同时也说明了出现了内存泄漏。
  • (6) 发生内存泄露之后,dump内存快照,分析 hprof 文件,找到泄露路径(使用 haha 库分析),发送到通知栏

LeakCanary对于内存泄漏的检测非常有效,但也并不是所有的内存泄漏都能检测出来。

  • 1、无法检测出Service中的内存泄漏问题
  • 2、如果最底层的MainActivity一直未走onDestroy生命周期(它在Activity栈的最底层),无法检测出它的调用栈的内存泄漏。
2c2aff757b430b65601240a19d326e5.png

2、java中的4中引用类型

  • 强引用:不会被GC回收
  • 软引用:内存不足的时候会被GC回收
  • 弱引用:当下次GC的时候会回收
  • 虚引用:任何情况都可以回收

二、leakCarcry分析

在分析Leak Canary原理之前,我们先来简单了解WeakReference和ReferenceQueue的作用,为什么要了解这些知识呢?Leak Canary其实内部就是使用这个机制来监控对象是否被回收了,当然Leak Canary的监控仅仅针对Activity和Fragment,所以这块有引入了ActivityLifecycleCallBack,后面会说,这里的回收是指JVM在合适的时间触发GC,并将回收的WeakReference对象放入与之关联的ReferenceQueue中表示GC回收了该对象,Leak Canary通过上卖弄的检测返现有些对象的生命周期本该已经结束了,但是任然在占用内存,这时候就判定是已经泄露了,那么下一步就是开始解析析headump文件,分析引用链,至此就结束了,其中需要注意的是这是WeakReference.get方法获取到的对象是null,所以Leak Canary使用了继承WeakReference.类,并把传入的对象作为成员变量保存起来,这样当GC发生时虽然把WeakReference中引用的对象置为了null也不会把WeakReference中我们拓展的类的成员变量置为null,这样我们就可以做其他的操作,比如:Leak Canary中把WeakReference存放在Set集合中,在恰当的时候需要移除Set中的WeakReference的引用,这个机制Glide中的内存缓存 也是使用了该机制,关于WeakReference和ReferenceQueue机制就不多说网上有很多可以了解一下。

1、WeakReference和ReferenceQueue机制

/**
 * 监控对象被回收,因为如果被回收就会就如与之关联的队列中
 */
private void monitorClearedResources() {
    Log.e("tag", "start monitor");
    try {
        int n = 0;
        WeakReference k;
        while ((k = (WeakReference) referenceQueue.remove()) != null) {
            Log.e("tag", (++n) + "回收了:" + k + "   object: " + k.get());
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}


private ReferenceQueue<WeakRefrencesBean> referenceQueue = new ReferenceQueue<>();

class WeakRefrencesBean {
    private String name;

    public WeakRefrencesBean(String name) {
        this.name = name;
    }
}

new Thread() {
        @Override
        public void run() {
            monitorClearedResources();
        }
    }.start();

    new Thread() {
        @Override
        public void run() {
            while (true) {
                new WeakReference<WeakRefrencesBean>(new WeakRefrencesBean("aaaa"), referenceQueue);
            }
        }
    }.start();

输出的日志:

1回收了:java.lang.ref.WeakReference@21f8376e   object: null
2回收了:java.lang.ref.WeakReference@24a74e0f   object: null
3回收了:java.lang.ref.WeakReference@39efe9c   object: null
4回收了:java.lang.ref.WeakReference@4ee20a5   object: null
3回收了:java.lang.ref.WeakReference@bf45c7a   object: null
4回收了:java.lang.ref.WeakReference@b94bc2b   object: null
5回收了:java.lang.ref.WeakReference@33eb6888   object: null

上面是一个监控对象回收,因为如果对象被回收就把该对象加入如与之关联的队列中,接着开启线程制造触发GC,并开启线程监控对象回收,Leak Canary也是利用这个机制完成对一些对象本该生命周期已经结束,还常驻内存,就算触发GC也不会回收,Leak Canary就判断为泄漏,针对于内存泄漏,我们知道有些对象是不能被GC回收的,JVM虚拟机的回收就是可达性算法,就是从GC Root开始检测,如果不可达那么就会被第一次标志,再次GC就会被回收。

2、能够作为 GC Root的对象

  • 虚拟机栈,在大家眼里也叫作栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI引用的对象;

3、Leak Canary是如何判断Activity或Fragment的生命周期结束了呢?

  • Leak Canary是通过 Application的内部类ActivityLifecycleCallbacks检测Activity的生命周期是否结束了,如果回调了onActivityDestroyed方法,那么表示Activity的声明周期已经结束了,这时候就要执行GC检测了。
  • 对于Fragment是通过FragmentManager的内部接口FragmentLifecycleCallbacks检测Fragment的声明周期的类似ActivityLifecycleCallbacks接口。

4、开始Leak Canary源码解读

步骤无非就是:
1、安装,实际上就是做一些初始化的操作;
2、检测时机,比如:回调onActivityDestroyed方法开始检测;
3、UI的展示;

5、安装

Leak Canary的地方就是 LeakCanary.install(this)方法开始,代码如下:

一般我们使用Leak Canaryu都是在Application中调用:

public class ExampleApplication extends Application {
    @Override
    public void onCreate() {
      super.onCreate();
      setupLeakCanary();
    }

   protected void setupLeakCanary() {
     enabledStrictMode();
     if (LeakCanary.isInAnalyzerProcess(this)) {
         return;
      }
   LeakCanary.install(this);
  }
   ...
 }

在install方法之前有个判断,这个判断是用来判断是否是在LeakCanary的堆统计进程(HeapAnalyzerService),也就是我们不能在我们的App进程中初始化LeakCanary,代码如下:

/**
 * 当前进程是否是运行{@link HeapAnalyzerService}的进程中,这是一个与普通应用程序进程不同的进程。
 */
public static boolean isInAnalyzerProcess(@NonNull Context context) {
    Boolean isInAnalyzerProcess = LeakCanaryInternals.isInAnalyzerProcess;
    // 这里只需要为每个进程计算一次。
    if (isInAnalyzerProcess == null) {
        //把Context和HeapAnalyzerService服务作为参数传进isInServiceProcess方法中
        isInAnalyzerProcess = isInServiceProcess(context, HeapAnalyzerService.class);
        LeakCanaryInternals.isInAnalyzerProcess = isInAnalyzerProcess;
    }
    return isInAnalyzerProcess;
}

在isInAnalyzerProcess方法中有调用了isInServiceProcess方法,代码如下:

public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
    PackageManager packageManager = context.getPackageManager();
    PackageInfo packageInfo;
    try {
        packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
    } catch (Exception e) {
        CanaryLog.d(e, "Could not get package info for %s", context.getPackageName());
        return false;
    }
    //主进程
    String mainProcess = packageInfo.applicationInfo.processName;


    //构造进程
    ComponentName component = new ComponentName(context, serviceClass);
    ServiceInfo serviceInfo;
    try {
        serviceInfo = packageManager.getServiceInfo(component, PackageManager.GET_DISABLED_COMPONENTS);
    } catch (PackageManager.NameNotFoundException ignored) {
        // Service is disabled.
        return false;
    }

    //判断当前HeapAnalyzerService服务进程名和主进程名是否相等,如果相等直接返回false,因为LeakCanary不能再当前进程中运行
    if (serviceInfo.processName.equals(mainProcess)) {
        CanaryLog.d("Did not expect service %s to run in main process %s", serviceClass, mainProcess);
        // Technically we are in the service process, but we're not in the service dedicated process.
        return false;
    }

    int myPid = android.os.Process.myPid();
    ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.RunningAppProcessInfo myProcess = null;
    List<ActivityManager.RunningAppProcessInfo> runningProcesses;
    try {
        runningProcesses = activityManager.getRunningAppProcesses();
    } catch (SecurityException exception) {
        // https://github.com/square/leakcanary/issues/948
        CanaryLog.d("Could not get running app processes %d", exception);
        return false;
    }
    if (runningProcesses != null) {
        for (ActivityManager.RunningAppProcessInfo process : runningProcesses) {
            if (process.pid == myPid) {
                myProcess = process;
                break;
            }
        }
    }
    if (myProcess == null) {
        CanaryLog.d("Could not find running process for %d", myPid);
        return false;
    }

    return myProcess.processName.equals(serviceInfo.processName);
}

实际上LeakCanary最终会调用LeakCanaryInternals.isInServiceProcess方法,通过PackageManager、ActivityManager以及android.os.Process来判断当前进程是否为HeapAnalyzerService运行的进程,因为我们不能在我们的App进程中初始化LeakCanary。

接下来我们开始LeakCanary真正的实现,从LeakCanary.install(this)方法开始,代码如下:

public static RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
            .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
            .buildAndInstall();
}

实际上这一步返回的RefWatcher的实现类AndroidRefWatcher,主要是做些关乎初始化的操作,这些不展开讲,直接进入buildAndInstall()方法中,代码如下:

public RefWatcher buildAndInstall() {

    //install()方法只能一次调用,多次调用将抛出异常
    if (LeakCanaryInternals.installedRefWatcher != null) {
        throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
    }

    //初始化RefWatcher,这个东西是用来检查内存泄漏的,包括解析堆转储文件这些东西
    RefWatcher refWatcher = build();

    //如果RefWatcher还没有初始化,就会进入这个分支
    if (refWatcher != DISABLED) {
        if (enableDisplayLeakActivity) {
            //setEnabledAsync最终调用了packageManager.setComponentEnabledSetting,
            // 将Activity组件设置为可用,即在manifest中enable属性。
            // 也就是说,当我们运行LeakCanary.install(this)的时候,LeakCanary的icon才显示出来
            LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
        }


        //ActivityRefWatcher.install和FragmentRefWatcher.Helper.install的功能差不多,注册了生命周期监听。
        // 不同的是,前者用application监听Activity的生命周期,后者用Activity监听也就是Activity回调onActivityCreated方法,
        // 然后获取FragmentManager调用registerFragmentLifecycleCallbacks方法注册,监听fragment的生命周期,
        // 而且用到了leakcanary-support-fragment包,兼容了v4的fragment。
        if (watchActivities) {
            ActivityRefWatcher.install(context, refWatcher);
        }
        if (watchFragments) {
            FragmentRefWatcher.Helper.install(context, refWatcher);
        }
    }
    LeakCanaryInternals.installedRefWatcher = refWatcher;
    return refWatcher;
}

在buildAndInstall方法中有几点:

  • 首先会调用RefWatcherBuilder.build方法创建RefWatcher,RefWatcher是检测内存泄漏相关的;
  • 紧接着将Activity组件设置为可用,即在manifest中enable属性,也就是说,当我们运行LeakCanary.install(this)的时候,LeakCanary的icon才在桌面才会显示出来;
  • 然后就是ActivityRefWatcher.install和FragmentRefWatcher.Helper.install方法,注册了Activity和Fragment的生命周期监听,不同的是,前者用application监听Activity的生命周期,后者用Activity监听也就是Activity回调onActivityCreated方法,然后通过Activity获取FragmentManager调用并FragmentManager的registerFragmentLifecycleCallbacks方法注册监听fragment的生命周期,而且用到了leakcanary-support-fragment包,兼容了v4的fragment。

RefWatcher类是用来监控对象的引用是否可达,当引用变成不可达,那么就会触发堆转储(HeapDumper),来看看RefWatcherBuilder.build方法的具体实现,代码如下:

public final RefWatcher build() {

    //如果已经初始化了,直接返回RefWatcher.DISABLED表示已经初始化了
    if (isDisabled()) {
        return RefWatcher.DISABLED;
    }

    if (heapDumpBuilder.excludedRefs == null) {
        heapDumpBuilder.excludedRefs(defaultExcludedRefs());
    }

    HeapDump.Listener heapDumpListener = this.heapDumpListener;
    if (heapDumpListener == null) {
        heapDumpListener = defaultHeapDumpListener();
    }

    DebuggerControl debuggerControl = this.debuggerControl;
    if (debuggerControl == null) {
        debuggerControl = defaultDebuggerControl();
    }


    //创建堆转储对象
    HeapDumper heapDumper = this.heapDumper;
    if (heapDumper == null) {
        //返回的是HeapDumper.NONE,HeapDumper内部实现类,
        heapDumper = defaultHeapDumper();
    }

    //创建监控线程池
    WatchExecutor watchExecutor = this.watchExecutor;
    if (watchExecutor == null) {
        //默认返回 NONE
        watchExecutor = defaultWatchExecutor();
    }


    //默认的Gc触发器
    GcTrigger gcTrigger = this.gcTrigger;
    if (gcTrigger == null) {
        gcTrigger = defaultGcTrigger();
    }

    if (heapDumpBuilder.reachabilityInspectorClasses == null) {
        heapDumpBuilder.reachabilityInspectorClasses(defaultReachabilityInspectorClasses());
    }
    
    //创建把参数构造RefWatcher
    return new RefWatcher(watchExecutor, debuggerControl, gcTrigger, heapDumper, heapDumpListener,
            heapDumpBuilder);
}

如上代码知道,实际上是为了创建RefWatcher实例,和一些在检测中的环境初始化,比如线程池、GC触发器等等。

回到最初的biuldInstall方法中,知道监控Activity和Fragment是查不到的所以这里就只分析Activity相关的,也就是ActivityRefWatcher.install方法,代码如下:

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    //注册ActivityLifecycleCallbacks监听每一个Activity的生命周期
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}

可以知道这里是使用的装饰模式,使用ActivityRefWatcher对RefWatcher做了包装,接着注册ActivityLifecycleCallbacks监听每一个Activity的生命周期的onActivityDestroyed方法,这也就是检测泄漏开始的地方,而在onActivityDestroyed方法方法中会调用refWatcher.watch方法把activity作为参数传进去,代码如下:

 private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
    @Override
    public void onActivityDestroyed(Activity activity) {
        //当Activity被销毁了,那么应该检测是否内存泄漏
        refWatcher.watch(activity);
    }
};

可以看到在Activity销毁时会回调onActivityDestroyed方法,然后把该activity作为参数传递给refWatcher.watch(activity)方法,watch方法代码如下:

public void watch(Object watchedReference, String referenceName) {
   
    ..........

    final long watchStartNanoTime = System.nanoTime();
    //给该引用生成UUID
    String key = UUID.randomUUID().toString();
    //给该引用的UUID保存至Set中,强引用
    retainedKeys.add(key);
    //KeyedWeakReference 继承至WeakReference,由于KeyedWeakReference如果回收了,那么当中的对象通过get返回的是null,
    // 所以需要保存key和name作为标识,Glide也是此做法,KeyedWeakReference实现WeakReference
    final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

    //开始检测
    ensureGoneAsync(watchStartNanoTime, reference);
}

在watch方法中有如下几点:

  • 首先通过UUID生成表示该引用的Key,而这个Key会当做强引用保存到RefWatcher的成员变量Set集合中;
  • 接着创建KeyedWeakReference,而KeyedWeakReference 继承至WeakReference,由于KeyedWeakReference如果回收了,那么当中的对象通过get返回的是null,所以为了能在GC之后拿到Key,需要将保存key和name作为KeyedWeakReference中,Glide也是此做法;
  • 接着调用ensureGoneAsync(watchStartNanoTime, reference)方法开始检测是否有内存泄漏;

ensureGoneAsync方法代码如下:

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

在ensureGoneAsync方法中直接执行线程池(AndroidWatchExecutor),而这个线程池就是在刚开始的时候LeakCanary.install方法中创建RefWatcher的子类AndroidRefWatcher的时候创建的,接着看看ensureGone方法,代码如下:

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    //gc 开始的时间
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    //从Set中移除不能访问引用,意思就是GC之后该引用对象是否加入队列了,如果已经加入队列说明不会造成泄漏的风险
    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
        // The debugger can create false leaks.
        return RETRY;
    }
    if (gone(reference)) {
        return DONE;
    }

    //尝试GC
    gcTrigger.runGc();

    //从Set中移除不能访问引用,意思就是GC之后该引用对象是否加入队列了,如果已经加入队列说明不会造成泄漏的风险
    removeWeaklyReachableReferences();


    //到这一步说明该对象按理来说声明周期是已经结束了的,但是通过前面的GC却不能回收,说明已经造成了内存泄漏,那么解析hprof文件,得到该对象的引用链,也就是要触发堆转储
    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);

        HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
                .referenceName(reference.name)
                .watchDurationMs(watchDurationMs)
                .gcDurationMs(gcDurationMs)
                .heapDumpDurationMs(heapDumpDurationMs)
                .build();
        //开始解释堆转储文件
        heapdumpListener.analyze(heapDump);
    }
    return DONE;
}

在ensureGone方法中有如下几点:

  • 调用removeWeaklyReachableReferences方法,从Set中移除不能访问引用,意思就是GC之后该引用对象是否加入队列了,如果已经加入队列说明不会造成泄漏的风险,就将引用从Set集合中移除;
  • 紧接着调用gcTrigger.runGc方法尝试GC,看看能不能回收引用对象;
  • 再次调用removeWeaklyReachableReferences方法,从Set中移除不能访问引用,意思就是GC之后该引用对象是否加入队列了,如果已经加入队列说明不会造成泄漏的风险,也就是在手动触发GC之后,再次检测是否可以回收对象;
  • *最后通过gone(reference)方法检测Set集合中是否还存在该对象,如果存在说明已经泄漏了,就像前面说的,如果发生GC并且对象是可以被回收的,那么就会加入引用队列, 最后到这一步说明该对象按理来说声明周期是已经结束了的,但是通过前面的GC却不能回收,说明已经造成了内存泄漏,那么解析hprof文件,得到该对象的引用链,也就是要触发堆转储。

在前面说很多检测GC回收是怎么做到的呢,接下来看看removeWeaklyReachableReferences方法,代码如下:

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.
    //WeakReferences会在它们指向的对象变得无法访问时排队。 这是在完成或垃圾收集实际发生之前。
    //队列不为null,说明该对象被收了,加入此队列
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
        retainedKeys.remove(ref.key);
    }
}

这里直接使用一个while循环从队列取出元素进行判断,这里的queue.poll()是不会阻塞的,所以为什么LeakCanary会做两次验证的原因,为什么LeakCanary不使用queue.remove()方法呢?你想想queue.remove()方法是阻塞当前线程,从前面知道每次Activity或者Fragment销毁回调生命周期方法都会创建一个KeyedWeakReference实例,也就是说如果不泄露就一直阻塞当前线程,这样反而对造成不必要的开销,我也是猜猜而已。

6、总结

  • LeakCanary是通过WeakReference+Reference机制检测对象是否能被回收;
  • LeakCanary检测的时机是当某组件的生命周期已经结束,才会触发检测;

参考链接:https://www.jianshu.com/p/fa9d4eae7f05

所以说LeakCanary针对Activity/Fragment的内存泄漏检测非常好用,但是对于以上检测不到的情况,还得配合Android Monitor + MAT

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

推荐阅读更多精彩内容