【OC RunLoop】

目录
一、RunLoop是什么
二、RunLoop的底层实现
  1、RunLoop和线程的关系
  2、RunLoop的运行模式
  3、Source0事件源、Source1事件源、Observer、Timer事件源
三、RunLoop的运行流程
四、RunLoop的实际应用
  1、处理NSTimer不工作的问题
  2、常驻线程


一、RunLoop是什么


RunLoop即运行循环,它就是一个OC对象,它的主要作用有三个:

  • 保证App的持续运行,具体地说,我们的App在启动后就会在main函数那里创建并启动主线程对应的RunLoop,这个RunLoop内部维护着一个do...while循环,正是由于这个do...while循环的存在,才使得主线程得以保活——即主线程不会执行完任务立马就退出——也就是App得以保活,否则App一启动执行完main函数就退出了;
  • 处理App的各种事件,具体地说,RunLoop内部的do...while循环里循环处理着App的各种事件,包括Source0事件、Source1事件和Timer事件;
  • 线程的休眠唤醒则是RunLoop区别于其它语言EventLoop的核心所在,线程没事做时就休眠,有事做时就唤醒,这样可以节省CPU资源。


二、RunLoop的底层实现


RunLoop是NSRunLoop类型的,它的C语言实现为CFRunLoopRef,因为CFRunLoopRef是开源的,所以接下来我们会从它的源码来看看RunLoop的底层实现。

typedef struct __CFRunLoop *CFRunLoopRef;
struct __CFRunLoop {
    pthread_t _pthread; // RunLoop对应的线程

    CFMutableSetRef _modes; // RunLoop所有的运行模式
    CFRunLoopModeRef _currentMode; // RunLoop当前的运行模式

    CFMutableSetRef _commonModes; // RunLoop“特殊的运行模式”
    CFMutableSetRef _commonModeItems; // RunLoop“特殊的运行模式“里的items
};

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFStringRef _name; // 运行模式的名字,如@"NSDefaultRunLoopMode"、@"UITrackingRunLoopMode"
    CFMutableSetRef _sources0; // Set,Source0事件源集合
    CFMutableSetRef _sources1; // Set,Source1事件源集合
    CFMutableArrayRef _observers; // Array,观察者数组
    CFMutableArrayRef _timers; // Array,Timer事件源数组
};

1、RunLoop和线程的关系

苹果并没有为我们提供创建RunLoop的API,仅仅提供了获取RunLoop的API。

// Core Foundation框架
CFRunLoopGetMain(); // 获取主线程对应的RunLoop
CFRunLoopGetCurrent(); // 获取当前线程对应的RunLoop

// Foundation框架,对Core Foundation框架函数的封装
[NSRunLoop mainRunLoop]; // 获取主线程对应的RunLoop
[NSRunLoop currentRunLoop]; // 获取当前线程对应的RunLoop

这两套API的底层实现大概如下(伪代码,详见CFRunLoop.c文件):

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}


// 一个全局的字典,线程是key,RunLoop是value
static CFMutableDictionaryRef __CFRunLoops;

CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    
    if (!__CFRunLoops) { // 如果是第一次获取RunLoop(那肯定是获取主线程对应的RunLoop,因为App一启动系统就会自动去获取主线程对应的RunLoop,我们自己写的获取且早着呢)

        // 初始化全局的字典
        __CFRunLoops = CFDictionaryCreateMutable();
        
        // 创建主线程对应的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 主线程为key,主线程对应的RunLoop为value,存入全局的字典里
        CFDictionarySetValue(dict, pthread_main_thread_np(), mainLoop);
    }
    
    // 从全局的字典里读取某个线程对应的RunLoop
    CFRunLoopRef loop = CFDictionaryGetValue(__CFRunLoops, thread);
    
    if (!loop) { // 如果读取不到
        
        // 创建该线程对应的RunLoop,
        loop = __CFRunLoopCreate(thread);
        // 该线程为key,该线程对应的RunLoop为value,存入全局的字典里
        CFDictionarySetValue(__CFRunLoops, thread, loop);
    }

    // 注册一个回调,当某个线程销毁时,也销毁该线程对应的RunLoop
    _CFSetTSD(thread, loop, __CFFinalizeRunLoop);

    return loop;
}
  • RunLoop是基于线程来管理的,它们俩是一一对应的关系,共同存储在一个全局的字典里,线程是key,RunLoop是value
  • 对于主线程的RunLoop来说,App一启动系统就会自动创建并启动,而对于子线程的RunLoop来说,除非我们主动去获取,否则不会创建,我们获取也即创建子线程的RunLoop后,还需要手动启动它;
  • RunLoop的销毁发生在线程销毁时。

