Key-Value Observing(kvo)一:原理分析

一、kvo简介

Key-Value Observing Programming Guide
对于kvo使用分为3步:

  • 1.Registering as an Observer
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

observer:要添加的监听者对象,当监听的属性发生改变时会通知该对象,必须实现- observeValueForKeyPath:ofObject:change:context:方法,否则程序会抛出异常。
keyPath:监听的属性,不能传nil
options:指明通知发出的时机以及change中的键值。
context:是一个可选的参数,可以传任何数据。

⚠️添加监听的方法addObserver:forKeyPath:options:context:并不会对监听和被监听的对象以及context做强引用,必须自己保证他们在监听过程中不被释放。

  • 2.Receiving Notification of a Change
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
  • 3.Removing an Object as an Observer
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

1.1 options

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,//更改前的值
    NSKeyValueObservingOptionOld = 0x02,//更改后的值
    NSKeyValueObservingOptionInitial = 0x04,//观察最初的值(在注册观察服务时会调用一次触发方法)
    NSKeyValueObservingOptionPrior  = 0x08 //分别在值修改前后触发方法(即一次修改有两次触发)
};

可以看到NSKeyValueObservingOptions4个枚举值,测试代码如下:

self.obj = [HPObject alloc];
self.obj.name = @"hp1";
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.obj.name = @"hp2";

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change:%@",change);
}

修改options参数输出如下:

//NSKeyValueObservingOptionNew
change:{
    kind = 1;
    new = hp2;
}
//NSKeyValueObservingOptionOld
change:{
    kind = 1;
    old = hp1;
}
//NSKeyValueObservingOptionInitial
change:{
    kind = 1;
}
change:{
    kind = 1;
}
//NSKeyValueObservingOptionPrior
change:{
    kind = 1;
    notificationIsPrior = 1;
}
 change:{
    kind = 1;
}
//NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior
change:{
    kind = 1;
    new = hp1;
}
change:{
    kind = 1;
    notificationIsPrior = 1;
    old = hp1;
}
change:{
    kind = 1;
    new = hp2;
    old = hp1;
}
// 0
change:{
    kind = 1;
}

NSKeyValueChangeKey定义如下:

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;

FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
  • NSKeyValueChangeKindKey:指明了变更的类型,一般情况下返回的都是1。集合中的元素被插入,删除,替换时返回2、3、4
    NSKeyValueChange定义如下:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//普通类型设置
    NSKeyValueChangeInsertion = 2,//集合元素插入
    NSKeyValueChangeRemoval = 3,//集合元素移除
    NSKeyValueChangeReplacement = 4,//集合元素替换
};
  • NSKeyValueChangeNewKey:改变后新值的key,如果是集合返回的数据是集合。
  • NSKeyValueChangeOldKey:改变前旧值的key,如果是集合返回的数据是集合。
  • NSKeyValueChangeIndexesKey:若果是集合类型,这个键的值是NSIndexSet对包含了增加,移除或者替换对象的index
  • NSKeyValueChangeNotificationIsPriorKey:NSKeyValueObservingOptionPrior调用前标记。

