NSOperation笔记

NSOperation

NSOperation表示了一个独立的任务。NSOperation是一个抽象类。不能直接使用。你可以使用系统定义好的它的两个字类NSInvocationOperationNSBlockOperation。也可以自己定义一个继承自NSOperation的类使用。
很多执行任务类型的案例都很好的运用了NSOperation,包括AFNetworking,SDWebIamge,图像压缩,自然语言处理或者其他很多需要返回处理后数据的、可重复的、结构化的、相对长时间运行的任务。
NSOperationQueue控制着NSOperation任务的执行。通常是吧一个NSOperation对象添加到NSOperationQueue中,NSOperationQueue会直接在子线程里执行添加到其中的NSOperation任务。
如果不使用NSOperationQueue,也可以调用NSOperation的start方法来执行任务。
直接通过调用 start 方法来执行一个 operation ,但是这种方式并不能保证 operation 是异步执行的。

isAsynchronous

NSOperation 类的 isAsynchronous 方法的返回值标识了一个 operation 相对于调用它的 start 方法的线程来说是否是异步执行的。在默认情况下,isAsynchronous 方法的返回值是 NO ,也就是说会阻塞调用它的 start 方法的线程。
如果想要调用它的 start方法后异步执行任务,需要编写一些额外的代码来让这个 operation 异步执行。仅仅重写isAsynchronous返回YES是不会让任务异步执行的,还需要自己创建新的线程来保证任务的异步执行。更多信息,可以看Asynchronous Versus Synchronous Operations
系统提供的NSInvocationOperationNSBlockOperation这两个类的isAsynchronous都是NO,也就是这两个类都会阻塞调用start方法的线程。

Asynchronous Versus Synchronous Operations

如果使用start方法来开启一个任务,默认是同步的。只需要在子类里重写main方法,在这个方法里写上你需要执行的任务。
如果要用start方法开启一个异步地任务,则至少需要重写下面的方法:

  • start:start方法会开始执行NSOperation的任务,这个方法的实现会更新isExecuting的状态并且会调用NSOperation的main方法。如果NSOperation已经取消了或者已经完成了,这个方法会直接返回,不会调用面方法。如果这个NSOperation正在执行或者还没有准备好执行,这个方法会抛出一个NSInvalidArgumentException异常。
    如果这个NSOperation依赖的其他NSOperation尚未完成,则这个NSOperation的isReady是NO。重写这个方法不能调用super。这个方法里还需要追踪NSOperation的状态。它应分别手动地为isExecuting和isFinished的keypath生成KVO通知。手动的生成KVO通知
    NSOperation已经添加到NSOperationQueue后在调用这个方法或者先调用了这个方法然后再把NSOperation添加到NSOperationQueue都是错误的。
    在并发操作中,stat方法负责以异步地方式开启NSOperation。无论是创建新的线程还是使用异步函数,都要在start方法里完成。
    在开始NSOperation时,start方法里应该更新isExecuting这个属性来报告NSOperation的状态,可以通过手动发一个isExecuting key path KVO通知。
    当完成或取消NSOperation时,需要对isExecuting和isFinished手动生成KVO通知。在取消NSOperation的情况下,即使NSOperation没有完成任务,人需要更新isFinished。因为在队列中的NSOperation的状态完成的情况下才会被移除。
    注意:
    在start的方法里任何时候都不应该调用super。在定义并发的NSOperation时,需要提供默认的start方法提供的行为,其中包括开启任务和生成KVO通知。start方法话还应该见检查在任务开启前NSOperation是否已经被取消了。Cancel Command

  • isAsynchronous:这个只默认是NO,这个值本身不会影响任务同步还是异步。

  • isExecuting:自定义并发的NSOperation时,必须要重写这个属性。当你改变这个属性时,需要手动生成kvo通知。

  • isFinished:自定义并发的NSOperation时,必须要重写这个属性。当你改变这个属性时,需要手动生成kvo通知。

NSOperation State

和NSOperation的状态关联的属性:

  • isReady:isReady表示NSOperation什么时候准备好执行。它的值是YES表示已经准备好执行,如果NSOperation有其他依赖的NSOperation还没有完成,则isReady的值是NO。
    大部分情况下,你不需要自己管理这个属性,如果你自定义的operation的准备状态是由依赖操作意外的因素决定的,你可以自己实现这个属性。
  • isExecuting:这个属性表示NSOperation是否正在执行任务。operation正在执行任务时是YES,否则是NO。
  • isFinished:这个值是YES表示operation的任务完成了或者operation被取消了。当这个值是YES是,opearation queue才会把operation移除队列,operation的依赖关系也会被清除。
  • isCancelled:这个属性默认值是NO,调用cancel方法来设置这个值是YES。一旦operation取消了,operation的状态要设置为完成。取消operation并不会阻止operation执行代码。如果方法返回YES,operation需要定期调用此方法并停止。
    在完成任务之前,你应该始终检查这个属性,通常在自定义的main方法开始时检查。

