18.优化 - matrix-battery-canary 分析2

  有上篇文章可知,配置的Task会在BatteryMonitorCore执行startstoponForeground会执行onTurnOnonTurnOffonForeground方法,有些Task还会执行onBackgroundCheck方法等。所以就从TaskonTurnOnonTurnOffonForegroundonBackgroundCheck等这些方法来看相应的监控。
  由配置Task的顺序来看

1.JiffiesMonitorFeature

该类主要由onForeground来驱动,主要是对app在前后台对线程及进程的监控记录和比较。

    private final ThreadWatchDog mFgThreadWatchDog = new ThreadWatchDog();
    private final ThreadWatchDog mBgThreadWatchDog = new ThreadWatchDog();    
    public void onForeground(boolean isForeground) {
        super.onForeground(isForeground);
        if (isForeground) {
            mFgThreadWatchDog.start();
            mBgThreadWatchDog.stop();
        } else {
            mBgThreadWatchDog.start();
            mFgThreadWatchDog.stop();
        }
    }
    public void watchBackThreadSate(boolean isForeground, int pid, int tid) {
        if (isForeground) {
            // 给线程添加需要观察的线程
            mFgThreadWatchDog.watch(pid, tid); 
        } else {
            mBgThreadWatchDog.watch(pid, tid);
        }
    }

    class ThreadWatchDog implements Runnable {
        private long duringMillis;
        // 观察的线程集合
        private final List<ProcessInfo.ThreadInfo> mWatchingThreads = new ArrayList<>();

        @Override
        public void run() {
            List<JiffiesSnapshot.ThreadJiffiesSnapshot> threadJiffiesList = new ArrayList<>();
            synchronized (mWatchingThreads) {
                for (ProcessInfo.ThreadInfo item : mWatchingThreads) {
                    // 解析 "/proc/" + pid + "/task/" + tid + "/stat" 里面的信息
                    // 拿到了 tid 线程的相关信息并封装到 ThreadJiffiesSnapshot 中,主要拿到了 tid 的名字、utime(用户态运行的时间)、
                    // stime(内核态运行的时间)、cutime (累计所有的waited-for进程曾经在用户态运行的时间)
                    // cstime (累计所有的waited-for进程曾经在内核态运行的时间)
                    JiffiesSnapshot.ThreadJiffiesSnapshot snapshot = JiffiesSnapshot.ThreadJiffiesSnapshot.parseThreadJiffies(item);
                    if (snapshot != null) {
                        snapshot.isNewAdded = false;
                        threadJiffiesList.add(snapshot);
                    }
                }
            }
            if (!threadJiffiesList.isEmpty()) {
                ListEntry<JiffiesSnapshot.ThreadJiffiesSnapshot> threadJiffiesListEntry = ListEntry.of(threadJiffiesList);// 包裹一层?
                mCore.getConfig().callback.onWatchingThreads(threadJiffiesListEntry); // 主要做了 print
            }

            // 下一次运行维度时机,stop 方法则是将该任务移除
            if (duringMillis <= 5 * 60 * 1000L) {
                mCore.getHandler().postDelayed(this, setNext(5 * 60 * 1000L));
            } else if (duringMillis <= 10 * 60 * 1000L) {
                mCore.getHandler().postDelayed(this, setNext(10 * 60 * 1000L));
            } else {
                // done
                synchronized (mWatchingThreads) {
                    mWatchingThreads.clear();
                }
            }
        }
        ...
        private long setNext(long millis) {
            duringMillis += millis;
            return millis;
        }
    }