综上:

  • NSKeyValueObservingOptionNew:指明change字典中应该包含改变后的新值。
  • NSKeyValueObservingOptionOld:指明change字典中应该包含改变前的旧值。
  • NSKeyValueObservingOptionInitial:注册后立马调用一次,这种通知只会发送一次。可以做一些一次性的工作。当同时指定new/old/initial的情况时,initial通知只包含new值。(实际上还是old值,因为是注册后立马调用,所以实际上对它来说是新值。任何情况下initial都不会包含old
  • NSKeyValueObservingOptionPrior:修改前后触发,会调用两次。修改前触发会包含notificationIsPrior字段。当同时指定new/old时,修改前会包含old,修改后会包含newold。(一般的通知发出时机都是在属性改变后,虽然change字典中包含了oldnew,但是通知还是在属性改变后才发出)。
  • 0:直接传递0,在每次调用的时候都返回包含kindchange。可以理解为默认实现。

1.2 context

这个参数最后会被传递到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。
对于多个keyPath的观察,需要在observeValueForKeyPath同时判断objectkeyPath,可以声明一个静态变量传递给context用来区分不同的通知提高代码的可读性:

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

当然如果子类和父类都实现了对同一对象的同一属性的观察,并且父类和子类都可能对其进行设值,那么这个时候用context区分就很有用了。

1.3 移除观察者

官方文档说了在观察者dealloc的时候被观察者不会自动移除观察者,还是会继续给观察者发送消息。需要自己保证移除。
比如某个页面监听了一个对象的属性,这个对象是从前一个页面传递进来的(本质上是对象不被释放)。在不移除观察的情况下,多次进入这个页面在属性变化的时候就发生了crash

image.png

根本原因是之前进入页面的时候观察者没有移除,导致发送消息的时候之前的observer不存在。

kvo的使用三步曲要完整

当然如果页面是个单例则不会崩溃,如果addObserver每次都调用则会进行多次回调。

二、kvo初探

2.1 kvo手动自动通知

在被观察者中实现automaticallyNotifiesObserversForKey可以控制kvo是否自动通知:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    NSLog(@"%s key:%@",__func__,key);
    return NO;
}
  • 当返回NO的时候不会自动调用通知,当返回YES的时候会进行自动通知。
  • automaticallyNotifiesObserversForKey是在注册观察者的时候进行调用的。所以在中途通过开关配置是无效的(只在addObserver第一次调用的时候调用)。
    image.png

willChangeValueForKey & didChangeValueForKey手动通知

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}
  • 可以通过willChangeValueForKeydidChangeValueForKey进行手动通知。
  • 手动通知key可以自己写(key必须存在类中),所以这里可以做映射。将多个属性的变化映射到一个属性上。
  • 手动通知不受自动开关状态的影响。
  • 如果手动和自动同时开启,则两个都会发送通知。

2.2 嵌套层次的监听

keyPathsForValuesAffectingValueForKey(key - keys)
比如下载文件:
下载进度 = 已下载数据大小 / 总数据大小。总数据大小由于添加文件可能会发生变化。

@interface HPObject : NSObject

@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end

@implementation HPObject

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSLog(@"%s key:%@",__func__,key);
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress {
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

@end

监听和调用:


- (void)viewDidLoad {
    [super viewDidLoad];
    [self.obj addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change:%@",change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.obj.writtenData += 10;
    self.obj.totalData += 1;
}

- (void)dealloc {
    [self.obj removeObserver:self forKeyPath:@"downloadProgress"];
    NSLog(@"dealloc");
}
  • keyPathsForValuesAffectingValueForKey中对key进行了映射(只在addObserver第一次调用的时候调用)。
  • keyPathsForValuesAffectingValueForKey中会进行递归映射,也就是totalDatawrittenData也会去查找自身的依赖。
+[HPObject keyPathsForValuesAffectingValueForKey:] key:downloadProgress
+[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
  • 这个时候通过touchesBegan中调用writtenDatatotalData就能监听到downloadProgress的变化了。writtenDatatotalData设置值的时候会调用到downloadProgress中(newValue 取值)。所以只要有任一一个变化都会调用到observeValueForKeyPath中。
  • 在首次(所有的第一次,不论页面是否重建与否,这里是与keyPathsForValuesAffectingValueForKey次数对应的)touchesBegan时,observeValueForKeyPath在上面的案例中会调用3次。
    image.png

    可以看到确实是系统内部直接多调用了一次。

2.3 kvo对可变数组的观察

self.obj.dateArray = [NSMutableArray array];
[self.obj addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
[self.obj.dateArray addObject:@(1)];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change:%@",change);
}

上面的案例dateArray添加元素并不能触发kvo,需要修改为:

[[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
[[self.obj mutableArrayValueForKey:@"dateArray"] removeObject:@"1"];
[self.obj mutableArrayValueForKey:@"dateArray"][0] = @"3";

输出:

change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        1
    );
}
change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635c0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        2
    );
}
change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 3;
}
change:{
    indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 4;
    new =     (
        3
    );
}
  • 通过mutableArrayValueForKey获取dateArray再添加就能监听到了。
  • 这个时候kind变为了2、3、4就与前面介绍的NSKeyValueChange对应上了。