cancel operation

cancel方法调用后会把isCancelled设置为YES。这个方法不会强制停止operation的代码,它仅仅会更新operation内部的标志来反映状态的变化。如果operation已经完成执行了,这个方法不起作用。在operaton queue里的还未开始执行任务的operation调用cancel方法后,可以比平时更快的把operation从operation queue里移除。如果取消不在opeation queue中的operation,则此方法会立即将operation标记为完成状态。
NSOperation的默认实现包括对取消的检查,如果在start方法调用前取消了operation,start方法会直接退出。推测start的默认实现里应该是先判断isCancelled,如果是YES就直接return了。
在macOS10.6以后,如果operation有依赖的其他operation还未完成,此时operation调用cancel方法,会忽略掉依赖。如果operation是在队列中的,这允许队列调用operation的start方法,以在不调用opeartion的main方法的情况下从队列中删除操作。

NSInvocationOperation

This class implements a non-concurrent operation.

NSInvocationOperation是非并发的operation。

NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil];
[operation start];

- (void)test{
    NSLog(@"%@", [NSThread currentThread]);
}

log:

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

可以看出任务是在主线程执行的,并没有生成新的线程。

来看一下取消的效果:

NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil];
[operation cancel];
NSLog(@"isCancelled == %@, isFinished == %@", operation.isCancelled?@"YES":@"NO",  operation.isFinished?@"YES":@"NO");
[operation start];
NSLog(@"isFinished == %@", operation.isFinished?@"YES":@"NO");

- (void)test{
    NSLog(@"%@", [NSThread currentThread]);
}

log:

isCancelled == YES, isFinished == NO
isFinished == YES

可以看出,operation调用cancel后,isCancelled(默认是NO)会立即变成YES,而此时isFinished依然是NO。当operation调用start后,并没有调用test方法,并且把isFinished设置成了YES。

NSBlockOperation

An operation that manages the concurrent execution of one or more blocks.
When executing more than one block, the operation itself is considered finished only when all blocks have finished executing.

虽然文档里提到了NSBlockOperation是管理block并发执行的,实际block的并行执行和block的数量有关。

NSBlockOperation *p = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block1 == %@", [NSThread currentThread]);
}];
NSLog(@"isAsynchronous == %@", p.isAsynchronous?@"YES":@"NO");
[p start];

log:

isAsynchronous == NO
block1 == <NSThread: 0x604000066c40>{number = 1, name = main}

可以看出,block是在主线程里执行的,并没有生成新的线程。
如果再添加一个一个block的话:

 NSBlockOperation *p = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block1  %@", [NSThread currentThread]);
 }];
 [p addExecutionBlock:^{
        sleep(3);
        NSLog(@"block2 %@", [NSThread currentThread]);
 }];
 NSLog(@"isAsynchronous == %@", p.isAsynchronous?@"YES":@"NO");
 [p start];
 NSLog(@"operation finish");

log:

isAsynchronous == NO
block1  <NSThread: 0x604000261ac0>{number = 1, name = main}
block2 <NSThread: 0x600000461a00>{number = 3, name = (null)}
operation finish

isAsynchronous == NO,虽然两个block是并发执行的,但是start方法会阻塞调用start的线程。blcok1是在主线程中完成的,block2则是在一个新的线程中完成的,并且NSBlockOperation在调用start方法后会阻塞当前线程,等到所有的block执行完后才会继续执行后面的代码。参考
NSBlockOperation的cancel和NSInvocationOperation是一样的,调用cancel后把isCancelled设置为YES,然后再调用start把isFinished设置为YES。
总结一下,就是NSOperation的cancel方法需要把isCancelled属性设置为YES。start方法里需要判断isCancelled属性,如果是YES就不执行任务,并且把isFinished设置为YES。

The default implementation of this method updates the execution state of the operation and calls the receiver’s main method....if the receiver was cancelled or is already finished, this method simply returns without calling main.
上面是start方法的描述,提到了start方法会调用main方法,如果operation的isCancelled或isFinished是YES,则start方法会直接返回不会调用main方法。