2、RunLoop的运行模式

  • 系统为RunLoop提供了好几种运行模式,其中NSDefaultRunLoopModeUITrackingRunLoopMode是我们经常使用的。NSDefaultRunLoopMode是RunLoop默认的运行模式,大多数时候RunLoop就运行在这种模式下,UITrackingRunLoopMode是界面滑动时RunLoop的运行模式,系统会自动完成不同情况下这两种运行模式的切换;
  • 当然RunLoop还有一种“特殊的运行模式”,就是NSRunLoopCommonModes,严格来说它不是一种运行模式,而是一些运行模式的组合。比如说系统会默认把NSDefaultRunLoopModeUITrackingRunLoopMode添加到NSRunLoopCommonModes里,RunLoop运行在NSRunLoopCommonModes模式时,并不是说它就真得运行在NSRunLoopCommonModes下,而是说RunLoop在切换真正的运行模式时会自动把一个运行模式里面的Source0/Source1/Observer/Timer同步到另一个运行模式里;
  • 一个RunLoop可以有多个运行模式,而每个运行模式里又可以有多个Source0/Source1/Observer/Timer,但是RunLoop一次只能运行在一个运行模式下,这个运行模式被称为CurrentMode,如果要切换运行模式,就得退出RunLoop,重新选择一个运行模式运行。RunLoop分Mode的目的就是为了把不同Mode里的Source0/Source1/Observer/Timer给隔离开来,在这个模式下的时候就做专心做这个模式里的事,在那个模式下的时候就专心做那个模式里的事,让它们互相不影响,当然如果你想一件事能在多个模式下做,那就把它扔到CommonModes下。

3、Source0事件源、Source1事件源、Observer、Timer事件源

Source0事件源

Source0事件源主要包括原始指针事件、手势事件、performSelector:onThread:等事件,这些事件都是我们用代码写的。

Source1事件源

Source1事件源主要包括锁屏、静音、靠近传感器、加速等系统事件,还有基于Port的线程间通信事件,这些事件都不是我们用代码写的,是系统发出的。

此外需要注意的是原始指针事件、手势事件也是首先被捕捉为Source1事件来唤醒主线程,然后再包装为Source0事件处理的。

Observer

Observer不是RunLoop的事件源,而是RunLoop的观察者,它主要用来观察RunLoop状态的变化,从而触发回调做一些自定义的处理,比如系统的UI刷新和autoreleasepool创建、销毁就是通过Observer观察RunLoop的状态实现的。RunLoop的状态有如下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入RunLoop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 线程即将处理Timer事件
    kCFRunLoopBeforeSources = (1UL << 2), // 线程即将处理Source事件
    kCFRunLoopBeforeWaiting = (1UL << 5), // 线程即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 线程被唤醒
    kCFRunLoopExit          = (1UL << 7), // 刚刚退出RunLoop
};
  • UI刷新

App一启动,系统就会添加一个Observer,监听主线程对应的RunLoop,主要负责UI刷新。这个Observer监听是“线程即将进入休眠”和“刚刚退出RunLoop”两个状态,它的回调里才会真正刷新UI。也就是说我们编写的UI代码,如设置view的frame、设置view的背景色等,并不是执行到那一行就立马刷新生效的,而是RunLoop的线程即将进入休眠或刚刚退出RunLoop时才刷新生效的。

  • autoreleasepool的创建和销毁