此外还有一个函数需要注意

        // 二个进程取差值 里面相同的线程也取差值
        public Delta<JiffiesSnapshot> diff(JiffiesSnapshot bgn) {
            return new Delta<JiffiesSnapshot>(bgn, this) {// bgn end
                @Override
                protected JiffiesSnapshot computeDelta() {
                    JiffiesSnapshot delta = new JiffiesSnapshot();
                    delta.pid = end.pid;
                    delta.name = end.name;
                    // 进程或者线程里面的 totalJiffies = utime + stime + cutime + cstime 的差值
                    delta.totalJiffies = Differ.DigitDiffer.globalDiff(bgn.totalJiffies, end.totalJiffies);
                    // 线程数差值
                    delta.threadNum = Differ.DigitDiffer.globalDiff(bgn.threadNum, end.threadNum);
                    delta.threadEntries = ListEntry.ofEmpty();
                    
                    // 取二次进程快照中相同的线程做差值后并排序
                    if (end.threadEntries.getList().size() > 0) {
                        List<ThreadJiffiesSnapshot> deltaThreadEntries = new ArrayList<>();
                        for (ThreadJiffiesSnapshot endRecord : end.threadEntries.getList()) {
                            boolean isNewAdded = true;
                            long jiffiesConsumed = endRecord.value;
                            for (ThreadJiffiesSnapshot bgnRecord : bgn.threadEntries.getList()) {
                                if (bgnRecord.name.equals(endRecord.name) && bgnRecord.tid == endRecord.tid) {
                                    isNewAdded = false;
                                    jiffiesConsumed = Differ.DigitDiffer.globalDiff(bgnRecord, endRecord).value;
                                    break;
                                }
                            }
                            if (jiffiesConsumed > 0) {
                                ThreadJiffiesSnapshot deltaThreadJiffies = new ThreadJiffiesSnapshot(jiffiesConsumed);
                                deltaThreadJiffies.tid = endRecord.tid;
                                deltaThreadJiffies.name = endRecord.name;
                                deltaThreadJiffies.stat = endRecord.stat;
                                deltaThreadJiffies.isNewAdded = isNewAdded;
                                deltaThreadEntries.add(deltaThreadJiffies);
                            }
                        }
                        if (deltaThreadEntries.size() > 0) {
                            Collections.sort(deltaThreadEntries, new Comparator<ThreadJiffiesSnapshot>() {
                                @Override
                                public int compare(ThreadJiffiesSnapshot o1, ThreadJiffiesSnapshot o2) {
                                    long minus = o1.get() - o2.get();
                                    if (minus == 0) return 0;
                                    if (minus > 0) return -1;
                                    return 1;
                                }
                            });
                            delta.threadEntries = ListEntry.of(deltaThreadEntries);
                        }
                    }
                    return delta;
                }
            };
        }
  • 总结:
  1. 取进程或者线程信息可以使用 proc/pid/stat/proc/pid/task/tid/stat这二个命令。
  2. 进程或者线程中的 utime + stime + cutime + cstime 这几个值可以反应当前 cpu 状态,内核态时间过多可以表明不太正常等。
  3. 前后台监控的时机、间隔不一致

2.DeviceStatMonitorFeature

