RunLoop 知识详解

一、简介

RunLoop是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoopCFRunLoopRefCFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

二、RunLoop 内部的逻辑

image

实际上 RunLoop 内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,直到超时或被手动停止,该函数才会返回。

三、CFRunLoopRef之系统默认注册的5个Mode

(1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

四、RunLoop的四个作用:

(1)使程序一直运行接受用户输入
(2)决定程序在何时应该处理哪些Event
(3)调用解耦
(4)节省CPU时间

程序主线程一开始,就会一直跑,其内部一定是开启了一个和主线程对应的RunLoop,并且可以看出函数返回的是一个int返回值的 UIApplicationMain()函数
int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));
    }
}
UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象,使得我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个Cocoa程序的线程中,都可以通过:

NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 来获取到当前线程的run loop。

一个run loop就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。

NSRunLoop是一种更加高明的消息处理模式,他就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source中了。使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。

五、runloop应用

1、 NSTimer

前面一直提到Timer Source作为事件源,事实上它的上层对应就是NSTimer(其实就是CFRunloopTimerRef)这个开发者经常用到的定时器(底层基于使用mk_timer实现),甚至很多开发者接触RunLoop还是从NSTimer开始的。其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。

NSTimer的创建通常有两种方式,尽管都是类方法,一种是timerWithTimeInterval,另一种scheduledTimerWithTimeInterval。二者最大的区别就是scheduledTimerWithTimeInterval除了创建一个定时器外还会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的。
例如下面的代码中如果timer2不加入到RunLoop中是无法正常工作的。同时注意如果滚动UIScrollView(UITableView/UICollectionview)二者是无法正常工作的,但是如果将NSDefaultRunLoopMode改为NSRunLoopCommonModes则可以正常工作,这也解释了前面介绍的Mode内容。

-(NSTimer *)timer1{
    if(_timer1 == nil){
        _timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
    }
    return _timer1;
}

-(NSTimer *)timer2{
    if(_timer2 == nil){
        _timer2 = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop]addTimer:_timer2 forMode:NSDefaultRunLoopMode];
    }
    return _timer2;
}

问题一、target无法释放导致内存泄漏

定时器添加到runloop中后,runloop会将当前计时器retain;
定时器在初始化时候会指定一个target,会导致target无法释放。

通过移除定时器可以避免:

viewcontroller中

-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    [self.timer1 invalidate];
    [self.timer2 invalidate];
    self.timer1 = nil;
    self.timer2 = nil;
}

View中

-(void)removeFromSuperview{
    [super removeFromSuperview];
    [self.timer invalidate];
    self.timer = nil;
}

问题二、Timer不是一种实时机制

在一个循环中如果RunLoop没有被识别(这个时间大概在50-100ms)或者当前RunLoop在执行一个长的call out(例如执行某个循环操作)则NSTimer可能就会存在误差
如下面一个例子:

@interface TimerRunloopViewController ()
@property (weak, nonatomic) IBOutlet UILabel *labelShow;
@property(nonatomic,strong) NSThread *thread;
@property(nonatomic,strong) NSTimer *timer;
@end

@implementation TimerRunloopViewController

-(NSThread *)thread{
    if(_thread == nil){
        _thread = [[NSThread alloc]initWithTarget:self selector:@selector(performTask) object:nil];
    }
    return _thread;
}

-(NSTimer *)timer{
    if(_timer == nil){
        _timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerPerform) userInfo:nil repeats:YES];
    }
    return _timer;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self TRDataInit];
}

-(void)dealloc{
    NSLog(@"timerRunloop dealloc");
}

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
//    [self.thread cancel];
    [self.timer invalidate];
    self.timer = nil;
}

- (void)TRDataInit{
    [self.thread start];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    [self.thread cancel];
}

#pragma mark - 线程执行任务
- (void)performTask{
    [[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode];
    NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);
    // 区分直接调用和「performSelector:withObject:afterDelay:」区别,下面的直接调用无论是否运行RunLoop一样可以执行,但是后者则不行。
    //[self caculate];
    [self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];
    // 取消当前RunLoop中注册测selector(注意:只是当前RunLoop,所以也只能在当前RunLoop中取消)
    //[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
    NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);
    // 非主线程RunLoop必须手动调用
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"注意:如果RunLoop不退出(运行中),这里的代码并不会执行,RunLoop本身就是一个循环.");
}

