什么情况下Activity会被杀掉呢?

首先一个报错来作为开篇:

Caused by androidx.fragment.app.Fragment$InstantiationException
Unable to instantiate fragment xxx: could not find Fragment constructor

这个报错原因就是Fragment如果重载了有参的构造方法,没有实现默认无参构造方法。Activity被回收又回来尝试重新恢复Fragment的时候报错的。
那如何模拟Activity被回收呢?
可能有人知道,一个方便快捷的方法就是:打开 开发者选项 - 不保留活动,这样每次Activity回到后台都会被回收,也就可以很方便的测试这种case。

但抛开这种方式我怎么来复现这种情况呢?
这里我提出一种方式:我是不是可以打开我的App,按Home回到后台,然后疯狂的打开手机里其他的大型应用或者游戏这类的能占用大量手机内存的App,等手机内存占用大的时候是不是可以复现这种情况呢?

结论是不可以,不要混淆两个概念,系统内存不足App内存不足,两者能引起的后果也是不同的

  • 系统内存不足 -> 杀掉应用进程
  • App内存不足 -> 杀掉后台Activity

首先明确一点,Android框架对进程创建与管理进行了封装,对于APP开发者只需知道Android四大组件的使用。当Activity, Service, ContentProvider, BroadcastReceiver任一组件启动时,当其所承载的进程存在则直接使用,不存在则由框架代码自动调用startProcessLocked创建进程。所以说对APP来说进程几乎是透明的,但了解进程对于深刻理解Android系统是至关关键的。

1. 系统内存不够 -> 杀掉应用进程

1.1. LKM简介

Android底层还是基于Linux,在Linux中低内存是会有oom killer去杀掉一些进程去释放内存,而Android中的lowmemorykiller就是在此基础上做了一些调整来的。因为手机上的内存毕竟比较有限,而Android中APP在不使用之后并不是马上被杀掉,虽然上层ActivityManagerService中也有很多关于进程的调度以及杀进程的手段,但是毕竟还需要考虑手机剩余内存的实际情况,lowmemorykiller的作用就是当内存比较紧张的时候去及时杀掉一些ActivityManagerService还没来得及杀掉但是对用户来说不那么重要的进程,回收一些内存,保证手机的正常运行。

lowmemkiller中会涉及到几个重要的概念:
/sys/module/lowmemorykiller/parameters/minfree:里面是以”,”分割的一组数,每个数字代表一个内存级别
/sys/module/lowmemorykiller/parameters/adj: 对应上面的一组数,每个数组代表一个进程优先级级别

比如:
/sys/module/lowmemorykiller/parameters/minfree:18432, 23040, 27648, 32256, 55296, 80640
/sys/module/lowmemorykiller/parameters/adj: 0, 100, 200, 300, 900, 906

代表的意思是两组数一一对应:

  • 当手机内存低于80640时,就去杀掉优先级906以及以上级别的进程
  • 当内存低于55296时,就去杀掉优先级900以及以上的进程

可能每个手机的配置是不一样的,可以查看一下手头的手机,需要root。

1.2. 如何查看ADJ

如何查看进程的ADJ呢?比如我们想看QQ的adj

-> adb shell ps | grep "qq" 
UID            PID  PPID C STIME TTY          TIME CMD
u0_a140       9456   959 2 10:03:07 ?     00:00:22 com.tencent.mobileqq
u0_a140       9987   959 1 10:03:13 ?     00:00:07 com.tencent.mobileqq:mini3
u0_a140      16347   959 0 01:32:48 ?     00:01:12 com.tencent.mobileqq:MSF
u0_a140      21475   959 0 19:47:33 ?     00:01:25 com.tencent.mobileqq:qzone

# 看到QQ的PID为 9456,这个时候打开QQ,让QQ来到前台
-> adb shell cat /proc/9456/oom_score_adj
0

# 随便打开一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
700

# 再随便打开另外一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
900

我们可以看到adj是在根据用户的行为不断变化的,前台的时候是0,到后台是700,回到后台后再打开其他App后是900
常见ADJ级别如下:

ADJ级别 取值 含义
NATIVE_ADJ -1000 native进程
SYSTEM_ADJ -900 仅指system_server进程
PERSISTENT_PROC_ADJ -800 系统persistent进程
PERSISTENT_SERVICE_ADJ -700 关联着系统或persistent进程
FOREGROUND_APP_ADJ 0 前台进程
VISIBLE_APP_ADJ 100 可见进程
PERCEPTIBLE_APP_ADJ 200 可感知进程,比如后台音乐播放
BACKUP_APP_ADJ 300 备份进程
HEAVY_WEIGHT_APP_ADJ 400 重量级进程
SERVICE_ADJ 500 服务进程
HOME_APP_ADJ 600 Home进程
PREVIOUS_APP_ADJ 700 上一个进程
SERVICE_B_ADJ 800 B List中的Service
CACHED_APP_MIN_ADJ 900 不可见进程的adj最小值
CACHED_APP_MAX_ADJ 906 不可见进程的adj最大值

So,当系统内存不足的时候会kill掉整个进程,皮之不存毛将焉附,Activity也就不在了,当然也不是开头说的那个case。

2. App内存不足 -> 杀掉后台Activity

上面分析了是直接kill掉进程的情况,一旦出现进程被kill掉,说明内存情况已经到了万劫不复的情况了,抛开内存泄漏的情况下,framework也需要一些策略来避免无内存可用的情况。下面我们来找一找fw里面回收Activity的逻辑(代码Base Android-30)。

Android Studio查看源码无法查看com.android.internal包名下的代码,双击Shift,勾选右上角Include non-prject Items.

入口定位到ActivityThreadattach方法,ActivityThread是App的入口程序,main方法中创建并调用atttach

// ActivityThread.java
    private void attach(boolean system, long startSeq) {
        ...
            // Watch for getting close to heap limit.
            BinderInternal.addGcWatcher(new Runnable() {
                @Override public void run() {
                    // mSomeActivitiesChanged在生命周期变化的时候会修改为true
                    if (!mSomeActivitiesChanged) {
                        return;
                    }
                    Runtime runtime = Runtime.getRuntime();
                    long dalvikMax = runtime.maxMemory();
                    long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
                    if (dalvikUsed > ((3*dalvikMax)/4)) {
                        mSomeActivitiesChanged = false;
                        try {
                            ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
                        } catch (RemoteException e) {
                            throw e.rethrowFromSystemServer();
                        }
                    }
                }
            });
        ...
    }

这里关注BinderInternal.addGcWatcher, 下面有几个点需要理清:

  1. addGcWatcher是干嘛的,这个Runnable什么时候会被执行。
  2. 这里的maxMemory() / totalMemory() / freeMemory()都怎么理解,值有什么意义
  3. releaseSomeActivities()做了什么事情,回收Activity的逻辑是什么。

还有一个小的点是这里还用了mSomeActivitiesChanged这个标记位来标记让检测工作不会过于频繁的执行,检测到需要releaseSomeActivities后会有一个mSomeActivitiesChanged = false;赋值。而所有的mSomeActivitiesChanged = true操作都在handleStartActivity/handleResumeActivity...等等这些操作Activity声明周期的地方。控制了只有Activity声明周期变化了之后才会继续去检测是否需要回收。

2.1. GcWatcher

BinderInternal.addGcWatcher是个静态方法,相关代码如下:

public class BinderInternal {
    private static final String TAG = "BinderInternal";
    static WeakReference<GcWatcher> sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
    static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
    static Runnable[] sTmpWatchers = new Runnable[1];

    static final class GcWatcher {
        @Override
        protected void finalize() throws Throwable {
            handleGc();
            sLastGcTime = SystemClock.uptimeMillis();
            synchronized (sGcWatchers) {
                sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
            }
            for (int i=0; i<sTmpWatchers.length; i++) {
                if (sTmpWatchers[i] != null) {
                    sTmpWatchers[i].run();
                }
            }
            sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
        }
    }

    public static void addGcWatcher(Runnable watcher) {
        synchronized (sGcWatchers) {
            sGcWatchers.add(watcher);
        }
    }
    ...
}

两个重要的角色:sGcWatcherssGcWatcher

  • sGcWatchers保存了调用BinderInternal.addGcWatcher后需要执行的Runnable(也就是检测是否需要kill Activity的Runnable)。
  • sGcWatcher是个装了new GcWatcher()的弱引用。