⚠️kvo监听集合元素变化,需要用到kvc的原理机制才能监听到变化。由于kvo底层也是由kvc实现的

集合相关API如下:
NSMutableArraymutableArrayValueForKeymutableArrayValueForKeyPath
NSMutableSetmutableSetValueForKeymutableSetValueForKeyPath
NSMutableOrderedSetmutableOrderedSetValueForKeymutableOrderedSetValueForKeyPath
These methods provide the additional benefit of maintaining key-value observing compliance for the objects held in the collection object
说明了集合类型的要特殊处理,具体可以参考kvc的说明:Accessing Collection Properties

2.3.1 可变数组专属API

当然除了上面对于集合类型的赋值通过kvc相关接口还可以通过数组专属API来完成。

@property (nonatomic, strong) NSMutableArray <HPObject *>*array;

self.array = [NSMutableArray array];
HPObject *obj1 = [HPObject alloc];
obj1.name = @"obj1";
[self.array addObject:obj1];
HPObject *obj2 = [HPObject alloc];
obj2.name = @"obj2";
[self.array addObject:obj2];

[self.array addObserver:self toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:1] forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.array[0].name = @"_obj0";
self.array[1].name = @"_obj1";

输出:

change:{
    kind = 1;
    new = "_obj1";
}

这样当self.array[1].name发生变化的时候就监听到了。这里本质上就相当于是对obj1的监听。后续数组中替换了1位置的数组是监听不到的。

HPObject *obj3 = [HPObject alloc];
obj3.name = @"obj3";
self.array[1] = obj3;
self.array[1].name = @"_obj3";

这样替换后监听不到。

三、kvo原理分析

Key-Value Observing Implementation Details
根据官方文档可以看到使用了isa-swizzling技术。
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.

3.1 isa-swizzling验证

直接在addObserver的调用处打个断点:

image.png

addObserverobjHPObject变成了NSKVONotifying_HPObject

  1. 那么NSKVONotifying_HPObject是什么时候生成的呢?
    重新运行,在addObserver之前验证NSKVONotifying_HPObject
(lldb) p objc_getClass("NSKVONotifying_HPObject")
(Class _Nullable) $0 = nil

这样就意味着NSKVONotifying_HPObject是在addObserver的时候底层动态生成的。

  1. NSKVONotifying_HPObjectHPObject有什么关系呢?
