关于iOS多线程--这些是你必须知道的

  1. pthread
  2. NSThread
  3. GCD
    1. 同步、异步、并发、串行讲解
    2. 创建队列的几种方式
    3. 栅栏函数
    4. 队列组
    5. GCD快速迭代
  4. NSOperation和NSOperationQueue
    1. NSInvocationOperation和NSBlockOperation
    2. NSOperationQueue
    3. 任务依赖
  5. GCD和NSOperation的比较
  6. 多线程的安全隐患

关于多线程,在 iOS 中目前有 4 套方案,他们分别是:


下面我们分别来为大家一一介绍上述方案:

方案一:pthread

#import <pthread.h>


      //创建线程对象
        pthread_t thread = NULL;
        
        //传递的参数
        id str = @"i'm pthread param";
        
        //创建线程
        /* 参数一:线程对象 传递线程对象的地址
           参数二:线程属性 包括线程的优先级等
           参数三:子线程需要执行的方法
           参数四:需要传递的参数
         */
        int result = pthread_create(&thread, NULL, operate, (__bridge void *)(str));
        if (result == 0) {
            NSLog(@"创建线程 OK");
        } else {
            NSLog(@"创建线程失败 %d", result);
        }
        //手动把当前线程结束掉
        // pthread_detach:设置子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
        pthread_detach(thread);
void *operate(void *params){
    NSString *str = (__bridge NSString *)(params);
    
    NSLog(@"%@ - %@", [NSThread currentThread], str);
    
    return NULL;
}

方案二:NSThread

  • 先创建线程类,再启动
 // 创建
  NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];

  // 启动
  [thread start];
  • 创建后立即启动
 [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];

方案三:GCD

它是苹果为多核的并行运算提出的解决方案,所以它会自动合理地利用更多的CPU内核,最重要的是它会自动管理线程的生命周期(比如创建线程、调度任务、销毁线程)

1. 同步、异步、并发、串行讲解
GCD中有2个用来执行任务的函数

用同步的方式执行任务

//queue:队列  block:任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

用异步的方式执行任务

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

容易混淆的术语

有4个术语比较容易混淆:同步异步并发串行

同步和异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力(但是不一定能够开启新线程,比如异步在主队列中执行任务)

并发和串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务

各种队列的执行效果

注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

2. 创建队列的几种方式

  • 主队列: 它是一个特殊的 串行队列, 任何需要刷新 UI 的工作都要在主队列执行。
 dispatch_queue_t queue = dispatch_get_main_queue();
  • 自定义队列: 自己可以创建 串行队列, 也可以创建 并行队列
  //串行队列
  dispatch_queue_t queue = dispatch_queue_create("test1", NULL);
  dispatch_queue_t queue = dispatch_queue_create("test2", DISPATCH_QUEUE_SERIAL);

  //并行队列
  dispatch_queue_t queue = dispatch_queue_create("test3", DISPATCH_QUEUE_CONCURRENT);
  • 全局并行队列
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

3. 验证一下所学知识
下面我们来看看下面几段代码,猜一猜运行之后结果是个啥子嘛。。。

考题一:

    NSLog(@"任务一");
    dispatch_sync(dispatch_get_main_queue(), ^{
         NSLog(@"任务二");
    });
    NSLog(@"任务三");

额。。。知道结果了么?下面我们来揭晓答案

执行结果:


为什么会这样呢?
因为同步任务会阻塞当前线程,然后把 Block 中的任务放到主队列中执行,队列是FIFO,所以Block中的任务只有等到dispatch_sync执行完毕后才会执行,但是dispatch_sync要想执行完成必须Block中的任务执行完毕后才会结束.这就是非常经典的死锁现象.

为什么dispatch_sync在主线程会死锁

考题二:

        dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
        NSLog(@"任务一");
        dispatch_async(queue, ^{
            NSLog(@"任务二");
            dispatch_sync(queue, ^{
                NSLog(@"任务三");
            });
            NSLog(@"任务四");
        });
        NSLog(@"任务五");

看了考题一的分析 我相信考题二难不住你的,我们来看看打印结果:


其实原因跟上一个例子我们分析的原因类似,记住这句结论就好:

注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

3. 栅栏函数
在项目中有很多场景需要控制任务的执行顺序,比如需要等任务A, 任务B, 任务C都完成后(其中A, B, C没有顺序要求), 才进行下一步的处理任务, 可以使用 dispatch_group很方便的完成 (也可以使用栅栏函数)
如果上面的A, B, C任务顺序也有顺序要求呢? 必须A任务完成后, 才能进行B任务, B完成后才进行C任务, 这时我们就需要用到栅栏函数