弱引用的规则是如果一个对象只有一个弱引用来引用它,那GC的时候就会回收这个对象。那很明显new出来的这个GcWatcher()只会有sGcWatcher这一个弱引用来引用它,所以每次GC都会回收这个GcWatcher对象,而回收的时候会调用这个对象的finalize()方法,finalize()方法中会将之前注册的Runnable来执行掉。
注意哈,这里并没有移除sGcWatcher中的Runnable,也就是一开始通过addGcWatcher(Runnable watcher)进来的runnable一直都在,不管执行多少次run的都是它。

为什么整个系统中addGcWatcher只有一个调用的地方,但是sGcWatchers确实一个List呢?我在自己写了这么一段代码并且想着怎么能反射搞到系统当前的BinderInternal一探究竟的时候明白了一点点,我觉着他们就是怕有人主动调用了addGcWatcher给弄了好多个GcWatcher导致系统的失效了才搞了个List吧。。

2.2. App可用的内存

上面的Runnable是如何检测当前的系统内存不足的呢?通过以下的代码

        Runtime runtime = Runtime.getRuntime();
        long dalvikMax = runtime.maxMemory();
        long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
        if (dalvikUsed > ((3*dalvikMax)/4)) { ... }

看变量名字就知道,在使用的内存到达总内存的3/4的时候去做一些事情,这几个方法的注释如下:

    /**
     * Returns the amount of free memory in the Java Virtual Machine. 
     * Calling the gc method may result in increasing the value returned by freeMemory.
     * @return  an approximation to the total amount of memory currently  available for future allocated objects, measured in bytes.
     */
    public native long freeMemory();

    /**
     * Returns the total amount of memory in the Java virtual machine.
     * The value returned by this method may vary over time, depending on the host environment.
     * @return  the total amount of memory currently available for current and future objects, measured in bytes.
     */
    public native long totalMemory();

    /**
     * Returns the maximum amount of memory that the Java virtual machine will attempt to use.   
     * If there is no inherent limit then the value java.lang.Long#MAX_VALUE will be returned.
     * @return  the maximum amount of memory that the virtual machine will attempt to use, measured in bytes
     */
    public native long maxMemory();

首先确认每个App到底有多少内存可以用,这些Runtime的值都是谁来控制的呢?
可以使用adb shell getprop | grep "dalvik.vm.heap"命令来查看手机给每个虚拟机进程所分配的堆配置信息:

yocn@yocn ~ % adb shell getprop | grep "dalvik.vm.heap"
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [512k]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [8m]
[dalvik.vm.heaptargetutilization]: [0.75]

这些值分别是什么意思呢?

  • [dalvik.vm.heapgrowthlimit]和[dalvik.vm.heapsize]都是当前应用进程可分配内存的最大限制,一般heapgrowthlimit < heapsize,如果在Manifest中的application标签中声明android:largeHeap=“true”,APP直到heapsize才OOM,否则达到heapgrowthlimit就OOM
  • [dalvik.vm.heapstartsize] Java堆的起始大小,指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小,后面再根据需要逐渐向系统申请更多的物理内存,直到达到MAX
  • [dalvik.vm.heapminfree] 堆最小空闲值,GC后
  • [dalvik.vm.heapmaxfree] 堆最大空闲值
  • [dalvik.vm.heaptargetutilization] 堆目标利用率

比较难理解的就是heapminfree、heapmaxfree和heaptargetutilization了,按照上面的方法来说:
在满足 heapminfree < freeMemory() < heapmaxfree的情况下使得(totalMemory() - freeMemory()) / totalMemory()接近heaptargetutilization

所以一开始的代码就是当前使用的内存到达分配的内存的3/4的时候会调用releaseSomeActivities去kill掉某些Activity.

2.3. releaseSomeActivities

releaseSomeActivities在API 29前后差别很大,我们来分别看一下。

2.3.1. 基于API 28的版本的releaseSomeActivities实现如下:
// step①:ActivityManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
    synchronized(this) {
        final long origId = Binder.clearCallingIdentity();
        try {
            ProcessRecord app = getRecordForAppLocked(appInt);
            mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }
}

