腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 StartupTracer

版本

v0.6.5

温馨提示

  1. 在读这篇文章之前墙裂建议先读腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析
  2. TracePlugin 是比较复杂的,很多东西文章中可能讲的不是很清楚,配合 推荐 Matrix 源码完整注释
    可能会有更好的效果

概述

本篇文章是 腾讯开源的 APM 框架 Matrix 系列文章的第五篇,将对matrix-trace-canary这个模块种的StartupTracer类进行解析。这个类主要监控并上报App 冷/暖启动时间,Activity启动时间。上一篇为腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 FrameTracer

写在前面

各个时间节点的定义

其实StartupTracer的类注释已将告诉我们每个时间是如何定义的,下面我们在用文字描述一下

 * firstMethod.i       LAUNCH_ACTIVITY   onWindowFocusChange   LAUNCH_ACTIVITY    onWindowFocusChange
 * ^                         ^                   ^                     ^                  ^
 * |                         |                   |                     |                  |
 * |---------app---------|---|---firstActivity---|---------...---------|---careActivity---|
 * |<--applicationCost-->|
 * |<--------------firstScreenCost-------------->|
 * |<---------------------------------------coldCost------------------------------------->|
 * .                         |<-----warmCost---->|
 *
  • applicationCost(Application的启动时间):第一次启动Activity或者Service或者广播的时间(这里没有内容提供者是因为内容提供者是在Application初始化完成之前,加载完毕的) 减去 Application开始启动时间
  • firstScreenCost(首屏启动时间):第一个Activity 可操作的时间(Activity获取焦点) 减去 Application开始启动时间
  • coldCost(冷启动时间):主Activity可操作的时间(Activity获取焦点) 减去 Application开始启动时间
  • warmCost(暖启动时间):最近一个Activity开始启动的时间 减去 这个Activity可操作的时间(Activity获取焦点)

原理简介

当 onActivityFocused 被回调时,进行各个时间点的计算,配合 AppMethodBeat 中记录的方法执行时间,通过一定的逻辑 筛选出 导致启动时间长的方法并上报。

1. StartupTracer.生命周期方法

首先我们先来看一下 的构造方法,onAlive(),onDead()这三个方法,如果要问为什么看了上一篇文章腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 FrameTracer你就知道了。

 public StartupTracer(TraceConfig config) {
        this.config = config;
        //是否可用
        this.isStartupEnable = config.isStartupEnable();
        //SplashActivities
        this.splashActivities = config.getSplashActivities();
        this.coldStartupThresholdMs = config.getColdStartupThresholdMs();
        this.warmStartupThresholdMs = config.getWarmStartupThresholdMs();
    }

    @Override
    protected void onAlive() {
        super.onAlive();
        MatrixLog.i(TAG, "[onAlive] isStartupEnable:%s", isStartupEnable);
        if (isStartupEnable) {
            //注册全局Activity生命周期监听 详见【1.1】
            Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
            //添加监听 可以感知 activity获得焦点 和 activity的生命周期 详见【1.2】
            AppMethodBeat.getInstance().addListener(this);
        }
    }

    @Override
    protected void onDead() {
        super.onDead();
        if (isStartupEnable) {
            //移除监听
            AppMethodBeat.getInstance().removeListener(this);
            Matrix.with().getApplication().unregisterActivityLifecycleCallbacks(this);
        }
    }

首先构造方法也是读取配置并记录起来,onAlive()方法注册了ActivityLifecycleCallbacksIAppMethodBeatListener两个监听,onDead()中对这两个监听进行了移除

1.1 ActivityLifecycleCallbacks相关方法

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        //activeActivityCount == 0 && coldCost > 0 说明曾经已经冷启动过,这是没有activity了,但是进程还在
        if (activeActivityCount == 0 && coldCost > 0) {
            //是否是暖启动
            isWarmStartUp = true;
        }
        activeActivityCount++;
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
        activeActivityCount--;
    }
    ....

ActivityLifecycleCallbacks 的相关方法中只做了一件事 就是判断当前是否是暖启动。

1.2 StartupTracer.onActivityFocused

