什么是RunLoop,RunLoop有哪些使用场景

每次面试,Runloop这个概念几乎是必问的。所以,还是写点东西出来做个记录,同时也加深一下自己的记忆。

一.什么是RunLoop

        RunLoop 既运行循环机制,在应用级别考虑,应用程序中所有的任务处理(用户交互事件、网络请求回调数据接收等)都是在线程中执行,一般来讲一个线程一次只能执行一个任务,执行完线程销毁(OC 中子线程异步销毁,主线程除外),但是问题来了如何让线程保活呢?并且如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒呢?RunLoop 就是解决这些问题的,它所做的一切都是基于线程,可以说是为线程而生

OSX/iOS 系统中,提供的 两个 RunLoop 对象:CFRunLoopRef是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,且其是开源的,开源下载地址(CoreFoundation 源码)。NSRunLoop是基于 CFRunLoopRef 的封装,提供了面向对象的 API,并不开源。分析 CoreFoundation 库内的 RunLoop 源码分析。CoreFoundation 源码中 CFRunLoop 关于 RunLoop 的有五个类:

CFRunLoopRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

CFRunLoopModeRef

这五大类是如何通过C语言封装成的呢 ?

typedefstruct__CFRunLoop*CFRunLoopRef;

typedefstruct__CFRunLoopSource*CFRunLoopSourceRef;

typedefstruct__CFRunLoopObserver*CFRunLoopObserverRef;

typedefstruct__CFRunLoopTimer*CFRunLoopTimerRef;      

由 CFRunLoop.h文件查看到如上源码

typedefstruct__CFRunLoopMode*CFRunLoopModeRef;

由 CFRunLoopModeRef 类于CFRunLoop.c 第 521 行可找到,下面具体分析其各种的源码结构

RunLoop

CFRunLoop.c文件中 第 636 行:

struct__CFRunLoop{

        CFRuntimeBase _base;

        pthread_mutex_t_lock;/* locked for accessing mode list */

        __CFPort _wakeUpPort;// used for CFRunLoopWakeUp  内核向该端口发送消息可以唤醒

        runloopBoolean _unused;

        volatile_per_run_data *_perRunData;// reset for runs of the run loop

        pthread_t_pthread;//RunLoop对应的线程

        uint32_t_winthread;   

        CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode

        CFMutableSetRef _commonModeItems;//等同NSMutableSet 存储所有commonMode的item(source、timer、observer)

        CFRunLoopModeRef _currentMode;//当前运行的mode

        CFMutableSetRef _modes;//存储的是CFRunLoopModeRef

        struct_block_item*_blocks_head;//doblocks的时候用到

        struct_block_item*_blocks_tail;

        CFTypeRef _counterpart;

};

由源码可知,一个RunLoop对象,主要包含了对应的一个线程,若干个 Mode,若干个 commonMode,还有一个当前运行的    Mode(_currentMode)。

我们并不能去创建这个 CFRunLoopRef(为什么呢?),而是通过如下方法去获取当前线程的 RunLoop:

// CFRunLoop.h 中73行源码

CF_EXPORT  CFRunLoopRef  CFRunLoopGetCurrent(void);// 获取当前线程runloop

CF_EXPORT  CFRunLoopRef  CFRunLoopGetMain(void);// 获取主线程 runloop

具体使用姿势:

// 获取当前Runloop

CFRunLoopRef  runloop =CFRunLoopGetCurrent();

RunLoopMode - 运行模式

CFRunLoop.c文件中 第 523 行:

struct__CFRunLoopMode{

        CFRuntimeBase    _base;

        pthread_mutex_t   _lock;/* must have the run loop locked before locking this */

        CFStringRef   _name;//mode名称

        Boolean   _stopped;//mode是否被终止

        char  _padding[3];

        //几种事件

        CFMutableSetRef   _sources0;//sources0

        CFMutableSetRef   _sources1;//sources1

        CFMutableArrayRef   _observers;//通知

        CFMutableArrayRef   _timers;//定时器

        CFMutableDictionaryRef   _portToV1SourceMap;//字典  key是mach_port_t,value是CFRunLoopSourceRef

        __CFPortSet   _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中

        CFIndex   _observerMask;

#ifUSE_DISPATCH_SOURCE_FOR_TIMERS

        dispatch_source_t   _timerSource;

        dispatch_queue_t   _queue;   

        Boolean   _timerFired;// set to true by the source when a timer has fired

        Boolean   _dispatchTimerArmed;

#end

if#ifUSE_MK_TIMER_TOOmach_port_t   _timerPort;   

        Boolean   _mkTimerArmed;

#endif

#ifDEPLOYMENT_TARGET_WINDOWS

        DWORD    _msgQMask;

        void   (*_msgPump)(void);

#endif

        uint64_t   _timerSoftDeadline;/* TSR */

        uint64_t   _timerHardDeadline;/* TSR */

};

一个 CFRunLoopMode 对象有一个 name ,若干 source0、source1、timer、observer和若干port,所有事件都是由 Mode 在管理,而 一个线程下的 RunLoop 管理着若干个 Mode (ModeItems)。在一个线程中 runloop 保活线程并且在 指定 Mode下使其接受处理事件,又在每次 runloop 循环中(之前讲的死循环概念)进行 Mode间的切换。

Cocoa框架和Core Foundation框架中定义了五种 Mode (Guides and Sample Code )

Default 默认Mode,APP运行起来之后,主线程的RunLoop默认在该Mode下运行

NSDefaultRunLoopModeCocoa 框架

kCFRunLoopDefaultModeCore Foundation 框架

Event tracking 追踪触摸的手势,所有 UI 交互事件都运行在这个Mode下

UITrackingRunLoopMode(Cocoa)

Common modes 共有型 Model 含有上面两种Mode模式的意义

NSRunLoopCommonModes(Cocoa)

kCFRunLoopCommonModes(Core Foundation)

Connection 系统内核模式,系统调用事件发生会切换到相应模式下,开发者无法操作

GSEventReceiveRunLoopMode(Cocoa)

Modal 项目初始化模式,只会走一次

UIInitializationRunLoopMode(Cocoa)

RunLoop Source - 事件源

CFRunLoop.c文件中 第 943 行:

struct    __CFRunLoopSource {

        CFRuntimeBase    _base;   

        uint32_t     _bits;//用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理

        pthread_mutex_t     _lock;

        CFIndex_order;/* immutable */

        CFMutableBagRef    _runLoops;

        union{

                CFRunLoopSourceContextversion0;/* immutable, except invalidation */

                CFRunLoopSourceContext1version1;/* immutable, except invalidation */

                } _context;

};

事件源顾名思义事件的产生地,由上源码会发现  Source 分为两种:

Source0 非基于Port的

只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理 signal 状态,然后手动调 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

其作用范围是应用程序中事件,由App自己管理的UIEvent、CFSocket都是source0。以下是source0的结构体:

typedef    struct{

    CFIndex    version;

    void*  info;

    const    void*(*retain)(constvoid*info);

    void(*release)(constvoid*info);

    CFStringRef(*copyDescription)(constvoid*info);   

    Boolean (*equal)(constvoid*info1,constvoid*info2);

    CFHashCode(*hash)(constvoid*info);

    void(*schedule)(void*info,CFRunLoopRefrl,CFStringRefmode);

    void(*cancel)(void*info,CFRunLoopRefrl,CFStringRefmode);

    void(*perform)(void*info);

}CFRunLoopSourceContext;

Source1

包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。可以接收内核消息并触发回调,这种 Source 能主动唤醒 RunLoop 的线程。

其作用范围是由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体:

typedef    struct{

    CFIndex    version;

    void*  info;

    constvoid*(*retain)(constvoid*info);

    void(*release)(constvoid*info);

    CFStringRef(*copyDescription)(constvoid*info);   

    Boolean (*equal)(constvoid*info1,constvoid*info2);

    CFHashCode(*hash)(constvoid*info);

#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)

    mach_port_t (*getPort)(void*info);

    void*  (*perform)(void*msg,CFIndexsize,CFAllocatorRefallocator,void*info);

#else

    void*  (*getPort)(void*info);

    void(*perform)(void*info);

#endif

}CFRunLoopSourceContext1;

CFRunLoopObserver - 观察者

CFRunLoop.c文件中 第 981 行:

struct__CFRunLoopObserver {

    CFRuntimeBase _base;

    pthread_mutex_t _lock;

    CFRunLoopRef _runLoop;

    CFIndex _rlCount;

    CFOptionFlags _activities; /* immutable */

    CFIndex _order; /* immutable */

    CFRunLoopObserverCallBack _callout; /* immutable */

    CFRunLoopObserverContext _context; /* immutable, except invalidation */

};

Observer 是 mode 下数组保存 ,其都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。回调函数CFRunLoopObserverCallBack

CFRunLoopObserver 可以观察的状态有如下6种:

/* Run Loop Observer Activities */

typedefCF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

    kCFRunLoopEntry = (1UL <<0),

    kCFRunLoopBeforeTimers = (1UL <<1),

    kCFRunLoopBeforeSources = (1UL <<2),

    kCFRunLoopBeforeWaiting = (1UL <<5),

    kCFRunLoopAfterWaiting = (1UL <<6),

    kCFRunLoopExit = (1UL <<7),

    kCFRunLoopAllActivities =0x0FFFFFFFU

};

CFRunLoopTimer

CFRunLoop.c文件中 第 1049 行:

struct__CFRunLoopTimer {

    CFRuntimeBase _base;

    uint16_t _bits;

    pthread_mutex_t _lock;

    CFRunLoopRef _runLoop;

    CFMutableSetRef _rlModes;

    CFAbsoluteTime _nextFireDate;

    CFTimeInterval _interval; /* immutable */

    CFTimeInterval _tolerance;          /* mutable */

    uint64_t _fireTSR; /* TSR units */

    CFIndex _order; /* immutable */

    CFRunLoopTimerCallBack _callout; /* immutable */

    CFRunLoopTimerContext _context; /* immutable, except invalidation */

};

是基于时间的触发器,它和 NSTimer 是toll-free bridged的,可以相互转换的。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,可以在设定的时间点 RunLoop 会被唤醒并执行回调。

RunLoop 与线程的关系


苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

CFRunLoopRef CFRunLoopGetMain(void) {

    CHECK_FOR_FORK();

    staticCFRunLoopRef __main =NULL;// no retain needed

    if(!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());// no CAS needed

    return__main;

}

CFRunLoopRef CFRunLoopGetCurrent(void) {

    CHECK_FOR_FORK();

    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);

    if(rl)returnrl;

    return_CFRunLoopGet0(pthread_self());

}

获取RunLoop函数

staticCFMutableDictionaryRef __CFRunLoops =NULL;

staticCFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation

// t==0 is a synonym for "main thread" that always works

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {

    if(pthread_equal(t, kNilPthreadT)) {

t = pthread_main_thread_np();

    }

    __CFSpinLock(&loopsLock);

    if(!__CFRunLoops) {

        __CFSpinUnlock(&loopsLock);

CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault,0,NULL, &kCFTypeDictionaryValueCallBacks);

CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

if(!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void*volatile*)&__CFRunLoops)) {

    CFRelease(dict);

}

CFRelease(mainLoop);

        __CFSpinLock(&loopsLock);

    }

    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

    __CFSpinUnlock(&loopsLock);

    if(!loop) {

CFRunLoopRef newLoop = __CFRunLoopCreate(t);

        __CFSpinLock(&loopsLock);

loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

if(!loop) {

    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);

    loop = newLoop;

}

        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it

        __CFSpinUnlock(&loopsLock);