// step②:ActivityStackSupervisor.java
void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
    TaskRecord firstTask = null;
    ArraySet<TaskRecord> tasks = null;
    for (int i = 0; i < app.activities.size(); i++) {
        ActivityRecord r = app.activities.get(i);
        // 如果当前有正在销毁状态的Activity,Do Nothing
        if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
            return;
        }
        // 只有Activity在可以销毁状态的时候才继续往下走
        if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
                || r.state == PAUSED || r.state == STOPPING) {
            continue;
        }
        if (r.task != null) {
            if (firstTask == null) {
                firstTask = r.task;
            } else if (firstTask != r.task) {
                // 2.1 只有存在两个以上的Task的时候才会到这里
                if (tasks == null) {
                    tasks = new ArraySet<>();
                    tasks.add(firstTask);
                }
                tasks.add(r.task);
            }
        }
    }
    // 2.2 只有存在两个以上的Task的时候才不为空
    if (tasks == null) {
        if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
        return;
    }
    // If we have activities in multiple tasks that are in a position to be destroyed,
    // let's iterate through the tasks and release the oldest one.
    // 2.3 遍历找到ActivityStack释放最旧的那个
    final int numDisplays = mActivityDisplays.size();
    for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
        final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
        // Step through all stacks starting from behind, to hit the oldest things first.
        // 从后面开始遍历,从最旧的开始匹配
        for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
            final ActivityStack stack = stacks.get(stackNdx);
            // Try to release activities in this stack; if we manage to, we are done.
            // 尝试在这个stack里面销毁这些Activities,如果成功就返回。
            if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
                return;
            }
        }
    }
}

上面代码都加了注释,我们来理一理重点需要关注的点。整个流程可以观察tasks的走向

  • 2.1 & 2.2: 第一次循环会给firstTask赋值,当firstTask != r.task的时候才会给tasks赋值,后续会继续对tasks操作。所以单栈的应用不会回收,如果tasks为null,就直接return了,什么都不做
  • 2.3: 这一大段的双重for循环其实都没有第一步遍历出来的tasks参与,真正释放Activity的操作在ActivityStack中,所以尝试找到这些tasks对应的ActivityStack,让ActivityStack去销毁tasks,直到成功销毁。

继续查看releaseSomeActivitiesLocked:

// step③ ActivityStack.java
final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks, String reason) {
    // Iterate over tasks starting at the back (oldest) first.
    int maxTasks = tasks.size() / 4;
    if (maxTasks < 1) {
        maxTasks = 1;
    }
    // 3.1 maxTasks至少为1,至少清理一个
    int numReleased = 0;
    for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
        final TaskRecord task = mTaskHistory.get(taskNdx);
        if (!tasks.contains(task)) {
            continue;
        }
        int curNum = 0;
        final ArrayList<ActivityRecord> activities = task.mActivities;
        for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
            final ActivityRecord activity = activities.get(actNdx);
            if (activity.app == app && activity.isDestroyable()) {
                destroyActivityLocked(activity, true, reason);
                if (activities.get(actNdx) != activity) {
                    // Was removed from list, back up so we don't miss the next one.
                    // 3.2 destroyActivityLocked后续会调用TaskRecord.removeActivity(),所以这里需要将index--
                    actNdx--;
                }
                curNum++;
            }
        }
        if (curNum > 0) {
            numReleased += curNum;
            // 移除一个,继续循环需要判断 maxTasks > 0
            maxTasks--;
            if (mTaskHistory.get(taskNdx) != task) {
                // The entire task got removed, back up so we don't miss the next one.
                // 3.3 如果整个task都被移除了,这里同样需要将获取Task的index--。移除操作在上面3.1的destroyActivityLocked,移除Activity过程中,如果task为空了,会将task移除
                taskNdx--;
            }
        }
    }
    return numReleased;
}
  • 3.1: ActivityStack利用maxTasks 保证,最多清理tasks.size() / 4,最少清理1个TaskRecord,同时,至少要保证保留一个前台可见TaskRecord,比如如果有两个TaskRecord,则清理先前的一个,保留前台显示的这个,如果三个,则还要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有则只清理一个,保留两个,如果没有,则继续清理次老的,保留一个前台展示的,如果有四个,类似,如果有5个,则至少两个清理。一般APP中,很少有超过两个TaskRecord的。

  • 3.2: 这里清理的逻辑很清楚,for循环,如果定位到了期望的activity就清理掉,但这里这个actNdx--是为什么呢?注释说activity从list中移除了,为了能继续往下走,需要index--,但在这个方法中并没有将activity从lsit中移除的操作,那肯定是在destroyActivityLocked方法中。继续追进去可以一直追到TaskRecord.java#removeActivity(),从当前的TaskRecord的mActivities中移除了,所以需要index--。

  • 3.3: 我们弄懂了上面的actNdx--之后也就知道这里为什么要index--了,在ActivityStack.java#removeActivityFromHistoryLocked()中有

    if (lastActivity) {
        removeTask(task, reason, REMOVE_TASK_MODE_DESTROYING);
    }