App一启动,系统就会添加两个Observer,监听主线程对应的RunLoop,主要负责autoreleasepool的创建和销毁。第一个Observer监听的是“即将进入RunLoop”状态,它的回调里会创建一个autoreleasepool,这个Observer的优先级最高,以此保证创建autoreleasepool发生在其它所有回调之前,这样我们项目里的autorelease对象就可以放在这个autoreleasepool里了。第二个Observer监听的是“线程即将进入休眠”和“刚刚退出RunLoop”两个状态,“线程即将进入休眠”时它的回调里会销毁旧autoreleasepool并创建新autoreleasepool,这样旧autoreleasepool里的autorelease对象就可以顺利执行一次release操作使得引用计数-1,而新产生的autorelease对象就可以放在这个新autoreleasepool里了,“刚刚退出RunLoop”时它的回调里会销毁autoreleasepool,这个Observer的优先级最低,以此保证销毁autoreleasepool发生在其它所有回调之后,这样我们项目里autorelease对象就可以顺利执行一次release操作使得引用计数-1。简单地说,我们可以把线程唤醒做事情到线程休眠看做是一次RunLoop循环,App运行过程中会有无数次RunLoop循环,而每一次RunLoop循环开始时系统都会创建一个autoreleasepool,并把这一次循环里的autorelease对象都放进去,然后等这一次RunLoop循环结束时统一让autorelease对象的引用计数-1。

Timer事件源

Timer事件源主要包括NSTimerCADisplayLinkperformSelector:afterDelay:等定时器触发的事件,这些事件也都是我们用代码写的。

  • NSTimer

我们都知道NSTimer是基于RunLoop实现的,所以我们得把NSTimer添加到RunLoop中它才会工作,那NSTimer是怎么基于RunLoop实现的呢?也就是说NSTimer的工作原理是怎样的呢?我们在创建NSTimer的时候都会给定一个时间间隔——也就是每隔多长时间就触发一下定时器的回调,那么当我们把NSTimer添加到RunLoop之后,RunLoop内部的处理是每执行一次do-while循环就记录一下本次循环用了多长时间,然后累加上之前的时间,如果时间大于等于我们设置的时间间隔了,就调用一下定时器的回调,否则就不调用,那比如说我们设置了NSTimer每搁1秒钟调用一次回调,假设RunLoop第一次do-while循环任务量较少(因为RunLoop不是专门用来处理定时器的,它还有Source0、Source1等很多事件需要处理,这些事件有可能很简单也有可能很繁重)只用了0.2s,第二次do-while循环用了0.3s,第三次do-while循环也用了0.3s,此时累计用了0.8s,第一次do-while循环任务量较多用了0.5s,那累计就是1.3s,也就是1.3s后才触发了定时器的回调,并不是1s,这也就是为什么我们说NSTimer有可能不准,所以要想定时器非常准时可以使用GCD定时器,GCD定时器是跟系统内核挂钩的,不依赖于RunLoop,所以非常准时,所以建议能用GCD定时器尽量用GCD定时器。

  • CADisplayLink

CADisplayLinkNSTimer的工作原理基本是一样的,只不过CADisplayLink的调用频率和屏幕的刷新频率一样,每1/60秒调用一次。

  • performSelector:afterDelay:

performSelector:afterDelay:,内部其实就是创建了一个NSTimer并添加到当前线程的RunLoop中。


三、RunLoop的运行流程


RunLoop的运行流程大概如下图:

RunLoop的运行流程大概如下伪代码:

// 选择DefaultMode进入RunLoop,
// 
// App一启动,会走main函数,
// main函数里面会调用UIApplicationMain函数,
// UIApplicationMain函数里面就调用该函数获取并启动了主线程对应的RunLoop。
void CFRunLoopRun() {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10);
}

// 选择指定的Mode进入RunLoop,也可以指定RunLoop的超时时间
// 
// 切换Mode时,系统就会调用这个方法来重新进入RunLoop
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds);
}