因为StartupTracer注册了IAppMethodBeatListener监听,所以当有Activity获取焦点,就会回调onActivityFocused方法

 @Override
    public void onActivityFocused(String activity) {
        if (isColdStartup()) {//判断条件是 coldCost == 0 所以只会进来一次
            if (firstScreenCost == 0) {
                //首屏启动时间=当前时间点-APP启动时间点
                this.firstScreenCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
            }
            if (hasShowSplashActivity) {
                //冷启动耗时 = (MainActivity启动的时间)当前时间-蛋碎时间
                //类注释上画了,coldCost = 第二个activity onWindowFocusChange时的时间,
                coldCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
            } else {
                if (splashActivities.contains(activity)) {
                    hasShowSplashActivity = true;
                } else if (splashActivities.isEmpty()) {//未配置 splashActivities,冷启动时间 == 第一屏时间
                    MatrixLog.i(TAG, "default splash activity[%s]", activity);
                    coldCost = firstScreenCost;
                } else {
                    MatrixLog.w(TAG, "pass this activity[%s] at duration of start up! splashActivities=%s", activity, splashActivities);
                }
            }
            if (coldCost > 0) {
               
               //详见【1.3】 analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, coldCost, false);
            }

        } else if (isWarmStartUp()) {
            isWarmStartUp = false;
            //暖启动时间=当前时间- 最近一个activity被启动的时间
            long warmCost = uptimeMillis() - ActivityThreadHacker.getLastLaunchActivityTime();
            if (warmCost > 0) {
                analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, warmCost, true);
            }
        }

    }

1.3 StartupTracer.analyse

   /**
     * @param applicationCost: application启动用时
     * @param firstScreenCost: 首屏启动时间
     * @param allCost          :冷启动耗时 或者 暖启动耗时
     * @param isWarmStartUp    :是冷启动还是暖启动
     */
    private void analyse(long applicationCost, long firstScreenCost, long allCost, boolean isWarmStartUp) {
        MatrixLog.i(TAG, "[report] applicationCost:%s firstScreenCost:%s allCost:%s isWarmStartUp:%s", applicationCost, firstScreenCost, allCost, isWarmStartUp);
        long[] data = new long[0];
        if (!isWarmStartUp && allCost >= coldStartupThresholdMs) { //冷启动时间>阈值

            //获取 AppMethodBeat.sBuffer 中记录的数据 详见【1.4】
            data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sApplicationCreateBeginMethodIndex);
            //移除 sApplicationCreateBeginMethodIndex 节点
            ActivityThreadHacker.sApplicationCreateBeginMethodIndex.release();

        } else if (isWarmStartUp && allCost >= warmStartupThresholdMs) {//暖启动时间>阈值
            //详见【1.4】
            data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sLastLaunchActivityMethodIndex);
            //移除 sApplicationCreateBeginMethodIndex 节点
            ActivityThreadHacker.sLastLaunchActivityMethodIndex.release();
        }

        //执行 AnalyseTask 详见【1.5】
        MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(data, applicationCost, firstScreenCost, allCost, isWarmStartUp, ActivityThreadHacker.sApplicationCreateScene));

    }

1.4 AppMethodBeat.copyData()

   //获取从 startRecord 到结束的 所有 IndexRecord
    public long[] copyData(IndexRecord startRecord) {
        return copyData(startRecord, new IndexRecord(sIndex - 1));
    }

    private long[] copyData(IndexRecord startRecord, IndexRecord endRecord) {
        long current = System.currentTimeMillis();
        long[] data = new long[0];
        try {
            if (startRecord.isValid && endRecord.isValid) {
                int length;
                int start = Math.max(0, startRecord.index);
                int end = Math.max(0, endRecord.index);

                //计算出copy区域的长度和copy
                if (end > start) {//正常情况下 一次copy
                    length = end - start + 1;
                    data = new long[length];
                    System.arraycopy(sBuffer, start, data, 0, length);
                } else if (end < start) {// 两次copy(后半截+前半截)
                    length = 1 + end + (sBuffer.length - start);
                    data = new long[length];
                    System.arraycopy(sBuffer, start, data, 0, sBuffer.length - start);
                    System.arraycopy(sBuffer, 0, data, sBuffer.length - start, end + 1);
                }
                return data;
            }
            return data;
        } catch (OutOfMemoryError e) {//这里还捕获 OutOfMemoryError ,大厂程序员真的是细啊
            MatrixLog.e(TAG, e.toString());
            return data;
        } finally {
            MatrixLog.i(TAG, "[copyData] [%s:%s] length:%s cost:%sms", Math.max(0, startRecord.index), endRecord.index, data.length, System.currentTimeMillis() - current);
        }
    }