dispatch_barrier_async:在进程管理中起到一个栅栏的作用,该函数需要同dispatch_queue_create函数生成的并发队列一起使用才能生效。

第一种情况
A, B, C任务完成之后(A, B, C无顺序要求), 进行任务D
1.使用dispatch_barrier

        dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);

        dispatch_async(queue, ^{
            NSLog(@"开始任务A");
            [NSThread sleepForTimeInterval:1];
            NSLog(@"任务A done.");
        });
        dispatch_async(queue, ^{
            NSLog(@"开始任务B");
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"任务B done.");
        });
        dispatch_async(queue, ^{
            NSLog(@"开始任务C");
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"任务C done.");
        });

        dispatch_barrier_async(queue, ^{
            NSLog(@"----------> barrier <----------");
        });

        dispatch_async(queue, ^{
            NSLog(@"开始任务D");
        });

打印结果:

开始任务C
开始任务A
开始任务B
任务C done.
任务B done.
任务A done.
----------> barrier <----------
开始任务D

可以看出在执行完栅栏前面的操作之后才执行栅栏操作,然后再执行栅栏后边的操作。
如果不加barrier 函数, 输出如下:

开始任务B
开始任务A
开始任务D
开始任务C
任务C done.
任务B done.
任务A done.
dispatch_barrier_async和dispatch_barrier_sync使用区别:

dispatch_barrier_async和dispatch_barrier_sync是 GCD 中的两个方法。是不是和dispatch_async及dispatch_sync长得很像,就是多了一个barrier(译:栅栏)。
没错,除了有dispatch_async或dispatch_sync的作用外(是否阻塞当前线程),还有“栅栏”的效果。
意思就是,在该队列,以他们为界,待前面任务执行完成,再把自己内部的任务执行完,才会执行后面的任务。

知道和dispatch_async及dispatch_sync对应,就应该想到:
dispatch_barrier_async不阻塞当前线程,dispatch_barrier_async里面的任务异步执行。
dispatch_barrier_sync会阻塞当前线程,dispatch_barrier_sync里面的任务同步执行。

 dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQ", DISPATCH_QUEUE_CONCURRENT);

    //以下任务
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务1"); });
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务2"); });
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务3"); });
    dispatch_barrier_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"I am barrier");
    });
    NSLog(@"当前线程");
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务4"); });
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务5"); });

输出结果:


image.png
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQ", DISPATCH_QUEUE_CONCURRENT);

    //以下任务
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务1"); });
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务2"); });
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务3"); });
    dispatch_barrier_sync(concurrentQueue, ^{
        sleep(1);
        NSLog(@"I am barrier");
    });
    NSLog(@"当前线程");
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务4"); });
    dispatch_async(concurrentQueue, ^{ NSLog(@"任务5"); });

输出结果

2.使用 dispatch_group

      dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);

        dispatch_group_t group = dispatch_group_create();

        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"开始任务A");
            [NSThread sleepForTimeInterval:3];
            NSLog(@"任务A done.");
            dispatch_group_leave(group);
        });

        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"开始任务B");
            [NSThread sleepForTimeInterval:2];
            NSLog(@"任务B done.");
            dispatch_group_leave(group);
        });

        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"开始任务C");
            [NSThread sleepForTimeInterval:1];
            NSLog(@"任务C done.");
            dispatch_group_leave(group);
        });

        dispatch_group_notify(group, queue, ^{
            NSLog(@"开始任务D");
        });

输出如下:

开始任务C
开始任务B
开始任务A
任务C done.
任务B done.
任务A done.
开始任务D

第二种情况,任务依赖
A, B, C任务完成之后(A, B, C顺序要求依次执行), 进行任务D

        dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);

        dispatch_barrier_async(queue, ^{
            NSLog(@"开始任务A");
            [NSThread sleepForTimeInterval:1];
            NSLog(@"任务A done.");
        });
        dispatch_barrier_async(queue, ^{
            NSLog(@"开始任务B");
            [NSThread sleepForTimeInterval:0.5];
            NSLog(@"任务B done.");
        });
        dispatch_barrier_async(queue, ^{
            NSLog(@"开始任务C");
            [NSThread sleepForTimeInterval:0.2];
            NSLog(@"任务C done.");
        });

        dispatch_barrier_async(queue, ^{
            NSLog(@"开始任务D");
        });

输出如下:

开始任务A
任务A done.
开始任务B
任务B done.
开始任务C
任务C done.
开始任务D