如果task中没有activity了,需要将这个task移除掉。

以上就是基于API 28的releaseSomeActivities分析。

2.3.2. 基于29+的版本的releaseSomeActivities实现如下:
// ActivityTaskManagerService.java
    @Override
    public void releaseSomeActivities(IApplicationThread appInt) {
        synchronized (mGlobalLock) {
            final long origId = Binder.clearCallingIdentity();
            try {
                final WindowProcessController app = getProcessController(appInt);
                app.releaseSomeActivities("low-mem");
            } finally {
                Binder.restoreCallingIdentity(origId);
            }
        }
    }

// WindowProcessController.java
    void releaseSomeActivities(String reason) {
        // Examine all activities currently running in the process. Candidate activities that can be destroyed.
        // 检查进程里所有的activity,看哪些可以被关掉
        ArrayList<ActivityRecord> candidates = null;
        if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Trying to release some activities in " + this);
        for (int i = 0; i < mActivities.size(); i++) {
            final ActivityRecord r = mActivities.get(i);
            // First, if we find an activity that is in the process of being destroyed,
            // then we just aren't going to do anything for now; we want things to settle
            // down before we try to prune more activities.
            // 首先,如果我们发现一个activity正在执行关闭中,在关掉这个activity之前什么都不做
            if (r.finishing || r.isState(DESTROYING, DESTROYED)) {
                if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Abort release; already destroying: " + r);
                return;
            }
            // Don't consider any activities that are currently not in a state where they can be destroyed.
            // 如果当前activity不在可关闭的state的时候,不做处理
            if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
                    || r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
                if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
                continue;
            }

            if (r.getParent() != null) {
                if (candidates == null) {
                    candidates = new ArrayList<>();
                }
                candidates.add(r);
            }
        }

        if (candidates != null) {
            // Sort based on z-order in hierarchy.
            candidates.sort(WindowContainer::compareTo);
            // Release some older activities
            int maxRelease = Math.max(candidates.size(), 1);
            do {
                final ActivityRecord r = candidates.remove(0);
                r.destroyImmediately(true /*removeFromApp*/, reason);
                --maxRelease;
            } while (maxRelease > 0);
        }
    }

新版本的releaseSomeActivities放到了ActivityTaskManagerService.java这个类中,这个类是API 29新添加的,承载部分AMS的工作。
相比API 28基于Task栈的回收Activity策略,新版本策略简单清晰, 也激进了很多。

遍历所有Activity,刨掉那些不在可销毁状态的Activity,按照Activity堆叠的顺序,也就是Z轴的顺序,从老到新销毁activity。

有兴趣的读者可以自行编写测试代码,分别在API 28和API 28+的手机上测试看一下回收策略是否跟上面分析的一致。
也可以参考我写的TestKillActivity,单栈和多栈的情况下在高于API 28和低于API 28的手机上的表现。

总结:

  1. 系统内存不足时LMK会根据内存配置项来kill掉进程释放内存
  2. kill时会按照进程的ADJ规则来kill
  3. App内存不足时由GcWatcher来决定回收Activity的时机
  4. 可以使用getprop命令来查看当前手机的JVM内存分配和OOM配置
  5. releaseSomeActivities在API 28和API 28+的差别很大,低版本会根据Task数量来决定清理哪个task的。高版本简单粗暴,遍历activity,按照z order排序,优先release掉更老的activity。

参考资料:
Android lowmemorykiller分析
解读Android进程优先级ADJ算法
https://www.jianshu.com/p/3233c33f6a79
https://juejin.cn/post/7063068797304832030
Android可见APP的不可见任务栈(TaskRecord)销毁分析

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

推荐阅读更多精彩内容