自定义operation要考虑定义的operation是concurrent还是non-concurrent,这里的并发是相对于调用start方法的线程。

non-concurrent operations

For non-concurrent operations, you typically override only one method:

  • main
    文档里提到了,要自定义一个非并行的operation,
    只需要重写main方法即可。
@interface OQNonConcurrentOperation : NSOperation

@end
@interface OQNonConcurrentOperation ()

@end

@implementation OQNonConcurrentOperation
- (void)main {
    @try {
        if (self.isCancelled) return;
        
        NSLog(@"Start executing mainThread: %@, currentThread: %@",  [NSThread mainThread], [NSThread currentThread]);
        
        for (NSUInteger i = 0; i < 3; i++) {
            if (self.isCancelled) return;
            
            sleep(1);
            
            NSLog(@"Loop %@", @(i + 1));
        }
        NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
    }
    @catch(NSException *exception) {
        NSLog(@"Exception: %@", exception);
    }
}

@end

上面main方法的代码里使用了isCancelled。因为cancel方法不会强制停止operation的代码执行,所以如果operation正在执行main方法时被取消了,可以在执行任务的代码里加上isCancelled的判断来退出执行的任务。

concurrent operation

If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:

要自定义一个concurrent operation,至少需要重写上面的方法和属性。

At no time in your start method should you ever call super. When you define a concurrent operation, you take it upon yourself to provide the same behavior that the default start method provides, which includes starting the task and generating the appropriate KVO notifications. Your start method should also check to see if the operation itself was cancelled before actually starting the task. For more information about cancellation semantics

If you are implementing a concurrent operation, you must override this method and use it to initiate your operation. Your custom implementation must not call super at any time. In addition to configuring the execution environment for your task, your implementation of this method must also track the state of the operation and provide appropriate state transitions. When the operation executes and subsequently finishes its work, it should generate KVO notifications for the isExecuting and isFinished key paths respectively. For more information about manually generating KVO notifications,

文档里里提到了,自定义的concurrent operation,在重写start的方法里,不能调用super,并且要自己提供start方法里一些默认的行为,包括在开启任务前查看任务是否被取消;开启任务(由于任务封装在main方法里,可以理解为调用main方法);修改状态属性时手动生成KVO通知等。

@interface OQConcurrentOperation : NSOperation

@end
@implementation OQConcurrentOperation

@synthesize executing = _executing;
@synthesize finished  = _finished;

- (id)init {
    self = [super init];
    if (self) {
        _executing = NO;
        _finished  = NO;
    }
    return self;
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return _executing;
}

- (BOOL)isFinished {
    return _finished;
}

- (void)start {
   //在开启任务前判断operation是否已经被取消
    if (self.isCancelled) {
        //如果已经取消任务则把_finished设置成YES,并生成KVO通知,然后return。
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    //没有取消则异步开启任务
    [self willChangeValueForKey:@"isExecuting"];
    
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    _executing = YES;
    
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
    @try {
        NSLog(@"Start executing %@, mainThread: %@, currentThread: %@", NSStringFromSelector(_cmd), [NSThread mainThread], [NSThread currentThread]);

        sleep(3);
        
        [self willChangeValueForKey:@"isExecuting"];
        _executing = NO;
        [self didChangeValueForKey:@"isExecuting"];

        [self willChangeValueForKey:@"isFinished"];
        _finished  = YES;
        [self didChangeValueForKey:@"isFinished"];

        NSLog(@"Finish executing %@", NSStringFromSelector(_cmd));
    }
    @catch (NSException *exception) {
        NSLog(@"Exception: %@", exception);
    }
}
@end

NSOPerationQueue

 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
OQConcurrentOperation *p =  [[OQConcurrentOperation alloc] init];
 [queue addOperation:p];

在OQConcurrentOperation的start的方法里加入:

 NSLog(@" %@ method currentThread: %@", NSStringFromSelector(_cmd), [NSThread currentThread]);

log:

start method currentThread: <NSThread: 0x600000473d80>{number = 4, name = (null)}

从logke已看出,operation queue会生成新的线程来执行加入到其中的operation,执行的过程也是调用start方法。

queuePriority

For operations that are ready to execute, the operation queue always executes the one with the highest priority relative to the other ready operations.

queuePriority决定了operation在队列中执行的优先级。通过以下的顺序设置queuePriority属性可以加快或者推迟操作的执行:

NSOperationQueuePriorityVeryHigh
NSOperationQueuePriorityHigh
NSOperationQueuePriorityNormal
NSOperationQueuePriorityLow
NSOperationQueuePriorityVeryLow

依赖性

根据你应用的复杂度不同,将大任务再分成一系列子任务一般都是很有意义的,而你能通过NSOperation的依赖性实现。

比如说,对于服务器下载并压缩一张图片的整个过程,你可能会将这个整个过程分为两个操作。显然图片需要等到下载完成之后才能被调整尺寸,所以我们定义网络操作是压缩操作的依赖,通过代码来说就是:

[resizingOperation addDependency:networkingOperation];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];

