KVO的原理实现

一、什么是KVO

KVO和Notification是Objective-C语言中观察者模式的两种实现机制。KVO指定一个被观察对象,当被观察对象某个属性发生改变时,观察对象会获得通知,你不需要给被观察的对象添加任何额外代码。

KVO和Notification区别:KVO是被观察者直接发送消息给观察者,是对象间的交互,而Notification则是观察者和被观察者通过通知中心对象之间进行交互,即消息由被观察者发送到通知中心对象,再由中心对象发给观察者,两者之间并不进行直接的交互。

二、实现原理

Apple官方文档描述:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

从文档中可以看出KVO是通过一项叫做isa-swizzling技术来实现的。当为被观察者属性注册观察者时,被观察者的isa指针被修改,指向一个中间类,而不是真正的类。所以isa指针其实不需要指向实例对象真实的类。我们不要依赖isa指针来确定类成员,相反应该使用类方法来确定实例对象的类。

从网上查到总结如下内容:

当观察某对象时,KVO机制动态创建一个被观察对象类的子类,并为这个新的子类重写了被观察属性keyPath的setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

三、使用举例

使用KVO分三步:

  • 注册观察者:

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
  • 观察者中实现

    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    
  • 移除观察者

    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
      - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

从以上三步可以看出,使用KVO,不需要对被观察者对象做任何修改。

  1. 创建一个YKNoteKVOObject类,用于对它属性进行监听

    //
    //  YKNoteKVOObject.h
    //  YKNote
    //
    //  Created by wanyakun on 2016/11/3.
    //  Copyright © 2016年 com.ucaiyuan. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface YKNoteKVOObject : NSObject
    
    @property (nonatomic, copy) NSString *name;
    
    @property (nonatomic, assign) NSInteger age;
    
    @property (nonatomic, assign) CGFloat salary;
    
    @property (nonatomic, strong) NSMutableArray *friends;
    
    @end
    
  1. 创建一个Controller,添加监听,并打印
//
//  YKNoteKVOViewController.m
//  YKNote
//
//  Created by wanyakun on 2016/11/7.
//  Copyright © 2016年 com.ucaiyuan. All rights reserved.
//

#import "YKNoteKVOViewController.h"
#import "YKNoteKVOObject.h"
#import "YKNoteRuntimeUtils.h"
#import <objc/runtime.h>

@interface YKNoteKVOViewController ()

@property (nonatomic, strong) YKNoteKVOObject *yKNoteKVOObject;
@property (nonatomic, strong) YKNoteKVOObject *yKNoteKVOObject2;
@property (nonatomic, strong) YKNoteKVOObject *yKNoteKVOObject3;
@property (nonatomic, strong) YKNoteKVOObject *yKNoteKVOObject4;

@property (nonatomic, strong) UITextView *textView;

@end

@implementation YKNoteKVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self.view addSubview:self.textView];
    [self.textView fill];
    
    self.yKNoteKVOObject.name = @"YKNoteKVOObject";
    self.yKNoteKVOObject2.name = @"YKNoteKVOObject2";
    self.yKNoteKVOObject3.name = @"YKNoteKVOObject3";
    self.yKNoteKVOObject4.name = @"YKNoteKVOObject4";
    
    self.yKNoteKVOObject.friends = [[NSMutableArray alloc] initWithCapacity:5];
    
    self.textView.text = @"========before add observer objective detail========\n";
    [self addBeforDetailToTextView];
    
    [self addObserver];
    
    self.textView.text = [self.textView.text stringByAppendingString:@"========after add observer objective detail========\n"];
    [self addAfterDetailToTextView];
    
    
    self.textView.text = [self.textView.text stringByAppendingString:@"============cls supeCls============\n"];
    [self addClsSuperClsToTextView];
    
    self.textView.text = [self.textView.text stringByAppendingString:@"============Object IMP============\n"];
    [self addObjectIMPToTextView];
    
    self.textView.text = [self.textView.text stringByAppendingString:@"============Runtime IMP============\n"];
    [self addRuntimeMethodIMPToTextView];
    
    self.textView.text = [self.textView.text stringByAppendingString:@"============Observer change============\n"];
    //自动触发KVO通知
    self.yKNoteKVOObject.name = @"YKNoteKVOObject Change property";
    [self.yKNoteKVOObject setValue:@"YKNoteKVOObject setValue:forKey:" forKey:@"name"];
    [self.yKNoteKVOObject setValue:@"YKNoteKVOObject setValue:forKeyPath:" forKeyPath:@"name"];
    
    NSMutableArray *array = [self.yKNoteKVOObject mutableArrayValueForKey:@"friends"];
    [array addObject:@"wanyakun"];
    //手动触发KVO通知
    self.yKNoteKVOObject.salary = 1000;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)dealloc {
    [self removeObserver];
    self.yKNoteKVOObject = nil;
    self.yKNoteKVOObject2 = nil;
    self.yKNoteKVOObject3 = nil;
    self.yKNoteKVOObject4 = nil;
    self.textView = nil;
}