在每个网络请求开始前使用 dispatch_group_enter来进行标识,网络请求有回调后使用dispatch_group_leave来进行标识,这样就能保证group_notify在所有网络请求都有回调之后才调用

5. GCD快速迭代
我们知道for循环中的代码是串行执行的,如果此时我们有一系列的耗时操作需要执行,此时我们可以使用Dispatch_apply函数,他可以异步执行,同时可以利用多核优势,完美替代for循环。

  dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSLog(@"%zd = %@",index,[NSThread currentThread]);
    });

执行结果如下:


可以看到上述循环是在多个线程中并发执行的。

6. 考题:猜测打印结果

考题一:

- (void)test{
    NSLog(@"任务B");
}
- (void)viewDidLoad {
    [super viewDidLoad];    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务A");
        [self performSelector:@selector(test) withObject:nil afterDelay:1.0];
         NSLog(@"任务C");
    });
}

知道结果了么?


我们来看看打印结果:


为什么只输出了任务A和任务C而没有任务B呢?其实这里涉及到了RunLoop的知识,因为performSelector:withObject:afterDelay:的本质是向RunLoop中添加定时器,而子线程中默认是没有开启RunLoop的,所以这里我们需要稍微改动下代码,如下;

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务A");
       
        [self performSelector:@selector(hahha) withObject:nil afterDelay:1.0];
        
        NSLog(@"任务C");
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    });
}

关于RunLoop 有兴趣的朋友可以看看我的这篇文章: RunLoop的使用

考题二:

- (void)test{
    NSLog(@"任务B");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"任务A");
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

执行结果:

[73860:11959832] 任务A
[73860:11959410] *** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'

因为我们在执行完[thread start];的时候执行任务A,此时线程就被销毁了,如果我们要在thread线程中执行test方法需要保住该线程的命,即线程保活,代码需要修改如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"任务A");
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

方案四:NSOperation和NSOperationQueue

NSOperation 是苹果公司对 GCD面向对象的封装,所以使用起来非常方便。
NSOperationNSOperationQueue分别对应 GCD 的 任务 和 队列 。

使用步骤大致如下:

  1. 先将需要执行的操作封装到一个NSOperation对象中
  2. 然后将NSOperation对象添加到NSOperationQueue中
  3. 系统会⾃动将NSOperationQueue中的NSOperation取出来
  4. 将取出的NSOperation封装的操作放到⼀条新线程中执⾏

1. 任务

NSOperation只是一个抽象类,所以不能封装任务。
但是我们可以使用它的两个子类对象:NSInvocationOperationNSBlockOperation

  • NSInvocationOperation : 需要传入一个方法名。
 //1.创建NSInvocationOperation对象
  NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

  //2.开始执行
  [operation start];

打印结果:


其实等价于[self run];在主线程中执行。

如果我们想让任务在子线程中执行,我们需要创建一个NSOperationQueue,如下:

     // 创建队列
     NSOperationQueue *queue = [[NSOperationQueue alloc] init];  
       // 创建操作
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
       // 添加操作到队列中,会自动异步执行
    [queue addOperation:operation];

打印结果:


注意:操作对象默认在主线程中执行,只有将NSOperation放到一个 NSOperationQueue中,才会异步执行操作

  • NSBlockOperation:用来并发的执行一个或者多个Block对象。

注意:addExecutionBlock:该方法只要NSBlockOperation封装的操作数 > 1,就会异步执行操作

       //1.创建NSBlockOperation对象
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
    }];
    
    //2.开始任务
    [operation start];

打印结果:

<NSThread: 0x604000074780>{number = 1, name = main}

addExecutionBlock方式添加多个任务:

    NSBlockOperation *operation = [[NSBlockOperation alloc] init];
    
    [operation addExecutionBlock:^{
        //---下载图片----1---<NSThread: 0x600000260fc0>{number = 1, name = main}
        NSLog(@"---下载图片----1---%@", [NSThread currentThread]);
    }];//这种方式只有第一个是主线程,其余都是子线程
    
    [operation addExecutionBlock:^{
        //---下载图片----2---<NSThread: 0x600000263f40>{number = 3, name = (null)}
        NSLog(@"---下载图片----2---%@", [NSThread currentThread]);
    }];
    
    [operation addExecutionBlock:^{
        //---下载图片----3---<NSThread: 0x60800026c440>{number = 4, name = (null)}
        NSLog(@"---下载图片----3---%@", [NSThread currentThread]);
    }];
    
    [operation start];

2. 队列