该方法只要是从AppMethodBeat.sBuffer中copy出目标数据段。

1.5 AnalyseTask.run

AnalyseTask是一个Runnable所以我们直接进入run()方法

 public void run() {
            LinkedList<MethodItem> stack = new LinkedList();
            if (data.length > 0) {
                //根据之前 data 查到的 methodId ,拿到对应插桩函数的执行时间、执行深度,将每个函数的信息封装成 MethodItem,然后存储到 stack 集合当中 详见【1.6】
                TraceDataUtils.structuredDataToStack(data, stack, false, -1);
                //根据规则 裁剪 stack 中的数据 详见【1.7】
                TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
                    @Override
                    public boolean isFilter(long during, int filterCount) {
                        //如果 耗时小于 预设值 则进行裁剪
                        return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
                    }

                    @Override
                    public int getFilterMaxCount() {
                        //最大方法裁剪数 60
                        return Constants.FILTER_STACK_MAX_COUNT;
                    }

                    @Override
                    public void fallback(List<MethodItem> stack, int size) {//降级策略
                        MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
                        //循环删除 多余的shuju8
                        Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
                        while (iterator.hasNext()) {
                            iterator.next();
                            iterator.remove();
                        }

                    }
                });
            }

            StringBuilder reportBuilder = new StringBuilder();
            StringBuilder logcatBuilder = new StringBuilder();
            //获取最大的启动时间
            long stackCost = Math.max(allCost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
            //查询出最耗时的 methodId 详见【1.8】
            String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);

            // 如果超过阈值 打印log
            if ((allCost > coldStartupThresholdMs && !isWarmStartUp)
                    || (allCost > warmStartupThresholdMs && isWarmStartUp)) {
                MatrixLog.w(TAG, "stackKey:%s \n%s", stackKey, logcatBuilder.toString());
            }

            // report 详见【1.9】
            report(applicationCost, firstScreenCost, reportBuilder, stackKey, stackCost, isWarmStartUp, scene);
        }

1.6 TraceDataUtils.structuredDataToStack

 public static void structuredDataToStack(long[] buffer, LinkedList<MethodItem> result, boolean isStrict, long endTime) {
        long lastInId = 0L;
        //记录调用栈深度
        int depth = 0;
        //是个链表
        LinkedList<Long> rawData = new LinkedList<>();
        boolean isBegin = !isStrict;

        for (long trueId : buffer) {
            if (0 == trueId) {
                continue;
            }
            //是严格模式
            if (isStrict) {
                if (isIn(trueId) && AppMethodBeat.METHOD_ID_DISPATCH == getMethodId(trueId)) {
                    isBegin = true;
                }

                if (!isBegin) {
                    MatrixLog.d(TAG, "never begin! pass this method[%s]", getMethodId(trueId));
                    continue;
                }

            }

            //如果是 i 方法记录的数据
            if (isIn(trueId)) {
                //获取methodId
                lastInId = getMethodId(trueId);
                if (lastInId == AppMethodBeat.METHOD_ID_DISPATCH) { //如果是 handler 的 dispatchMessage 方法 depth 置为0
                    depth = 0;
                }
                depth++;
                //加入到链表中
                rawData.push(trueId);
            } else {// 如果是 0 方法记录的数据
                //获取methodId
                int outMethodId = getMethodId(trueId);
                if (!rawData.isEmpty()) {
                    //拿到i 方法中记录的数据
                    long in = rawData.pop();
                    depth--;
                    int inMethodId;
                    LinkedList<Long> tmp = new LinkedList<>();
                    tmp.add(in);
                    //如果  inMethodId 不等于 outMethodId 调用深度建议
                    while ((inMethodId = getMethodId(in)) != outMethodId && !rawData.isEmpty()) {
                        MatrixLog.w(TAG, "pop inMethodId[%s] to continue match ouMethodId[%s]", inMethodId, outMethodId);
                        in = rawData.pop();
                        depth--;
                        tmp.add(in);
                    }

                    //如果是 handler的 dispatchMessage方法
                    if (inMethodId != outMethodId && inMethodId == AppMethodBeat.METHOD_ID_DISPATCH) {
                        MatrixLog.e(TAG, "inMethodId[%s] != outMethodId[%s] throw this outMethodId!", inMethodId, outMethodId);
                        rawData.addAll(tmp);
                        depth += rawData.size();
                        continue;
                    }

                    //获取到 方法执行完的时间
                    long outTime = getTime(trueId);
                    // 获取方法开始执行的时间
                    long inTime = getTime(in);
                    //该方法执行时间
                    long during = outTime - inTime;
                    if (during < 0) {
                        MatrixLog.e(TAG, "[structuredDataToStack] trace during invalid:%d", during);
                        rawData.clear();
                        result.clear();
                        return;
                    }
                    //创建一个 methodItem 并加入
                    MethodItem methodItem = new MethodItem(outMethodId, (int) during, depth);
                    addMethodItem(result, methodItem);
                } else {
                    MatrixLog.w(TAG, "[structuredDataToStack] method[%s] not found in! ", outMethodId);
                }
            }
        }

        while (!rawData.isEmpty() && isStrict) {
            long trueId = rawData.pop();
            int methodId = getMethodId(trueId);
            boolean isIn = isIn(trueId);
            long inTime = getTime(trueId) + AppMethodBeat.getDiffTime();
            MatrixLog.w(TAG, "[structuredDataToStack] has never out method[%s], isIn:%s, inTime:%s, endTime:%s,rawData size:%s",
                    methodId, isIn, inTime, endTime, rawData.size());
            if (!isIn) {
                MatrixLog.e(TAG, "[structuredDataToStack] why has out Method[%s]? is wrong! ", methodId);
                continue;
            }
            MethodItem methodItem = new MethodItem(methodId, (int) (endTime - inTime), rawData.size());
            addMethodItem(result, methodItem);
        }
        TreeNode root = new TreeNode(null, null);
        //将链表转为树 进行整理数据,root是根节点
        stackToTree(result, root);
        //清空 result
        result.clear();
        //将 整理过的 数据 保存到 result中
        treeToStack(root, result);
    }