该类的启动是由onTurnOnonTurnOff控制

    @Override
    public void onTurnOn() {
        super.onTurnOn();
         // 设备是否在  充电中/满电 熄屏 省电模式
        int deviceStat = BatteryCanaryUtil.getDeviceStat(mCore.getContext());   
        @SuppressLint("VisibleForTests") TimeBreaker.Stamp firstStamp = new TimeBreaker.Stamp(String.valueOf(deviceStat));
        synchronized (TAG) {
            mStampList = new ArrayList<>();
            // 记录最近一次设备的状态
            mStampList.add(0, firstStamp);
        }

        mDevStatListener.setListener(new Consumer<Integer>() {
            @SuppressLint("VisibleForTests")
            @Override
            // 该回调由前篇中 BatteryMonitorCore 类的 start 方法中的 BatteryEventDelegate.getInstance().attach(this).startListening(); 来触发
// 前篇代码 BatteryMonitorCore start 方法代码
//            if (BatteryEventDelegate.isInit()) {
//                // 通过广播来监听系统的 屏幕状态(息屏、亮屏),电池状态(充电、不充电),发生状态更改时会通知观察者们
//                BatteryEventDelegate.getInstance().attach(this).startListening();
//            }
            public void accept(Integer integer) {
                BatteryCanaryUtil.getProxy().updateDevStat(integer); // 更新 设备是否在 充电中/满电 熄屏 省电模式
                synchronized (TAG) {
                    if (mStampList != Collections.EMPTY_LIST) {
                        MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertDevStat(integer));
                        mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(integer)));  //设备状态记录
                        checkOverHeat(); // 判断 mStampList 数量是否达到阈值,释放 mStampList 一部分空间
                    }
                }
            }
        });

        if (!mDevStatListener.isListening()) {
            mDevStatListener.startListen(mCore.getContext());
        }
    }

    @Override
    public void onTurnOff() {
        super.onTurnOff();
        // 移除监听
        mDevStatListener.stopListen();
    }

    // 判断是否 充电中/满电
    public static boolean isDeviceChargingV1(Context context) {
        try {
            Intent batIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
            if (batIntent == null) return false;
            int status = batIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);//电池状态
            return (status == BatteryManager.BATTERY_STATUS_CHARGING) || (status == BatteryManager.BATTERY_STATUS_FULL);//充电中 或者 满电
        } catch (Throwable ignored) {
            return false;
        }
    }

    // 判断是否是亮屏
    public static boolean isDeviceScreenOn(Context context) {
        try {
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            if (pm != null) {
                return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? pm.isInteractive() : pm.isScreenOn();
            }
        } catch (Exception ignored) {
        }
        return false;
    }

    // 判断是否是开启省电模式
    public static boolean isDeviceOnPowerSave(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            try {
                PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                if (pm != null) {
                    return pm.isPowerSaveMode();
                }
            } catch (Exception ignored) {
            }
        }
        return false;
    }

而最终mStampList记录的数据会在currentDevStatSnapshot该方法中被使用

    // 获取当前设备状态的快照
    public DevStatSnapshot currentDevStatSnapshot() {
        return currentDevStatSnapshot(0L);
    }

    public DevStatSnapshot currentDevStatSnapshot(long windowMillis) {
        try {
            // 返回 连接电源 断开电源 亮屏 熄屏 这几种状态的占比
            TimeBreaker.TimePortions timePortions = TimeBreaker.configurePortions(mStampList, windowMillis, 10L, new TimeBreaker.Stamp.Stamper() {
                @Override
                public TimeBreaker.Stamp stamp(String key) {
                    //设备是否在 充电 熄屏 省电模式
                    int devStat = BatteryCanaryUtil.getDeviceStat(mCore.getContext());
                    return new TimeBreaker.Stamp(String.valueOf(devStat));
                }
            });
            DevStatSnapshot snapshot = new DevStatSnapshot();
            snapshot.setValid(timePortions.isValid());
            snapshot.uptime = Snapshot.Entry.DigitEntry.of(timePortions.totalUptime);
            snapshot.chargingRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("1"));// 连接电源的百分比
            snapshot.unChargingRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("2"));// 断开电源的百分比
            snapshot.screenOff = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("3"));// 熄屏的百分比
            snapshot.lowEnergyRatio = Snapshot.Entry.DigitEntry.of((long) timePortions.getRatio("4"));// 低电量的百分比
            return snapshot;
        } catch (Throwable e) {
            MatrixLog.w(TAG, "configureSnapshot fail: " + e.getMessage());
            DevStatSnapshot snapshot = new DevStatSnapshot();
            snapshot.setValid(false);
            return snapshot;
        }
    }