#pragma mark - 定时器执行任务
- (void)timerPerform{
    static unsigned long count = 0;
    if([NSThread currentThread].isCancelled){
        [self.timer invalidate];
        self.timer = nil;
    }
    count ++;
    NSLog(@"timer count 值 %ld",count);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.labelShow.text = [NSString stringWithFormat:@"timer count 值 %ld",count];
    });
}

- (void)caculate{
    for (int i = 0;i < 9999;++i) {
        NSLog(@"%i,%@",i,[NSThread currentThread]);
        if ([NSThread currentThread].isCancelled) {
            return;
        }
    }
}

如果运行并且不退出上面的程序会发现,前两秒NSTimer可以正常执行,但是两秒后由于同一个RunLoop中循环操作的执行造成定时器跳过了中间执行的机会一直到caculator循环完毕,这也正说明了NSTimer不是实时系统机制的原因。

image
image
但是以上程序还有几点需要说明一下:
  1. NSTimer会对Target进行强引用直到任务结束或exit之后才会释放。如果上面的程序没有进行线程cancel而终止任务则即使关闭控制器也无法正确释放。

  2. 非主线程的RunLoop并不会自动运行(同时注意默认情况下非主线程的RunLoop并不会自动创建,直到第一次使用),RunLoop运行必须要在加入NSTimer或Source0、Sourc1、Observer输入后运行否则会直接退出。例如上面代码如果run放到NSTimer创建之前则既不会执行定时任务也不会执行循环运算。

  3. performSelector:withObject:afterDelay:执行的本质还是通过创建一个NSTimer然后加入到当前线程RunLoop(通过前后两次打印RunLoop信息可以看到此方法执行之后RunLoop的timer会增加1个。类似的还有performSelector:onThread:withObject:afterDelay:,只是它会在另一个线程的RunLoop中创建一个Timer),所以此方法事实上在任务执行完之前会对触发对象形成引用,任务执行完进行释放(例如上面会对ViewController形成引用,注意:performSelector: withObject:等方法则等同于直接调用,原理与此不同)。

  4. 同时上面的代码也充分说明了RunLoop是一个循环事实,run方法之后的代码不会立即执行,直到RunLoop退出。

  5. 上面程序的运行过程中如果突然dismiss,则程序的实际执行过程要分为两种情况考虑:如果循环任务caculate还没有开始则会在timer中停止timer运行(停止了线程中第一个任务),然后等待caculate执行并break(停止线程中第二个任务)后线程任务执行结束释放对控制器的引用;如果循环任务caculate执行过程中dismiss则caculate任务执行结束,等待timer下个周期运行(因为当前线程的RunLoop并没有退出,timer引用计数器并不为0)时检测到线程取消状态则执行invalidate方法(第二个任务也结束了),此时线程释放对于控制器的引用。

CADisplayLink是一个执行频率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改变刷新频率)的定时器,它也需要加入到RunLoop才能执行。
NSTimer类似,CADisplayLink同样是基于CFRunloopTimerRef实现,底层使用mk_timer(可以比较加入到RunLoop前后RunLoop中timer的变化)。和NSTimer相比它精度更高(尽管NSTimer也可以修改精度),不过和NStimer类似的是如果遇到大任务它仍然存在丢帧现象。
通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更广泛的用处。

2、 AutoreleasePool

AutoreleasePoolRunLoop 并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observercallout都是_ wrapRunLoopWithAutoreleasePoolHandler,这两个是和自动释放池相关的两个监听。

image
image

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。这个Observerorder-2147483647优先级最高,确保发生在所有回调操作之前。

第二个Observer会监听RunLoop进入休眠即将退出RunLoop两种状态,在即将进入休眠时会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() 释放自动释放池内对象。这个Observerorder2147483647,优先级最低,确保发生在所有回调操作之后。