- (void)printClasses:(Class)cls {
    //注册类总个数
    int count = objc_getClassList(NULL, 0);
    //先将类本身放入数组中
    NSMutableArray *array = [NSMutableArray arrayWithObject:cls];
    //开辟空间
    Class *classes = (Class *)malloc(sizeof(Class)*count);
    //获取已经注册的类
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        //获取cls的子类,一层。
        if (cls == class_getSuperclass(classes[i])) {
            [array addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@",array);
}

上面这段代码是打印类以及它的子类(单层)。
调用:

[self printClasses:[HPObject class]];
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self printClasses:[HPObject class]];
[self printClasses:objc_getClass("NSKVONotifying_HPObject")];

输出:

classes = (
    HPObject,
    HPCat
)
classes = (
    HPObject,
    "NSKVONotifying_HPObject",
    HPCat
)
classes = (
    "NSKVONotifying_HPObject"
)
  • NSKVONotifying_HPObject是在addObserver过程中底层动态添加的。
  • NSKVONotifying_HPObjectHPObject的子类。NSKVONotifying_HPObject本身没有子类。

3.2 kvo 生成子类分析

既然NSKVONotifying_HPObjectHPObject的子类,那么它都有什么内容呢?
方法:

- (void)printClassAllProtocol:(Class)cls {
    unsigned int count = 0;
    Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
    for (int i = 0; i < count; i++) {
        Protocol *proto = protocolList[i];
        NSLog(@"%s",protocol_getName(proto));
    }
    free(protocolList);
}

输出:

setName:-0x7fff207bbb57
class-0x7fff207ba662
dealloc-0x7fff207ba40b
_isKVOA-0x7fff207ba403

同理可以获取协议,属性以及成员变量:

- (void)printClassAllProtocol:(Class)cls {
    unsigned int count = 0;
    Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
    for (int i = 0; i < count; i++) {
        Protocol *proto = protocolList[i];
        NSLog(@"%s",protocol_getName(proto));
    }
    free(protocolList);
}

- (void)printClassAllProprerty:(Class)cls {
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList(cls, &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = propertyList[i];
        NSLog(@"%s-%s", property_getName(property), property_getAttributes(property));
    }
    free(propertyList);
}

- (void)printClassAllIvars:(Class)cls {
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSLog(@"%s-%s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
    }
    free(ivarList);
}

没有输出任何内容。那么核心就在方法了。
_isKVOA很好理解用来判断是否kvo生成的类,class标记类型。setName:是对父类namesetter方法进行了重写。dealloc中进行了isa重新指回。

3.2.1 class

addObserver后调用class输出:

(lldb) p self.obj.class
(Class) $0 = HPObject

那么重写class就是为了返回原来的类的信息。不会返回kvo类自己的class信息。

3.2.2 dealloc

既然NSKVONotifying_HPObject是动态创建的,那么它销毁吗?
deallocremoveObserver前后分别验证:

image.png

可以看到移除后isa指回了原来的类,也就是dealloc中进行了isa的指回。并且NSKVONotifying_HPObject类仍然存在。

3.2.3 setter

既然重写了setName:观察属性,那么成员变量能观察么?增加age成员变量:

[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.obj addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
self.obj->age = 18;

当对age进行赋值并没有触发回调。那么就说明了对setter方法进行的监听。
deallocremoveObserver后查看name的值:

image.png

那就说明在kvo生成的类中对name的修改影响到了原始类。
name下个内存断点:

(lldb) watchpoint set variable self->_obj->_name
Watchpoint created: Watchpoint 1: addr = 0x60000129b260 size = 8 state = enabled type = w
    watchpoint spec = 'self->_obj->_name'
    new value: 0x0000000000000000

在赋值的时候堆栈如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
  * frame #0: 0x00007fff2018b1e2 libobjc.A.dylib`objc_setProperty_nonatomic_copy + 44
    frame #1: 0x000000010afecd70 KVODemo`-[HPObject setName:](self=0x000060000129b250, _cmd="setName:", name=@"HP") at HPObject.h:19:39
    frame #2: 0x00007fff207c2749 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
    frame #3: 0x00007fff207c300b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
    frame #4: 0x00007fff207bbc64 Foundation`_NSSetObjectValueAndNotify + 269
    frame #5: 0x000000010afed248 KVODemo`-[HPDetailViewController viewDidLoad](self=0x00007fe291e0d890, _cmd="viewDidLoad") at HPDetailViewController.m:43:14

调用逻辑如下:

-[HPObject setName:]
Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
Foundation`_NSSetObjectValueAndNotify

_NSSetObjectValueAndNotify汇编调用主要如下:

"willChangeValueForKey:"
call   0x7fff2094ff0e 
"didChangeValueForKey:"
"_changeValueForKey:key:key:usingBlock:"

_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:中有获取observers操作:

_NSKeyValueObservationInfoGetObservances

那么意味着在处理完所有事情后会进行通知。
并且有NSKeyValueWillChangeNSKeyValueDidChange

image.png

继续在observeValueForKeyPath的回调中打个断点:

image.png

确认是在NSKeyValueNotifyObserver通知中进行的回调。

总结(kvo原理)

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

推荐阅读更多精彩内容