这样resizingOperation会等待其依赖的networkingOperation完成(isFinished返回YES)之后才会执行。
此外,确保不要意外地创建依赖循环,像A依赖B,B又依赖A,这也会导致杯具的死锁。

从operation queue里移除operation

The NSOperationQueue class regulates the execution of a set of NSOperation objects. After being added to a queue, an operation remains in that queue until it is explicitly canceled or finishes executing its task.

文档里提到了在队列里的operation被取消或完成时会从队列里移除。

在的start开始加入

NSLog(@"isCancelled == %@  currentThread: %@", self.isCancelled?@"YES":@"NO", [NSThread currentThread]);
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
OQConcurrentOperation *operation =  [[OQConcurrentOperation alloc] init];
[queue addOperation:p];
[operation cancel];

log:

isCancelled == YES  currentThread: <NSThread: 0x6000004706c0>{number = 4, name = (null)}

虽然operation被取消了,但是operation queue依然生成了新的线程并调用了start方法,因此取消operation并不会把operation从operation queue里移除。

文档里后面也提到了:

You cannot directly remove an operation from a queue after it has been added. An operation remains in its queue until it reports that it is finished with its task. Finishing its task does not necessarily mean that the operation performed that task to completion. An operation can also be canceled. Canceling an operation object leaves the object in the queue but notifies the object that it should abort its task as quickly as possible. For currently executing operations, this means that the operation object’s work code must check the cancellation state, stop what it is doing, and mark itself as finished. For operations that are queued but not yet executing, the queue must still call the operation object’s start method so that it can processes the cancellation event and mark itself as finished.

opertion会等待任务完成后才被移除。取消operation会把operation留在队列里但是会通知operation应该终止任务(通过isCancelled判断是否终止任务)。如果operation依然在队列里但是还没有执行,取消后,队列依然会生成新的线程且operation会在线程里调用start方法。

In macOS 10.6 and later, if you call the cancel method on an operation that is in an operation queue and has unfinished dependent operations, those dependent operations are subsequently ignored. Because the operation is already cancelled, this behavior allows the queue to call the operation’s start method to remove the operation from the queue without calling its main method.

文档里提到了,在macOS 10.6以后,如果让一个在operation queue里的operation调用了cancel方法,如果这个operation有其他的依赖,则这些依赖会被忽略。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
OQConcurrentOperation *operation1 =  [[OQConcurrentOperation alloc] init];
NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block1 == %@", [NSThread currentThread]);
        sleep(3);
    }];
[operation1 addDependency: operation2];
[queue addOperation: operation1];
[queue addOperation: operation2];

log:

14:12:59.709827+0800 OperationQueues[27342:5488772] block1 == <NSThread: 0x604000477100>{number = 4, name = (null)}
14:13:02.712193+0800 OperationQueues[27342:5488770] isCancelled == NO  currentThread: <NSThread: 0x600000671140>{number = 5, name = (null)}

从时间上可以看出第二个log晚了3s,也就是operation1是在operation2之后执行的。

如果把operation1取消了:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
OQConcurrentOperation *operation1 =  [[OQConcurrentOperation alloc] init];
NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"block1 == %@", [NSThread currentThread]);
        sleep(3);
    }];
[operation1 addDependency: operation2];
[queue addOperation: operation1];
[queue addOperation: operation2];
[operation1 cancel];

log:

14:41:09.661578+0800 OperationQueues[27915:5821739] isCancelled == YES  currentThread: <NSThread: 0x600000477540>{number = 4, name = (null)}
14:41:09.661577+0800 OperationQueues[27915:5821721] block1 == <NSThread: 0x604000477040>{number = 5, name = (null)

由于operation1被取消了,它的依赖关系也都被忽略了,因此没有等待operation2完成,而是直接在新的线程里调用了start方法。
在队列里的operation什么时候被移除

maxConcurrentOperationCount

The maximum number of queued operations that can execute at the same

maxConcurrentOperationCount表示队列同一时刻做多执行的operation的数量。如果maxConcurrentOperationCount设置为2,则队列同时最多执行两个operation,当有一个operation完成时,会执行其他的operation。operation完成是通过isFinished判断的。
看一下下面的代码:

@interface CustomOperation : NSOperation

@end

@implementation CustomOperation
@synthesize finished = _finished;

- (void)start{
 NSLog(@"%@", [NSThread currentThread]);  
}
NSOperationQueue *q = [[NSOperationQueue alloc] init];
q.maxConcurrentOperationCount = 2;
for (int i = 0; i < 10; i++) {
        CustomOperation *p = [[CustomOperation alloc] init];
       [q addOperation:p];
}

log:

<NSThread: 0x60400067f1c0>{number = 4, name = (null)}
<NSThread: 0x600000472140>{number = 5, name = (null)}

由于没有设置完成状态,即使start方法掉用完后队列依然没有执行其它的operation,打印了两个log之后就不再打印了。
把上面的start方法改一下:

- (void)start{
    NSLog(@"%@", [NSThread currentThread]);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
    });
}

log:

17:09:51 <NSThread: 0x60000027ee00>{number = 3, name = (null)}
17:09:51 <NSThread: 0x60400047a900>{number = 4, name = (null)}
17:09:54 <NSThread: 0x60400047a900>{number = 4, name = (null)}
17:09:54 <NSThread: 0x6000006699c0>{number = 5, name = (null)}
17:09:57 <NSThread: 0x60400047a900>{number = 4, name = (null)}
17:09:57 <NSThread: 0x60000027ee00>{number = 3, name = (null)}
17:10:01 <NSThread: 0x6000006699c0>{number = 5, name = (null)}
17:10:01 <NSThread: 0x60400047a900>{number = 4, name = (null)}
17:10:04 <NSThread: 0x60000027ee00>{number = 3, name = (null)}
17:10:04 <NSThread: 0x60400047a900>{number = 4, name = (null)}

从log可以看出,当把_finished设置为YES并且手动发出KVO通知时后,队列才会继续执行其它的operation。

参考链接

http://blog.leichunfeng.com/blog/2015/07/29/ios-concurrency-programming-operation-queues/

NSOperation在SDWebImage里的应用

SDWebImage有一个继承自NSOperation的类SDWebImageDownloaderOperation,图片的下载和处理封装在了这个类里。

SDWebImageDownloaderOperation重写了start方法和executing、finished属性,但是没有重写asynchronous属性。
start方法里用到了@synchronized(),线程同时执行这段代码。
然后判断了operation是否被取消了:

if (self.isCancelled) {
    self.finished = YES;
    [self reset];
    return;
 }

如果没有取消operation,则使用NSURLSession生成了NSURLSessionTask,最后将开启了下载任务。

self.dataTask = [session dataTaskWithRequest:self.request];
self.dataTask = [session [self.dataTask resume];

SDWebImageDownloaderOperation重写了cancel方法,方法里取消了NSURLSessionTask并且更新了operataion的状态:

[super cancel];//更新isCancelled的状态
[self.dataTask cancel];
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;

SDWebImageDownloader有一个属性

@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;

downloadQueue是用来添加operation的队列。
_downloadQueue.maxConcurrentOperationCount = 6;
代码里限制了_downloadQueue最多同时执行6个operation,由于dataTask的代理回调后才会把operation的状态设置为完成,所以SDWebImage最多同时下载6张图片。

上面是网络获取图片的过程,SDWebImage从本地缓存那图片做取消功能时也使用了NSOperation。
SDWebImage的缓存功能在类SDImageCache里面。SDWebImage的缓存分为内存缓存(memory cache)和磁盘缓存(diskCache)。
SDWebImage从缓存里取图片会先从内存缓存里取:

//这个过程是在主线程中进行的,并且在内存缓存里的图片是解压过的。
[self.memCache objectForKey:key]

如果内存缓存里没有,会从磁盘里取。SDImageCache有一个属性_ioQueue 是自动定义的串行队列:

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

从磁盘里取的过程:

NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{
     if (operation.isCancelled) {
          // do not call the completion if cancelled
          return;
     }

     @autoreleasepool {
            ...//取图片
      }
  });

从磁盘里取图片的过程封装成了一个block添加到了_ioQueue这个串行队列里。这段代码开始创建了一个operation,这个operation在block里被捕获了,用途是来通过operation的属性isCancelled来取消执行block里面的代码。

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

推荐阅读更多精彩内容