主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建AutoreleasePool否则一般不需要自己创建)。
其实在应用程序启动后系统还注册了其他Observer(例如即将进入休眠时执行注册回调_UIGestureRecognizerUpdateObserver用于手势处理、回调为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面实时绘制更新)和多个Source1(例如context为CFMachPort的Source1用于接收硬件事件响应进而分发到应用程序一直到UIEvent),这里不再一一详述。

3、UI更新

打印App启动之后的主线程RunLoop可以发现另外一个callout_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPvObserver,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新
通常情况下这种方式是完美的,因为除了系统的更新,还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView或CALayer的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听即将进入休眠退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。

4、 NSURLConnection

NSURLConnection一旦启动就会不断调用delegate方法接收数据,这样一个连续的的动作正是基于RunLoop来运行。一旦NSURLConnection设置了delegate就会立即创建一个线程com.apple.NSURLConnectionLoader,同时内部启动RunLoop并在NSDefaultMode模式下添加4个Source0。其中CFHTTPCookieStorage用于处理cookieCFMultiplexerSource负责各种delegate回调并在回调中唤醒delegate内部的RunLoop(通常是主线程)来执行实际操作。

5、AFNetworking

AFURLConnectionOperation这个类是基于 NSURLConnection构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop 启动前内部必须要有至少一个Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort添加进去了。通常情况下,调用者需要持有这个NSMachPort (mach_port)并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [self performSelector:onThread:] 将这个任务扔到了后台线程的 RunLoop 中。

6、GCD和RunLoop的关系

RunLoop本身和GCD并没有直接的关系。当调用了dispatch_async(dispatch_get_main_queue(), ^(void)block)libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作是全部由libDispatch驱动的。

7、RunLoop的启动和退出

一、启动RunLoop的三种方式
通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()可以获取当前线程的runloop。

- (void)run;  
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。
(1) 第一种方式,runloop会一直运行下去(线程常驻),在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
(2) 第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
(3) 第三种方式,runloop会运行一次超时时间到达或者第一个input source被处理,则runloop就会退出。

  • 前两种启动方式会重复调用runMode:beforeDate:方法。

二、 退出RunLoop的方式

(1) 第一种启动方式的退出方法
文档说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。
如果runloop没有input sources或者附加的timer,runloop就会退出。
虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。

(2)第二种启动方式的退出方法 runUntilDate:
可以通过 设置超时时间 来退出 runloop
(3)第三种启动方式的退出方法 runMode:beforeDate:
通过这种方式启动,runloop只会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。

  • 如果我们想控制 runloop 的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:
    具体可以参考苹果文档给出的方案,如下:
NSRunLoop *myLoop  = [NSRunLoop currentRunLoop];
myPort = (NSMachPort *)[NSMachPort port];
[myLoop addPort:_port forMode:NSDefaultRunLoopMode];
BOOL isLoopRunning = YES; // global
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//在关闭runloop的地方
- (void)quitLoop
{
    isLoopRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

总之
如果不想退出runloop可以使用第一种方式启动runloop;
使用第二种方式启动runloop,可以通过设置超时时间来退出;
使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。

8、更多RunLoop使用

1、使用perfromSelector在默认模式下设置图片,防止UITableView滚动卡顿
[MyImageView performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]]
2、sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存
3、老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。

小结:
1、runloop本质上是一个do-while循环,一个线程对应一个loop。主线程loop默认开启,子线程要自己手动开启。一个loop钟有多个模式(mode),默认都是default模式。
2、子线程保活(保证线程的长时间存活 ),比如在子线程加一个定时器,默认执行一次就不会再执行了
3、假如在主线程加一个定时器去修改UI,当我们滑动界面的时候就会发现UI不变了。因为默认是default模式,滑动界面时切换成响应模式。通过调整成common模式,就可以解决这个问题了。
4、重任务分散,比如批量大图加载。

参考文献:
iOS刨根问底-深入理解RunLoop
RunLoop总结:RunLoop的应用场景(三)滚动视图流畅性优化
CFRunloop 优化TableView加载高清大图UI卡顿问题。单独分批加载

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

推荐阅读更多精彩内容