更可靠和高精度的 iOS 定时器

定时器一般用于延迟一段时间执行特定的代码,必要的话按照指定的频率重复执行。iOS 中延时执行有多种方式,常用的有:

  • NSTimer
  • NSObject 的 (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
  • CADisplayLink
  • GCD 的 dispatch_after
  • GCD 的 dispatch_source_t

每种方法创建的定时器,其可靠性与最小精度都有不同。可靠性指是否严格按照设定的时间间隔按时执行,最小精度指支持的最小时间间隔是多少。

1. NSRunLoop

谈到定时器,首先需要了解的一个概念是 NSRunLoop。NSRunLoop 是消息处理的一种机制,类似于 Windows 中的消息循环,有个更通用的叫法是 Event Loop

其原理很简单,启动一个循环,无限地重复接受消息->等待消息->处理消息这个过程,直到退出。伪代码如下:

void loop() {
    do {
        void *msg = getMessage();
        processMessage(msg);
    } while (msg != quit);
}

每个线程内部都会有一个 RunLoop,启动 RunLoop 之后,就能够让线程在没有消息时休眠,在有消息时被唤醒并处理消息,避免资源长期被占用。

在 iOS 中,NSThead 和 NSRunLoop 是一一对应的,但创建线程的时候不会默认创建 NSRunLoop,实际上也不允许自己创建 NSRunLoop,在线程内第一次调用[NSRunLoop currentRunLoop]的时候才会自动创建。

1.1 NSRunLoop 处理的输入源(input sources):

  • 鼠标、键盘事件。
  • NSPort 对象。
  • NSConnection 对象。

NSRunLoop 也处理 NSTimer 事件,但 NSTimer 并不属于输入源的一种。

1.2 苹果使用 NSRunLoop 实现的功能:

  • 硬件操作,如触摸、按键、摇晃等。
  • 手势操作。
  • 界面刷新,如更新了 UI 的 frame,或手动调 setNeedsLayout/setNeedsDisplay。
  • 定时器。包括 NSTimer、CADisplayLink、PerformSelecter、GCD。
  • 网络请求。

深入了解 RunLoop有更深入完整的介绍。

2. NSTimer

最常用,能满足对间隔要求不严格、对精确度不敏感的需求。

2.1 使用方法

- (void)startNSTimer {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:self.timeInterval target:self selector:@selector(onNSTimerTimeout:) userInfo:nil repeats:YES];
}

- (void)onNSTimerTimeout:(id)sender {
    NSLog(@"onNSTimerTimeout");
}

2.2 可靠性

不可靠,其所在的 RunLoop 会定时检测是否可以触发 NSTimer 的事件,但由于 iOS 有多个 RunLoop 的运行模式,如果被切到另一个 run loop,NSTimer 就不会被触发。每个 RunLoop 的循环间隔也无法保证,当某个任务耗时比较久,RunLoop 的下一个消息处理就只能顺延,导致 NSTimer 的时间已经到达,但 Runloop 却无法及时触发 NSTimer,导致该时间点的回调被错过。

苹果官方文档:

A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer.

2.3 最小精度

理论上最小精度为 0.1 毫秒。不过由于受 Runloop 的影响,会有 50 ~ 100 毫秒的误差,所以,实际精度可以认为是 0.1 秒。

苹果官方文档:

Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds.

2.4 实测结果

间隔 0.1 秒,调用12次。其中倒数第二次调用前会执行一个比较耗时的运算任务。

代码:

- (void)startNSTimer {
    [self setupConfig];

    [self runNSTimerIfNeeded];

    NSLog(@"NSTimer start with interval: %.3f ms, start time: %@, total count: %d", self.timeInterval * 1000, [self timeStringWithTime:self.startTime], (int)self.maxCount);
}

- (void)runNSTimerIfNeeded {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:self.timeInterval
                                     target:self
                                   selector:@selector(onNSTimerTimeout:)
                                   userInfo:nil
                                    repeats:NO];

    self.startTime = [NSDate date];
}

- (void)onNSTimerTimeout:(NSTimer *)sender {
    NSLog(@"%d, %@", ++ self.curCount, [self diffTimeStringFromStart]);

    [self.timer invalidate];
    self.timer = nil;

    if (self.curCount < self.maxCount) {
        [self runNSTimerIfNeeded];
        [self runBusyTaskIfNeeded];
    }
}