int CFRunLoopRunSpecific(runloop, modeName, seconds) {
   
    // 先根据modeName去查找Mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName);
    // 如果Mode里没有Source0/Source1/Observer/Timer,则直接返回,不进入RunLoop
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    
    // 1、通知Observers:即将进入RunLoop
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    __CFRunLoopRun(runloop, currentMode, seconds) {
        
        int retVal = 0;
        do { // do...while循环
            // 2、通知Observers:线程即将处理Timer事件
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            // 3、通知Observers:线程即将处理Source事件
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            
            
            // 4、处理Source0事件
            __CFRunLoopDoSources0(runloop, currentMode);
            // 5、判断有没有Source1事件
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg)) {
                
                // 如果有,就跳转到handle_msg去处理
                goto handle_msg;
            }
            
            
            // 6、如果Source0事件处理完了、而且没有Source1事件,Timer事件的时间点还没到,则通知Observers:线程即将进入休眠
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            // 7、线程休眠,等待被唤醒,这里是利用内核函数mach_msg实现的。线程进入休眠后,切换到内核态,会卡死在这个地方,因为线程不做任何事情,不占用任何CPU资源,仅仅是等待着被唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy) {
                mach_msg(msg, MACH_RCV_MSG, port);
            }
    
            
            // 8、通知Observers:线程被唤醒,切换到用户态
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            
        handle_msg: // 9、处理唤醒事件
            if (msg_is_timer) { // 如果是被Timer事件唤醒的
                
                // 则处理Timer事件
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } else if (msg_is_dispatch) { // 如果是被GCD dispatch到主线程的事件唤醒的
                
                // 则处理GCD dispatch到主线程的事件
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { // 如果是被Source1事件唤醒的
                
                // 则处理Source1事件
                __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
            }
            
            // 10、根据前面的执行结果,决定如何操作
            if (timeout) { // 如果RunLoop对应线程的休眠时间超过了超时时间
                
                // 则退出RunLoop
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) { // 如果RunLoop被强行终止了
                
                // 则退出RunLoop
                retVal = kCFRunLoopRunStopped;
            } if (__CFRunLoopModeIsEmpty(runloop, currentMode, previousMode)) { // 如果RunLoop当前Mode里没有Source0/Source1/Observer/Timer了
                
                // 则退出RunLoop
                retVal = kCFRunLoopRunFinished;
            }

            // 如果RunLoop没超时,也没被强行终止,当前Mode里也没空,则继续RunLoop
        } while (0 == retVal);
    }
    
    // 11、通知Observers:刚刚退出RunLoop
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopExit);
    
    return retVal;
}

比如说有这样一个问题:App启动、App运行过程中点击屏幕、App杀死,这个过程系统都发生了什么?

App一启动,会走main函数,main函数里面会调用UIApplicationMain函数,UIApplicationMain函数里面会获取并启动主线程对应的RunLoop;主线程处理完一些事件后,没事做了,就会进入休眠状态,而一旦此时我们点击屏幕,系统就会捕捉到这个点击事件做为Source1事件来唤醒主线程,并把点击事件包装为Source0事件处理;之后App杀死,主线程销毁,主线程对应的RunLoop也就销毁了。(也可以结合事件传递和事件响应来做一番回答)


四、RunLoop的实际应用


1、处理NSTimer不工作的问题

Timer有两种创建方式,一种是timerWithXXX,一种是scheduledWithXXX。它们的区别是:timerWithXXX只会创建一个Timer,不会把Timer添加到RunLoop中;scheduledWithXXX不仅会创建一个Timer,还会把Timer添加到RunLoop中,而且是添加到了DefaultMode下。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

所以如果你发现Timer不工作,首先看看是不是用了timerWithXXX的创建方式,如果是,那么你可以手动把Timer添加到RunLoop中,或者换成scheduledWithXXX的创建方式。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 不工作
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        NSLog(@"%d", count++);
    }];
}