通过上面的介绍我们知道调用NSOperation对象的start()方法可以启动任务,但是这样做他们默认是 同步执行 的。即使是addExecutionBlock方法,也会在 当前线程和其他线程 中执行,也就是说还是会占用当前线程。此时我们就需要用到NSOperationQueue了。
只要任务添加到队列,便会自动调用任务的start()方法

      //1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"---下载图片----1---%@", [NSThread currentThread]);
    }];
    [operation addExecutionBlock:^{
        NSLog(@"---下载图片----2---%@", [NSThread currentThread]);
    }];
    
    // 2.添加操作到队列中(自动异步执行)
    [queue addOperation:operation];

打印结果:

任务依赖
需求:此时有 3 个任务,这三个任务因为比较耗时,所以需要异步并发执行。
任务一: 从服务器上下载一张图片
任务二:给这张图片加个水印
任务三:把图片返回给服务器。

这时候就需要控制任务的执行顺序了

//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下载图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"打水印   - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"上传图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//4.设置依赖
[operation2 addDependency:operation1];      //任务二依赖任务一
[operation3 addDependency:operation2];      //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

打印结果

  • 注意:不能添加相互依赖,比如 A依赖B,B又依赖A,否则会造成死锁
1. 从其他线程回到主线程的方法

我们都知道在其他线程操作完成后必须到主线程更新UI。所以,介绍完所有的多线程方案后,我们来看看有哪些方法可以回到主线程。

  • NSThread
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
  • GCD
dispatch_async(dispatch_get_main_queue(), ^{

});

  • NSOperationQueue
[[NSOperationQueue mainQueue] addOperationWithBlock:^{

}];

2. 延迟执行方案

公用延迟执行方法

- (void)delayMethod{
    NSLog(@"delayMethodEnd");
}

线程阻塞式

1.NSThread线程的sleep

[NSThread sleepForTimeInterval:2.0];

此方法是一种阻塞执行方式,建议放在子线程中执行,否则会卡住界面。但有时还是需要阻塞执行,比如进入欢迎界面需要沉睡2秒才进入主界面时。

非阻塞执行方式

  1. performSelector
[self performSelector:@selector(delayMethod) withObject:nil afterDelay:2.0];
  1. NSTimer定时器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];
  1. GCD的方式
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0  NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
    [weakSelf delayMethod];
});

此方法可以在参数中选择执行的线程,是一种非阻塞执行方式

GCD和NSOperation的比较

GCD 和 NSOperation的区别主要表现在以下几方面:

1、GCD是一套 C 语言API,执行和操作简单高效,NSOperation底层也通过GCD实现,这是他们之间最本质的区别,因此如果希望自定义任务,建议使用NSOperation;
2、依赖关系,NSOpeartion可以通过addDependency来添加任务的依赖,GCD需要添加依赖只能通过dispatch_barrier_async
3、KVO(键值对观察),可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)对此GCD无法通过KVO进行判断;
4、优先级,NSOpeartion可以设置queuePriority来设置优先级,跳转任务的执行先后顺序,GCD只能设置队列的优先级,且任务是根据先进先出FIFO的原则来执行的,不能设置任务的优先级。
5、继承,NSOperation是一个抽象类。实际开发中常用的是它的两个子类:NSInvocationOperation和NSBlockOperation,同样我们可以自定义NSOperation,GCD执行任务可以自由组装,没有继承那么高的代码复用度;
6、效率,直接使用GCD效率确实会更高效,NSOperation会多一点开销,但是通过NSOperation可以获得依赖,优先级,继承,键值对观察这些优势,相对于多的那么一点开销确实很划算,鱼和熊掌不可得兼,取舍在于开发者自己;
7、NSOperation可以设置暂停,挂起等操作,可以随时取消准备执行的任务(已经在执行的不能取消),GCD没法停止已经加入queue 的 block(虽然也能实现,但是需要很复杂的代码)
基于GCD简单高效,更强的执行能力,操作不太复杂的时候,优先选用GCD;而比较复杂的任务可以自己通过NSOperation实现。
8、NSOperation可以设置最大任务数,

多线程的安全隐患

当多个线程同时访问同一个资源时,很容易引发数据错乱和数据安全问题,比如下图:


image

那么我们该如何去解决这个问题呢?
我们可以使用线程同步技术。所谓同步,就是协同步调,按预定的先后次序进行。常见的线程同步技术就是加锁

image

关于锁的实现方案 网上有很多,这里我就不再列举了,可以参考:
iOS中保证线程安全的几种方式与性能对比
iOS 常见知识点(三):Lock
深入理解iOS开发中的锁

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

推荐阅读更多精彩内容