此外该类还可以监控CPU的频率和电池的温度

    public CpuFreqSnapshot currentCpuFreq() {
        CpuFreqSnapshot snapshot = new CpuFreqSnapshot();
        try {
            snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(BatteryCanaryUtil.getCpuCurrentFreq());
        } catch (Throwable e) {
            MatrixLog.printErrStackTrace(TAG, e, "#currentCpuFreq error");
            snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(new int[]{});
        }
        return snapshot;
    }

    // 获得每隔 cpu 核心的频率
    public static int[] getCpuCurrentFreq() {
        int[] output = new int[getCpuCoreNum()];
        for (int i = 0; i < getCpuCoreNum(); i++) {
            output[i] = 0;
            String path = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq";
            String cat = cat(path);
            if (!TextUtils.isEmpty(cat)) {
                try {
                    //noinspection ConstantConditions
                    output[i] = Integer.parseInt(cat) / 1000;
                } catch (Exception ignored) {
                }
            }
        }
        return output;
    }

    // 获取 cpu 核心数
    public static int getCpuCoreNum() {
        try {
            // Get directory containing CPU info
            File dir = new File("/sys/devices/system/cpu/");
            // Filter to only list the devices we care about
            File[] files = dir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return Pattern.matches("cpu[0-9]+", pathname.getName());
                }
            });
            // Return the number of cores (virtual CPU devices)
            return files.length;
        } catch (Exception ignored) {
            // Default to return 1 core
            return 1;
        }
    }

    public BatteryTmpSnapshot currentBatteryTemperature(Context context) {
        BatteryTmpSnapshot snapshot = new BatteryTmpSnapshot();
        snapshot.temp = Snapshot.Entry.DigitEntry.of(mCore.getCurrentBatteryTemperature(context));
        return snapshot;
    }

    // 获取电池温度
    public static int getBatteryTemperature(Context context) {
        try {
            Intent batIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
            if (batIntent == null) return 0;
            return batIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
        } catch (Throwable ignored) {
            return 0;
        }
    }
  • 总结
  1. 获取电池温度:
    Intent batIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    if (batIntent == null) return 0;
    return batIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
  2. 获取 CPU 核心数:"/sys/devices/system/cpu/"
  3. 获取单核 CPU 的频率:"/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"
  4. 获取设备状态:注册接收其接收系统发出的广播或者获取相应的系统服务来获取,代码见上方

3.AppStatMonitorFeature