这个方法主要是根据之前 data 查到的 methodId ,拿到对应插桩函数的执行时间、执行深度,将每个函数的信息封装成 MethodItem,然后存储到 stack 链表当中

1.7 TraceDataUtils.trimStack

 public static void trimStack(List<MethodItem> stack, int targetCount, IStructuredDataFilter filter) {
        if (0 > targetCount) {
            stack.clear();
            return;
        }

        int filterCount = 1;
        int curStackSize = stack.size();
        while (curStackSize > targetCount) {
            ListIterator<MethodItem> iterator = stack.listIterator(stack.size());
            while (iterator.hasPrevious()) {
                MethodItem item = iterator.previous();
                if (filter.isFilter(item.durTime, filterCount)) {//是否要过滤
                    iterator.remove();
                    curStackSize--;
                    if (curStackSize <= targetCount) {
                        return;
                    }
                }
            }
            curStackSize = stack.size();
            filterCount++;
            if (filter.getFilterMaxCount() < filterCount) {
                break;
            }
        }
        int size = stack.size();
        //如果 stack的 容量还是 大于 阈值,则使用降级策略
        if (size > targetCount) {
            filter.fallback(stack, size);
        }
    }

这个方法主要是 通过我们自定义的规则裁剪 stack 中的数据

1.7 TraceDataUtils.getTreeKey

 public static String getTreeKey(List<MethodItem> stack, long stackCost) {
        StringBuilder ss = new StringBuilder();
        long allLimit = (long) (stackCost * Constants.FILTER_STACK_KEY_ALL_PERCENT);

        LinkedList<MethodItem> sortList = new LinkedList<>();

        //过滤出主要耗时方法
        for (MethodItem item : stack) {
            if (item.durTime >= allLimit) {
                sortList.add(item);
            }
        }

        //排序
        Collections.sort(sortList, new Comparator<MethodItem>() {
            @Override
            public int compare(MethodItem o1, MethodItem o2) {
                return Integer.compare((o2.depth + 1) * o2.durTime, (o1.depth + 1) * o1.durTime);
            }
        });

        if (sortList.isEmpty() && !stack.isEmpty()) {//没有主要的耗时方法,就用第一个代替
            MethodItem root = stack.get(0);
            sortList.add(root);
        } else if (sortList.size() > 1 && sortList.peek().methodId == AppMethodBeat.METHOD_ID_DISPATCH) {//如果第一个是 handler.dipatchMessage 那就去掉
            sortList.removeFirst();
        }

        //拼接字符串
        for (MethodItem item : sortList) {
            ss.append(item.methodId + "|");
            break;
        }
        return ss.toString();
    }