#pragma mark - private method

- (void)addObserver {
    [self.yKNoteKVOObject addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [self.yKNoteKVOObject2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [self.yKNoteKVOObject3 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [self.yKNoteKVOObject3 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    [self.yKNoteKVOObject addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [self.yKNoteKVOObject addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

- (void)removeObserver {
    [self.yKNoteKVOObject removeObserver:self forKeyPath:@"name"];
    [self.yKNoteKVOObject2 removeObserver:self forKeyPath:@"age"];
    [self.yKNoteKVOObject3 removeObserver:self forKeyPath:@"name"];
    [self.yKNoteKVOObject3 removeObserver:self forKeyPath:@"age"];
    
    [self.yKNoteKVOObject removeObserver:self forKeyPath:@"salary"];
    [self.yKNoteKVOObject removeObserver:self forKeyPath:@"friends"];
}

- (void)addBeforDetailToTextView {
    NSString *beforeDetail =  [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject" objc:self.yKNoteKVOObject];
    NSString *beforeDetail2 = [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject2" objc:self.yKNoteKVOObject2];
    NSString *beforeDetail3 = [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject3" objc:self.yKNoteKVOObject3];
    NSString *beforeDetail4 = [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject4" objc:self.yKNoteKVOObject4];
    
    self.textView.text = [self.textView.text stringByAppendingString:[NSString stringWithFormat:@"%@%@%@%@", beforeDetail, beforeDetail2, beforeDetail3, beforeDetail4]];
}

- (void)addAfterDetailToTextView {
    NSString *afterDetail =  [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject" objc:self.yKNoteKVOObject];
    NSString *afterDetail2 = [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject2" objc:self.yKNoteKVOObject2];
    NSString *afterDetail3 = [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject3" objc:self.yKNoteKVOObject3];
    NSString *afterDetail4 = [YKNoteRuntimeUtils descriptionDetailWithName:@"yKNoteKVOObject4" objc:self.yKNoteKVOObject4];
    
    self.textView.text = [self.textView.text stringByAppendingString:[NSString stringWithFormat:@"%@%@%@%@", afterDetail, afterDetail2, afterDetail3, afterDetail4]];
}

- (void)addClsSuperClsToTextView {
    Class cls = object_getClass(self.yKNoteKVOObject);
    Class superCls = class_getSuperclass(cls);
    
    self.textView.text = [self.textView.text stringByAppendingString:[NSString stringWithFormat:@"yKNoteKVOObject isa:%@\nyKNoteKVOObject isa's super class:%@\n", cls, superCls]];
}

- (void)addObjectIMPToTextView {
    NSString *objcIMP = [NSString stringWithFormat:@"objcIMP yKNoteKVOObject setName:%p, setAge:%p\n", [self.yKNoteKVOObject methodForSelector:@selector(setName:)], [self.yKNoteKVOObject methodForSelector:@selector(setAge:)]];
    NSString *objcIMP2 = [NSString stringWithFormat:@"objcIMP yKNoteKVOObject2 setName:%p, setAge:%p\n", [self.yKNoteKVOObject2 methodForSelector:@selector(setName:)], [self.yKNoteKVOObject2 methodForSelector:@selector(setAge:)]];
    NSString *objcIMP3 = [NSString stringWithFormat:@"objcIMP yKNoteKVOObject3 setName:%p, setAge:%p\n", [self.yKNoteKVOObject3 methodForSelector:@selector(setName:)], [self.yKNoteKVOObject3 methodForSelector:@selector(setAge:)]];
    NSString *objcIMP4 = [NSString stringWithFormat:@"objcIMP yKNoteKVOObject4 setName:%p, setAge:%p\n", [self.yKNoteKVOObject4 methodForSelector:@selector(setName:)], [self.yKNoteKVOObject4 methodForSelector:@selector(setAge:)]];
    
    self.textView.text = [self.textView.text stringByAppendingString:[NSString stringWithFormat:@"%@%@%@%@", objcIMP, objcIMP2, objcIMP3, objcIMP4]];
}

- (void)addRuntimeMethodIMPToTextView {
    NSString *runtimeMethodImpl = [NSString stringWithFormat:@"runtimeMethodImpl yKNoteKVOObject setName:%p, setAge:%p\n", class_getMethodImplementation(object_getClass(self.yKNoteKVOObject), @selector(setName:)), class_getMethodImplementation(object_getClass(self.yKNoteKVOObject), @selector(setAge:))];
    NSString *runtimeMethodImpl2 = [NSString stringWithFormat:@"runtimeMethodImpl yKNoteKVOObject2 setName:%p, setAge:%p\n", class_getMethodImplementation(object_getClass(self.yKNoteKVOObject2), @selector(setName:)), class_getMethodImplementation(object_getClass(self.yKNoteKVOObject2), @selector(setAge:))];
    NSString *runtimeMethodImpl3 = [NSString stringWithFormat:@"runtimeMethodImpl yKNoteKVOObject3 setName:%p, setAge:%p\n", class_getMethodImplementation(object_getClass(self.yKNoteKVOObject3), @selector(setName:)), class_getMethodImplementation(object_getClass(self.yKNoteKVOObject3), @selector(setAge:))];
    NSString *runtimeMethodImpl4 = [NSString stringWithFormat:@"runtimeMethodImpl yKNoteKVOObject4 setName:%p, setAge:%p\n", class_getMethodImplementation(object_getClass(self.yKNoteKVOObject4), @selector(setName:)), class_getMethodImplementation(object_getClass(self.yKNoteKVOObject4), @selector(setAge:))];
    
    self.textView.text = [self.textView.text stringByAppendingString:[NSString stringWithFormat:@"%@%@%@%@", runtimeMethodImpl, runtimeMethodImpl2, runtimeMethodImpl3, runtimeMethodImpl4]];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    self.textView.text = [self.textView.text stringByAppendingString:[NSString stringWithFormat:@"The value of %@ change to : %@\n",keyPath, [change objectForKey:@"new"]]];
    NSLog(@"The value of %@ change to : %@\n", keyPath, [change objectForKey:@"new"]);
}

#pragma mark - getter
- (YKNoteKVOObject *)yKNoteKVOObject {
    if (_yKNoteKVOObject == nil) {
        _yKNoteKVOObject = [[YKNoteKVOObject alloc] init];
    }
    return _yKNoteKVOObject;
}

- (YKNoteKVOObject *)yKNoteKVOObject2 {
    if (_yKNoteKVOObject2 == nil) {
        _yKNoteKVOObject2 = [[YKNoteKVOObject alloc] init];
    }
    return _yKNoteKVOObject2;
}

- (YKNoteKVOObject *)yKNoteKVOObject3 {
    if (_yKNoteKVOObject3 == nil) {
        _yKNoteKVOObject3 = [[YKNoteKVOObject alloc] init];
    }
    return _yKNoteKVOObject3;
}

- (YKNoteKVOObject *)yKNoteKVOObject4 {
    if (_yKNoteKVOObject4 == nil) {
        _yKNoteKVOObject4 = [[YKNoteKVOObject alloc] init];
    }
    return _yKNoteKVOObject4;
}

- (UITextView *)textView {
    if (_textView == nil) {
        _textView = [[UITextView alloc] init];
        _textView.contentOffset = CGPointMake(0, 20);
        _textView.editable = NO;
    }
    return _textView;
}

@end
  1. 输出的内容

       ========before add observer objective detail========
       yKNoteKVOObject:<YKNoteKVOObject: 0x60800024edf0>
         class YKNoteKVOObject
         objcClass YKNoteKVOObject
         implementmethod 
             setFriends:,
             setSalary:,
             setAge:,
             age,
             salary,
             friends,
             subObject,
             setSubObject:,
             .cxx_destruct,
             name,
             setName:,
             init
       yKNoteKVOObject2:<YKNoteKVOObject: 0x60800024eee0>
         class YKNoteKVOObject
         objcClass YKNoteKVOObject
         implementmethod 
             setFriends:,
             setSalary:,
             setAge:,
             age,
             salary,
             friends,
             subObject,
             setSubObject:,
             .cxx_destruct,
             name,
             setName:,
             init
       yKNoteKVOObject3:<YKNoteKVOObject: 0x60800005bc30>
         class YKNoteKVOObject
         objcClass YKNoteKVOObject
         implementmethod 
             setFriends:,
             setSalary:,
             setAge:,
             age,
             salary,
             friends,
             subObject,
             setSubObject:,
             .cxx_destruct,
             name,
             setName:,
             init
       yKNoteKVOObject4:<YKNoteKVOObject: 0x60800024e0d0>
         class YKNoteKVOObject
         objcClass YKNoteKVOObject
         implementmethod 
             setFriends:,
             setSalary:,
             setAge:,
             age,
             salary,
             friends,
             subObject,
             setSubObject:,
             .cxx_destruct,
             name,
             setName:,
             init
       ========after add observer objective detail========
       yKNoteKVOObject:<YKNoteKVOObject: 0x60800024edf0>
         class YKNoteKVOObject
         objcClass NSKVONotifying_YKNoteKVOObject
         implementmethod 
             setFriends:,
             setAge:,
             setName:,
             class,
             dealloc,
             _isKVOA
       yKNoteKVOObject2:<YKNoteKVOObject: 0x60800024eee0>
         class YKNoteKVOObject
         objcClass NSKVONotifying_YKNoteKVOObject
         implementmethod 
             setFriends:,
             setAge:,
             setName:,
             class,
             dealloc,
             _isKVOA
       yKNoteKVOObject3:<YKNoteKVOObject: 0x60800005bc30>
         class YKNoteKVOObject
         objcClass NSKVONotifying_YKNoteKVOObject
         implementmethod 
             setFriends:,
             setAge:,
             setName:,
             class,
             dealloc,
             _isKVOA
       yKNoteKVOObject4:<YKNoteKVOObject: 0x60800024e0d0>
         class YKNoteKVOObject
         objcClass YKNoteKVOObject
         implementmethod 
             setFriends:,
             setSalary:,
             setAge:,
             age,
             salary,
             friends,
             subObject,
             setSubObject:,
             .cxx_destruct,
             name,
             setName:,
             init
       ============cls supeCls============
       yKNoteKVOObject isa:NSKVONotifying_YKNoteKVOObject
       yKNoteKVOObject isa's super class:YKNoteKVOObject
       ============Object IMP============
       objcIMP yKNoteKVOObject setName:0x1004c145d, setAge:0x1004c7c16
       objcIMP yKNoteKVOObject2 setName:0x1004c145d, setAge:0x1004c7c16
       objcIMP yKNoteKVOObject3 setName:0x1004c145d, setAge:0x1004c7c16
       objcIMP yKNoteKVOObject4 setName:0x1003af810, setAge:0x1003af870
       ============Runtime IMP============
       runtimeMethodImpl yKNoteKVOObject setName:0x1004c145d, setAge:0x1004c7c16
       runtimeMethodImpl yKNoteKVOObject2 setName:0x1004c145d, setAge:0x1004c7c16
       runtimeMethodImpl yKNoteKVOObject3 setName:0x1004c145d, setAge:0x1004c7c16
       runtimeMethodImpl yKNoteKVOObject4 setName:0x1003af810, setAge:0x1003af870
       ============Observer change============
       The value of name change to : YKNoteKVOObject Change property
       The value of name change to : YKNoteKVOObject setValue:forKey:
       The value of name change to : YKNoteKVOObject setValue:forKeyPath:
       The value of friends change to : (
           wanyakun
       )
       The value of salary change to : 1000
    

从输出能容可以得出:

class = [objc class],

objcClass = object_getClass(objc)

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
  • 添加observer之前,所有对象的class和objcClass均为YKNoteKVOObject。
  • 添加observer之后,添加了observer的对象的class为YKNoteKVOObject, objecClass为NSKVONotifying_YKNoteKVOObject
    • 添加observer之后,对象的isa指针已经被替换为NSKVONotifying_YKNoteKVOObject,为YKNoteKVOObject子类,即动态生成子类(在原类添加前缀NSKVONotifying_),并使用子类替换对象的isa指针。
    • 子类NSKVONotifying_YKNoteKVOObject重载了被添加了observer的属性的set方法。

四、KVO Compliance

官方文档:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-SW3

其中涉及到到两种技术:自动发射key-value change通知和手动发射key-value change通知

  • 自动发射key-value change通知

    NSObject实现了自动发射key-value change通知,使用存取器、KVC方法都会自动发射变化通知,对于集合属性,可以通过代理对象来达到自动发射变化通知。

    // Call the accessor method.
    self.yKNoteKVOObject.name = @"YKNoteKVOObject Change property";
    // Use setValue:forKey:
    [self.yKNoteKVOObject setValue:@"YKNoteKVOObject setValue:forKey:" forKey:@"name"];
    // Use a key path, where 'yKNoteKVOObject' is a kvc-compliant property of 'name'.
    [self.yKNoteKVOObject setValue:@"YKNoteKVOObject setValue:forKeyPath:" forKeyPath:@"name"];
    // Use mutableArrayValueForKey: to retrieve a relationship proxy object
    NSMutableArray *array = [self.yKNoteKVOObject mutableArrayValueForKey:@"friends"];
    [array addObject:@"wanyakun"];
    
  • 手动发射key-value change通知

    手动发射key-value change通知需要实现automaticallyNotifiesObserversForKey:方法。

    /*
     手动实现KVO发射通知,需要重载automaticallyNotifiesObserversForKey:,或者实现automaticallyNotifiesObserversOf<key>, 对于不需要自动发射KVO通知的属性需要返回NO,然后在setter方法中添加willChangeValueForKey:和didChangeValueForKey:方法的调用代码
     */
    + (BOOL)automaticallyNotifiesObserversOfSalary {
        return NO;
    }
    
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"salary"]) {
            return NO;
        } else {
            return [super automaticallyNotifiesObserversForKey:key];
        }
    }
    
    - (void)setSalary:(CGFloat)salary {
        [self willChangeValueForKey:@"salary"];
        _salary = salary;
        [self didChangeValueForKey:@"salary"];
    }
    

五、总结

使用Clang对addObserver代码进行编译,对编译结果代码查看,其中并无NSKVONotifying_YKNoteKVOObject任何代码。更说明,NSKVONotifying_YKNoteKVOObject子类是在runtime时动态生成的。由于这种继承方式的注入是在运行时而不是编译时实现的,如果给定的实例没有观察者,那么KVO不会有任何开销。

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

推荐阅读更多精彩内容