结果:

2016-08-29 11:32:40.302 TimerDemo[37258:10736148] NSTimer start with interval: 100.000 ms, start time: 1472441560302.602 ms, total count: 12
2016-08-29 11:32:40.403 TimerDemo[37258:10736148] 1, interval: 101.045 ms, discrepancy: 1.045 ms
2016-08-29 11:32:40.505 TimerDemo[37258:10736148] 2, interval: 100.890 ms, discrepancy: 0.890 ms
2016-08-29 11:32:40.606 TimerDemo[37258:10736148] 3, interval: 101.087 ms, discrepancy: 1.087 ms
2016-08-29 11:32:40.707 TimerDemo[37258:10736148] 4, interval: 101.038 ms, discrepancy: 1.038 ms
2016-08-29 11:32:40.809 TimerDemo[37258:10736148] 5, interval: 101.061 ms, discrepancy: 1.061 ms
2016-08-29 11:32:40.910 TimerDemo[37258:10736148] 6, interval: 101.069 ms, discrepancy: 1.069 ms
2016-08-29 11:32:41.012 TimerDemo[37258:10736148] 7, interval: 101.031 ms, discrepancy: 1.031 ms
2016-08-29 11:32:41.113 TimerDemo[37258:10736148] 8, interval: 101.035 ms, discrepancy: 1.035 ms
2016-08-29 11:32:41.214 TimerDemo[37258:10736148] 9, interval: 100.890 ms, discrepancy: 0.890 ms
2016-08-29 11:32:41.315 TimerDemo[37258:10736148] 10, interval: 101.042 ms, discrepancy: 1.042 ms
2016-08-29 11:32:41.315 TimerDemo[37258:10736148] start busy task
2016-08-29 11:32:41.970 TimerDemo[37258:10736148] finish busy task
2016-08-29 11:32:41.970 TimerDemo[37258:10736148] 11, interval: 654.319 ms, discrepancy: 554.319 ms
2016-08-29 11:32:42.071 TimerDemo[37258:10736148] 12, interval: 100.906 ms, discrepancy: 0.906 ms

可以看到偏差在 1 ~ 2 毫秒左右。在第 10 次之后执行了一个较耗时的任务,导致第 11 次比预期延迟了 0.5 秒执行。后面的回调仍然按照预设的延时执行。

3. performSelector:withObject:afterDelay:

这是 NSObject 对 NSTimer 封装后提供的一个比较简单的延时方法,内部用的也是 NSTimer,所以,同上。

4. CADisplayLink

CADisplayLink 也可以用作定时器,其调用间隔与屏幕刷新频率一致,也就是每秒 60 帧,间隔 16.67 ms。与 NSTimer 类似,如果在两次屏幕刷新之间执行了一个比较耗时的任务,其中的某一帧就会被跳过,造成 UI 卡顿。

4.1 使用方法

- (void)runCADisplayLinkTimer {
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onCADisplayLinkTimeout)];
    displayLink.frameInterval = 0.0167; // S
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    self.displayLink = displayLink;
}

- (void)onCADisplayLinkTimeout {
    NSLog(@"onCADisplayLinkTimeout");
}

4.2 可靠性

如果执行的任务很耗时,也会导致回调被错过,所以并不十分可靠。但是,假如调用者能够确保任务能够在最小时间间隔内执行完成,CADisplayLink 就比较可靠,因为屏幕的刷新频率是固定的。

4.3 最小精度

受限于每秒 60 帧的屏幕刷新频率,注定 CADisplayLink 的最小精度为 16.67 毫秒。误差在 1 毫秒左右。

另外需要注意的是,虽然 CADisplayLink 有一个属性 frameInterval 是用于设置定时器的调用间隔,但是这个属性会在第一次回调之后才生效,对于第一次回调,总是会以 1/60 的间隔来执行的。这样会导致的结果是,比如你设置了每 1 秒执行一次某个方法,但是第一次执行的时候,却是在 16.7 毫秒之后,远远小于预设值。

4.4 实测结果

间隔 0.1 秒,调用12次。其中倒数第二次调用前会执行一个比较耗时的运算任务。

代码:

- (void)startCADisplayLinkTimer {
    [self setupConfig];

    [self runCADisplayLinkTimer];

    NSLog(@"CADisplayLink start with interval: %.3f ms, start time: %@, total count: %d", self.timeInterval * 1000, [self timeStringWithTime:self.startTime], (int)self.maxCount);
}