在activity onCreate和 stop 时收集 app 是否在前台 有前台服务 在后台信息并记录,并且在 app 进入后台后(过指定时间后)根据判断 app 的前台服务的 importance 值来输出相应的信息。

    @Override
    public void onTurnOn() {
        super.onTurnOn();
        TimeBreaker.Stamp firstStamp = new TimeBreaker.Stamp("1");
        TimeBreaker.Stamp firstSceneStamp = new TimeBreaker.Stamp(mCore.getScene());  // mCore.getScene() = Current AppScene
        synchronized (TAG) {
            mStampList = new ArrayList<>();
            mStampList.add(0, firstStamp);
            mSceneStampList = new ArrayList<>();
            mSceneStampList.add(0, firstSceneStamp);
        }
    }

    @Override
    public void onTurnOff() {
        super.onTurnOff();
        synchronized (TAG) {
            mStampList.clear();
            mSceneStampList.clear();
        }
    }

    @Override
    // activity onCreate(isForeground = true) 和 stop(isForeground = false) 时回调
    public void onForeground(boolean isForeground) {
        super.onForeground(isForeground);
        // 判断 app 是否在前台 有前台服务 在后台
        int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), isForeground);
        BatteryCanaryUtil.getProxy().updateAppStat(appStat);
        synchronized (TAG) {
            if (mStampList != Collections.EMPTY_LIST) {
                MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertAppStat(appStat));
                mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(appStat)));
                checkOverHeat();
            }
        }

        MatrixLog.i(TAG, "updateAppImportance when app " + (isForeground ? "foreground" : "background"));
        updateAppImportance();
    }

        // importance 值说明
        //public static final int IMPORTANCE_BACKGROUND = 400//后台
        //public static final int IMPORTANCE_EMPTY = 500//空进程
        //public static final int IMPORTANCE_FOREGROUND = 100//在屏幕最前端、可获取到焦点
        //public static final int IMPORTANCE_SERVICE = 300//在服务中
        //public static final int IMPORTANCE_VISIBLE = 200//在屏幕前端、获取不到焦点

    // 获取到所有的运行的程序,更新本进程组进程的 importance 值和本进程的 importance值
    private void updateAppImportance() {
        // 理论上不可能进入该 if
        // 默认1024(本函数最后更新该值)<= 100     默认1024(本函数最后更新该值)<= 100
        if (mAppImportance <= mForegroundServiceImportanceLimit && mGlobalAppImportance <= mForegroundServiceImportanceLimit) {
            return;
        }

        Runnable runnable = new Runnable() {
            @SuppressWarnings("SpellCheckingInspection")
            @Override
            public void run() {
                Context context = mCore.getContext();
                String mainProc = context.getPackageName();
                if (mainProc.contains(":")) {
                    mainProc = mainProc.substring(0, mainProc.indexOf(":"));
                }

                ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
                if (am == null) {
                    return;
                }
                //获取当前运行程序信息
                List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
                if (processes == null) {
                    return;
                }

                for (ActivityManager.RunningAppProcessInfo item : processes) {
                    if (item.processName.startsWith(mainProc)) { // 同一个进程组
                        // default = 1024
                        if (mGlobalAppImportance > item.importance) {
                            MatrixLog.i(TAG, "update global importance: " + mGlobalAppImportance + " > " + item.importance
                                    + ", reason = " + item.importanceReasonComponent);
                            mGlobalAppImportance = item.importance;
                        }
                        if (item.processName.equals(context.getPackageName())) { // 主进程
                            // default = 1024
                            if (mAppImportance > item.importance) {
                                MatrixLog.i(TAG, "update app importance: " + mAppImportance + " > " + item.importance
                                        + ", reason = " + item.importanceReasonComponent);
                                mAppImportance = item.importance;
                            }
                        }
                    }
                }
            }
        };

        if (Looper.myLooper() == Looper.getMainLooper()) {
            mCore.getHandler().post(runnable);
        } else {
            runnable.run();
        }
    }

    @WorkerThread
    @Override
    // app stop 后台时回调   duringMillis = 10min
    public void onBackgroundCheck(long duringMillis) {
        super.onBackgroundCheck(duringMillis);
        MatrixLog.i(TAG, "#onBackgroundCheck, during = " + duringMillis);

        if (mGlobalAppImportance > mForegroundServiceImportanceLimit || mAppImportance > mForegroundServiceImportanceLimit) {
            Context context = mCore.getContext();
            ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            if (am == null) {
                return;
            }
            // 获取正在运行后台服务列表
            List<ActivityManager.RunningServiceInfo> runningServices = am.getRunningServices(Integer.MAX_VALUE);
            if (runningServices == null) {
                return;
            }

            for (ActivityManager.RunningServiceInfo item : runningServices) {
                // app 进程组的服务
                if (!TextUtils.isEmpty(item.process) && item.process.startsWith(context.getPackageName())) {
                    if (item.foreground) {
                        MatrixLog.i(TAG, "checkForegroundService whether app importance is low, during = " + duringMillis);
                        // foreground service is running when app importance is low
                        if (mGlobalAppImportance > mForegroundServiceImportanceLimit) {
                            // global
                            MatrixLog.w(TAG, "foreground service detected with low global importance: "
                                    + mAppImportance + ", " + mGlobalAppImportance + ", " + item.service);
                            mCore.onForegroundServiceLeak(false, mAppImportance, mGlobalAppImportance, item.service, duringMillis);
                        }

                        if (mAppImportance > mForegroundServiceImportanceLimit) {
                            if (item.process.equals(context.getPackageName())) {
                                // myself
                                MatrixLog.w(TAG, "foreground service detected with low app importance: "
                                        + mAppImportance + ", " + mGlobalAppImportance + ", " + item.service);
                                mCore.onForegroundServiceLeak(true, mAppImportance, mGlobalAppImportance, item.service, duringMillis);
                            }
                        }
                    }
                }
            }
        }

        // MatrixLog.i(TAG, "checkBackgroundAppState when app background, during = " + duringMillis);
        // checkBackgroundAppState(duringMillis);
    }
  • 总结:
  1. 可以根据进程信息中的 importance 来判断 app 的状态
    - importance = 400 后台
    - importance = 500 空进程
    - importance =100 在屏幕最前端、可获取到焦点
    - importance = 300 在服务中
    - importance = 200 在屏幕前端、获取不到焦点

结束。余下几个是一类

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

推荐阅读更多精彩内容