CFRelease(newLoop);

    }

    if(pthread_equal(t, pthread_self())) {

        _CFSetTSD(__CFTSDKeyRunLoop, (void*)loop,NULL);

        if(0== _CFGetTSD(__CFTSDKeyRunLoopCntr)) {

            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void*)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void(*)(void*))__CFFinalizeRunLoop);

        }

    }

    returnloop;

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

AutoreleasePool和RunLoop


App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件:

BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;

Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用mach port 转发给需要的App进程

随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发,此过程是Source0 完成的。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面刷新

当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。

苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面

定时器

当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行。

所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合。

无论是单次执行的NSTimer还是重复执行的NSTimer都不是准时的,这与当前NSTimer所处的线程有很大的关系,如果NSTimer当前所处的线程正在进行大数据处理(假设为一个大循环),NSTimer本次执行会等到这个大数据处理完毕之后才会继续执行

这期间有可能会错过很多次NSTimer的循环周期,但是NSTimer并不会将前面错过的执行次数在后面都执行一遍,而是继续执行后面的循环,也就是在一个循环周期内只会执行一次循环。

无论循环延迟的多离谱,循环间隔都不会发生变化,在进行完大数据处理之后,有可能会立即执行一次NSTimer循环,但是后面的循环间隔始终和第一次添加循环时的间隔相同。这个事件是怎么执行的?并且为什么有的时候会延迟?为什么子线程中创建的Timer并不执行?

首先,在进入循环开始以后,就要处理source0事件,处理后检测一下source1端口是否有消息,如果一个Timer的时间间隔刚好到了则此处有可能会得到一个消息,则runLoop直接跳转至端口激活处从而去处理Timer事件。

第二,为什么会延迟?我们知道,两次端口事件是在两个runLoop循环中分别执行的。比如Timer的时间间隔为1秒,在第一次Timer回调结束后,在很短时间内立即进入runLoop的下一次循环,这次并不是Timer回调并且是一个计算量非常大的任务,计算时间超过了1秒,那么runLoop的第二个循环就要执行很久,无法进入下一个循环等待有可能即将到来的Timer第二次回调的信号,所以Timer第二次回调就会推迟了。

第三,为什么在子线程中创建的Timer并且提交到当前runLoop中并不会运行?这还是要从runLoop的获取函数中看,当调用currentRunLoop的时候会取当前线程对应的runLoop,而首次是取不到的,则会创建一个新的runLoop。但是!这个runLoop并没有run。就是没有开启

- (void)applicationDidBecomeActive:(UIApplication*)application{

// NSThread 创建一个子线程   

 [NSThreaddetachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:selfwithObject:nil];

}

// 测试把timer加到不运行的runloop上的情况

- (void)testTimerSheduleToRunloop1

{

        NSLog(@"Test timer shedult to a non-running runloop");

         SvTestObject *testObject4 = [[SvTestObject alloc] init];

        NSTimer*timer = [[NSTimeralloc] initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:1] interval:1target:testObject4 selector:@selector(timerAction:) userInfo:nilrepeats:NO];

         [[NSRunLoop    currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

        // 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去//

        NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);

        // 下面一行, 该线程的runloop就会运行起来,timer才会起作用

        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];

        NSLog(@"invoke release to testObject4");

}

- (void)applicationWillResignActive:(UIApplication*)application

{

    NSLog(@"SvTimerSample Will resign Avtive!");

}

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

Runloop 开发中运用

滑动与图片刷新:

当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

GCD

实际上 RunLoop 底层也会用到 GCD 的东西,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。





友情链接:

源码级 RunLoop 剖析 - 简书

iOS - RunLoop 底层源码详解及具体运用 - 简书

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

推荐阅读更多精彩内容