这个方法主要是 获取耗时方法的 methodId拼接成的字符串

1.9 AnalyseTask.report

 /**
         * @param applicationCost:Application 启动时间
         * @param firstScreenCost:首屏启动时间
         * @param reportBuilder:需要上报的         method信息
         * @param stackKey                    :主要耗时方法id
         * @param allCost:                    冷启动耗时 或者 暖启动耗时
         * @param isWarmStartUp:是否是           暖启动
         * @param scene:app                   启动时的场景(可分为 activity ,service ,brodcast )
         */
        private void report(long applicationCost, long firstScreenCost, StringBuilder reportBuilder, String stackKey,
                            long allCost, boolean isWarmStartUp, int scene) {

            TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
            if (null == plugin) {
                return;
            }
            //上报正常启动信息
            try {
                JSONObject costObject = new JSONObject();
                //添加设备信息
                costObject = DeviceUtil.getDeviceInfo(costObject, Matrix.with().getApplication());
                //Application 启动时间
                costObject.put(SharePluginInfo.STAGE_APPLICATION_CREATE, applicationCost);
                //Application 启动场景
                costObject.put(SharePluginInfo.STAGE_APPLICATION_CREATE_SCENE, scene);
                //首屏启动时间
                costObject.put(SharePluginInfo.STAGE_FIRST_ACTIVITY_CREATE, firstScreenCost);
                //冷启动时间 或者 暖启动时间
                costObject.put(SharePluginInfo.STAGE_STARTUP_DURATION, allCost);
                //冷启动 or 暖启动
                costObject.put(SharePluginInfo.ISSUE_IS_WARM_START_UP, isWarmStartUp);
                Issue issue = new Issue();
                issue.setTag(SharePluginInfo.TAG_PLUGIN_STARTUP);
                issue.setContent(costObject);
                //上报
                plugin.onDetectIssue(issue);
            } catch (JSONException e) {
                MatrixLog.e(TAG, "[JSONException for StartUpReportTask error: %s", e);
            }


            //上报 启动速度超过预设阈值的信息
            if ((allCost > coldStartupThresholdMs && !isWarmStartUp)
                    || (allCost > warmStartupThresholdMs && isWarmStartUp)) {

                try {
                    JSONObject jsonObject = new JSONObject();
                    jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication());
                    jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.STARTUP);
                    jsonObject.put(SharePluginInfo.ISSUE_COST, allCost);
                    jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString());
                    jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey);
                    jsonObject.put(SharePluginInfo.ISSUE_SUB_TYPE, isWarmStartUp ? 2 : 1);
                    Issue issue = new Issue();
                    issue.setTag(SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
                    issue.setContent(jsonObject);
                    plugin.onDetectIssue(issue);

                } catch (JSONException e) {
                    MatrixLog.e(TAG, "[JSONException error: %s", e);
                }
            }
        }
    }

这个方法就是组建json然后进行上报的操作,在正常启动下会上报一组Tag为Trace_StartUp的json,如果启动时间超过了预设阈值的情况下还会上传一组Tag为Trace_EvilMethod的json

StartupTracer 上报数据解析

tag: Trace_EvilMethod or Trace_StartUp

application_create:(Application的启动时间)第一次启动Activity或者Service或者广播的时间(这里没有内容提供者是因为内容提供者是在Application初始化完成之前,加载完毕的) 减去 Application开始启动时间
application_create_scene:启动场景 Activity(159,100),Service(114),broadcastReceiver()113
first_activity_create:(首屏启动时间)第一个Activity 可操作的时间(Activity获取焦点) 减去 Application开始启动时间
startup_duration:启动时间可分为 :
    * (冷启动时间):主Activity可操作的时间(Activity获取焦点) 减去 Application开始启动时间
    * (暖启动时间):最近一个Activity开始启动的时间 减去 这个Activity可操作的时间(Activity获取焦点) 
is_warm_start_up:是否是暖启动

detail:固定为STARTUP
cost:总耗时同 startup_duration
stack:方法栈信息, 每个item之间用“\n”隔开,每个item的含义为,调用深度,methodId,调用次数,耗时
    * 比如:0,118,1,5 -> 调用深度为0,methodId=118,调用次数=1,耗时5ms
stackKey:主要耗时方法 的methodId
subType:2:暖启动,1:冷启动

系列文章

参考资料

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

推荐阅读更多精彩内容