- (void)runCADisplayLinkTimer {
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onCADisplayLinkTimeout)];
    NSInteger frameInterval = floor(self.timeInterval * 1000 / (1000 / 60.0));
    displayLink.frameInterval = frameInterval;

    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    self.displayLink = displayLink;

    self.startTime = [NSDate date];
}

- (void)onCADisplayLinkTimeout {
    NSLog(@"%d, %@", ++ self.curCount, [self diffTimeStringFromStart]);

    if (self.curCount < self.maxCount) {
        self.startTime = [NSDate date];
        [self runBusyTaskIfNeeded];
    } else {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
}

结果:

2016-08-29 11:33:47.835 TimerDemo[37258:10736148] CADisplayLink start with interval: 100.000 ms, start time: 1472441627835.872 ms, total count: 12
2016-08-29 11:33:47.845 TimerDemo[37258:10736148] 1, interval: 10.061 ms, discrepancy: -89.939 ms
2016-08-29 11:33:47.946 TimerDemo[37258:10736148] 2, interval: 99.829 ms, discrepancy: -0.171 ms
2016-08-29 11:33:48.046 TimerDemo[37258:10736148] 3, interval: 99.573 ms, discrepancy: -0.427 ms
2016-08-29 11:33:48.145 TimerDemo[37258:10736148] 4, interval: 99.427 ms, discrepancy: -0.573 ms
2016-08-29 11:33:48.246 TimerDemo[37258:10736148] 5, interval: 99.801 ms, discrepancy: -0.199 ms
2016-08-29 11:33:48.346 TimerDemo[37258:10736148] 6, interval: 99.754 ms, discrepancy: -0.246 ms
2016-08-29 11:33:48.446 TimerDemo[37258:10736148] 7, interval: 99.791 ms, discrepancy: -0.209 ms
2016-08-29 11:33:48.546 TimerDemo[37258:10736148] 8, interval: 99.836 ms, discrepancy: -0.164 ms
2016-08-29 11:33:48.646 TimerDemo[37258:10736148] 9, interval: 99.840 ms, discrepancy: -0.160 ms
2016-08-29 11:33:48.746 TimerDemo[37258:10736148] 10, interval: 99.811 ms, discrepancy: -0.189 ms
2016-08-29 11:33:48.746 TimerDemo[37258:10736148] start busy task
2016-08-29 11:33:49.399 TimerDemo[37258:10736148] finish busy task
2016-08-29 11:33:49.400 TimerDemo[37258:10736148] 11, interval: 653.891 ms, discrepancy: 553.891 ms
2016-08-29 11:33:49.412 TimerDemo[37258:10736148] 12, interval: 12.566 ms, discrepancy: -87.434 ms

除了第一次回调,间隔误差比较大之外,别的回调误差在 0.1 ~ 0.5 毫秒之间,精度比 NSTimer 要高。第 11 次回调,受耗时任务影响,延时了 0.5 秒。值得注意的是,第 12 次,延时再次与第一次回调一样,变成了 1/60 秒左右。

换言之,CADisplayLink 在第一次回调以及在耗时任务之后的回调,精度不可控。

5. GCD dispatch_after

dispatch_after 用起来十分简单,代码紧凑易读,而且可以很轻松地在别的线程分配延时任务,所以使用范围很广泛。

5.1 使用方法

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    //handle timeout
});

5.2 可靠性

Any fire of the timer may be delayed by the system in order to improve power consumption and system performance. The upper limit to the allowable delay may be configured with the ‘leeway’ argument, the lower limit is under the control of the system.

5.3 最小精度

延时参数的单位是纳秒。如果有延时,则无法保证。

5.4 实测结果

间隔 0.1 秒,调用12次。其中倒数第二次调用前会执行一个比较耗时的运算任务。

代码:

- (void)startDispatchAfterTimer {
    [self setupConfig];

    [self runDispatchAfterTimerIfNeeded];

    NSLog(@"DispatchAfterTimer start with interval: %.3f ms, start time: %@, total count: %d", self.timeInterval * 1000, [self timeStringWithTime:self.startTime], (int)self.maxCount);
}

- (void)runDispatchAfterTimerIfNeeded {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self onDispatchAfterTimeout];
    });

    self.startTime = [NSDate date];
}