- (void)viewDidLoad {
    [super viewDidLoad];

    // 工作
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        NSLog(@"%d", count++);
    }];
    // 把Timer添加到RunLoop中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:(NSDefaultRunLoopMode)];
}


- (void)viewDidLoad {
    [super viewDidLoad];

    // 工作
    static int count = 0;
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        NSLog(@"%d", count++);
    }];
}

如果你发现Timer仅仅是在界面滑动时不工作,那么你可以把Timer添加到CommonModes下,因为Timer默认是被添加到DefaultMode下,所以在TrackingMode下不工作。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 界面滑动和不滑动时,都可以工作
    static int count = 0;
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
        NSLog(@"%d", count++);
    }];
    // 添加到CommonModes下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:(NSRunLoopCommonModes)];
}

如果你是在子线程中使用Timer,Timer默认是不工作的,因为子线程的RunLoop没有启用,创建倒是创建了(把Timer添加到RunLoop时系统会创建),因此我们需要手动启用一下RunLoop。

// 不工作
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        // 用scheduledWithXXX创建定时器时,不仅会创建一个Timer,还会把Timer添加到RunLoop中,所以这个方法里获取了子线程的RunLoop了,也就是说子线程的RunLoop被创建了,就差启动
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"11");
        }];
    });
}


// 工作
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        // 用scheduledWithXXX创建定时器时,不仅会创建一个Timer,还会把Timer添加到RunLoop中,所以这个方法里获取了子线程的RunLoop了,也就是说子线程的RunLoop被创建了,就差启动
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"11");
        }];
        
        // 手动启用一下子线程的RunLoop
        [[NSRunLoop currentRunLoop] run];
    });
}

2、常驻线程

AFNetworking里就创建了一个子线程,并且让这个子线程一直活着,将来在某一刻时刻需要这个子线程做事情的时候就告诉它做事情。这样做的好处就是当你需要经常在子线程里做事情的时候可以节省线程的创建和销毁开销,如果你做完一个任务就销毁一个线程,做下一个任务又创建一个线程,做完又销毁,那这个过程是非常耗性能的,所以不如干脆让一个子线程一直存在于内存中。

我们知道线程一执行完任务,它的生命周期就结束了,生命周期一结束,这个线程就无法再使用了,即便它还存在于内存中,但它已经不能做事情了。所以我们说的常驻线程其实是指保住线程的生命周期,不让它结束,而不是保住线程一直存在于内存中,要想保住线程一直存在于内存中很简单啊,用强指针就可以了,而要想保住线程的生命周期就不能让线程执行完它的任务,那咱们任务里添加个while(1)死循环吧,可以是可以,但是这太占用CPU资源了吧,所以我们可以用RunLoop来实现常驻线程,即:

  • 获取(即创建)子线程的RunLoop
  • 往RunLoop中添加一个Source或Observer或Timer(通常我们选择添加Source,其它两个太重了犯不着),以保证RunLoop不会因没有Source、Observer、Timer而退出
  • 启动RunLoop
  • 而如果想要结束常驻线程,则可以在适当的时机移除掉RunLoop里的Source
@interface ViewController ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建一个线程并启动
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
    [self.thread start];
}

- (void)threadAction {
    
    NSLog(@"threadAction:%@", [NSThread currentThread]);

    // 获取(即创建)子线程的RunLoop
    // 往RunLoop中添加一个Source或Observer或Timer(通常我们选择添加Source,其它两个太重了犯不着),以保证RunLoop不会因没有Source、Observer、Timer而退出
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    // 启动RunLoop
    [[NSRunLoop currentRunLoop] run];
}

@end
// 假设我们要在点击屏幕的时候停掉线程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 千万不能直接这样移除,因为这样[NSRunLoop currentRunLoop]获取的是主线程的RunLoop,而不是子线程的RnuLoop
//    [[NSRunLoop currentRunLoop] removePort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    
    // 一定要在子线程self.thread里移除
    [self performSelector:@selector(removePort) onThread:self.thread withObject:nil waitUntilDone:YES];
}

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

推荐阅读更多精彩内容