- (void)onDispatchAfterTimeout {
    NSLog(@"%d, %@", ++ self.curCount, [self diffTimeStringFromStart]);
    if (self.curCount < self.maxCount) {
        [self runDispatchAfterTimerIfNeeded];
        [self runBusyTaskIfNeeded];
    }
}

结果:

2016-08-29 11:34:09.652 TimerDemo[37258:10736148] DispatchAfterTimer start with interval: 100.000 ms, start time: 1472441649652.825 ms, total count: 12
2016-08-29 11:34:09.756 TimerDemo[37258:10736148] 1, interval: 103.876 ms, discrepancy: 3.876 ms
2016-08-29 11:34:09.866 TimerDemo[37258:10736148] 2, interval: 109.686 ms, discrepancy: 9.686 ms
2016-08-29 11:34:09.976 TimerDemo[37258:10736148] 3, interval: 109.772 ms, discrepancy: 9.772 ms
2016-08-29 11:34:10.085 TimerDemo[37258:10736148] 4, interval: 108.764 ms, discrepancy: 8.764 ms
2016-08-29 11:34:10.195 TimerDemo[37258:10736148] 5, interval: 109.057 ms, discrepancy: 9.057 ms
2016-08-29 11:34:10.299 TimerDemo[37258:10736148] 6, interval: 104.544 ms, discrepancy: 4.544 ms
2016-08-29 11:34:10.408 TimerDemo[37258:10736148] 7, interval: 108.753 ms, discrepancy: 8.753 ms
2016-08-29 11:34:10.516 TimerDemo[37258:10736148] 8, interval: 107.597 ms, discrepancy: 7.597 ms
2016-08-29 11:34:10.626 TimerDemo[37258:10736148] 9, interval: 109.933 ms, discrepancy: 9.933 ms
2016-08-29 11:34:10.736 TimerDemo[37258:10736148] 10, interval: 109.791 ms, discrepancy: 9.791 ms
2016-08-29 11:34:10.736 TimerDemo[37258:10736148] start busy task
2016-08-29 11:34:11.394 TimerDemo[37258:10736148] finish busy task
2016-08-29 11:34:11.394 TimerDemo[37258:10736148] 11, interval: 657.669 ms, discrepancy: 557.669 ms
2016-08-29 11:34:11.496 TimerDemo[37258:10736148] 12, interval: 102.005 ms, discrepancy: 2.005 ms

平均误差 9 毫秒。

6. GCD dispatch_source_t

经测试,dispatch_source_t 的最小精度和可靠性都与 diapatch_after 差不多。

6.1 实测结果

间隔 0.1 秒,调用12次。其中倒数第二次调用前会执行一个比较耗时的运算任务。

代码:

- (void)startDispatchSourceTimer {
    [self setupConfig];

    [self runDispatchSourceTimerIfNeeded];

    NSLog(@"DispatchSourceTimer start with interval: %.3f ms, start time: %@, total count: %d", self.timeInterval * 1000, [self timeStringWithTime:self.startTime], (int)self.maxCount);
}

- (void)runDispatchSourceTimerIfNeeded {
    self.sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());

    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 0);
    dispatch_source_set_timer(self.sourceTimer, start, (int64_t)(self.timeInterval * NSEC_PER_SEC), 0);

    dispatch_source_set_event_handler(self.sourceTimer, ^{
        [self onDispatchSourceTimeout];
    });

    dispatch_resume(self.sourceTimer);

    self.startTime = [NSDate date];
}

- (void)onDispatchSourceTimeout {
    NSLog(@"%d, %@", ++ self.curCount, [self diffTimeStringFromStart]);

    dispatch_cancel(self.sourceTimer);
    self.timer = nil;

    if (self.curCount < self.maxCount) {
        [self runDispatchAfterTimerIfNeeded];
        [self runBusyTaskIfNeeded];
    }
}

结果:

2016-08-29 11:34:24.088 TimerDemo[37258:10736148] DispatchSourceTimer start with interval: 100.000 ms, start time: 1472441664088.390 ms, total count: 12
2016-08-29 11:34:24.089 TimerDemo[37258:10736148] 1, interval: 1.429 ms, discrepancy: -98.571 ms
2016-08-29 11:34:24.196 TimerDemo[37258:10736148] 2, interval: 106.696 ms, discrepancy: 6.696 ms
2016-08-29 11:34:24.306 TimerDemo[37258:10736148] 3, interval: 109.500 ms, discrepancy: 9.500 ms
2016-08-29 11:34:24.416 TimerDemo[37258:10736148] 4, interval: 109.999 ms, discrepancy: 9.999 ms
2016-08-29 11:34:24.526 TimerDemo[37258:10736148] 5, interval: 109.744 ms, discrepancy: 9.744 ms
2016-08-29 11:34:24.636 TimerDemo[37258:10736148] 6, interval: 109.691 ms, discrepancy: 9.691 ms
2016-08-29 11:34:24.746 TimerDemo[37258:10736148] 7, interval: 109.767 ms, discrepancy: 9.767 ms
2016-08-29 11:34:24.856 TimerDemo[37258:10736148] 8, interval: 109.799 ms, discrepancy: 9.799 ms
2016-08-29 11:34:24.966 TimerDemo[37258:10736148] 9, interval: 109.820 ms, discrepancy: 9.820 ms
2016-08-29 11:34:25.076 TimerDemo[37258:10736148] 10, interval: 109.804 ms, discrepancy: 9.804 ms
2016-08-29 11:34:25.076 TimerDemo[37258:10736148] start busy task
2016-08-29 11:34:25.734 TimerDemo[37258:10736148] finish busy task
2016-08-29 11:34:25.734 TimerDemo[37258:10736148] 11, interval: 657.591 ms, discrepancy: 557.591 ms
2016-08-29 11:34:25.835 TimerDemo[37258:10736148] 12, interval: 101.295 ms, discrepancy: 1.295 ms

从结果看,与 diapatch_after 区别不大。

7. 更高精度的定时器

上述的各种定时器,都受限于苹果为了保护电池和提高性能采用的策略,导致无法实时地执行回调。如果你的确需要使用更高精度的定时器,官方也提供了方法,见 High Precision Timers in iOS / OS X

前面所述的定时器,使用方法各有不同,但其核心代码实际上是一样的。

There are many API’s in iOS and OS X that allow waiting for a specified period of time. They may be Objective C or C, and they take different kinds of arguments, but they all end up using the same code inside the kernel.

而有别于普通定时器的高精度定时器,则是基于高优先级的线程调度类创建的定时器,在没有多线程冲突的情况下,这类定时器的请求会被优先处理。

7.1 实现方法

  • 把定时器所在的线程,移到高优先级的线程调度类。
  • 使用更精确的计时器API,换言之,你想要 10 秒后执行,就绝对在 10 秒后执行,绝不提前,也不延迟。

7.2 如何使用

提高调度优先级:

#include <mach/mach.h>
#include <mach/mach_time.h>
#include <pthread.h>

void move_pthread_to_realtime_scheduling_class(pthread_t pthread) {
    mach_timebase_info_data_t timebase_info;
    mach_timebase_info(&timebase_info);

    const uint64_t NANOS_PER_MSEC = 1000000ULL;
    double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;

    thread_time_constraint_policy_data_t policy;
    policy.period      = 0;
    policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
    policy.constraint  = (uint32_t)(10 * clock2abs);
    policy.preemptible = FALSE;

    int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
                   THREAD_TIME_CONSTRAINT_POLICY,
                   (thread_policy_t)&policy,
                   THREAD_TIME_CONSTRAINT_POLICY_COUNT);
    if (kr != KERN_SUCCESS) {
        mach_error("thread_policy_set:", kr);
        exit(1);
    }
}

精确延时:

#include <mach/mach.h>
#include <mach/mach_time.h>

static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;

static mach_timebase_info_data_t timebase_info;

static uint64_t abs_to_nanos(uint64_t abs) {
    return abs * timebase_info.numer  / timebase_info.denom;
}

static uint64_t nanos_to_abs(uint64_t nanos) {
    return nanos * timebase_info.denom / timebase_info.numer;
}

void example_mach_wait_until(int argc, const char * argv[]) {
    mach_timebase_info(&timebase_info);
    uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
    uint64_t now = mach_absolute_time();
    mach_wait_until(now + time_to_wait);
}

7.3 最小精度

小于 0.5 毫秒。这里有一份实现的代码以及与普通定时器的对比。

8. 参考

原文地址:http://blog.lessfun.com/blog/2016/08/05/reliable-timer-in-ios/

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

推荐阅读更多精彩内容