iOS底层探索21、KVO 原理

KVO 苹果文档地址
KVO: Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
键值编码是一种机制,允许在其他对象(被监听对象)的指定属性发生更改时被通知到监听对象。

一、KVO的简单使用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor redColor];

    self.person = [MyPerson new];
    self.person.name = @"initName";
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"newkey=%@ - oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"%@+",self.person.name];
}

- (void)dealloc {
     [self.person removeObserver:self forKeyPath:@"name" context:NULL];
}

方法注释:

`- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;`
/* Register or deregister as an observer of the value at a key path relative to the receiver. 
      The options determine what is included in observer notifications and when they're sent, 
      as described above, and the context is passed in observer notifications as described above. 
      You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible 
      because it allows you to more precisely specify your intent. 
      When the same observer is registered for the same key path multiple times, 
      but with different context pointers each time, -removeObserver:forKeyPath: 
      has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/

1、context

- context:上下文,它是什么呢?--> 通过苹果文档和实际场景均可得知,context用来更安全精确便利 地指定和区分开我们的监听;
为何forKeyPath来区分不好?--> 当存在监听多个对象/多个属性,在处理监听时 仅仅判断监听的是哪个对象 就挺麻烦了,性能和代码可读性会变得复杂。
建议使用context.

2、removeObserver - 移除观察者

Removing an Object as an Observer 苹果文档

1)移除观察者
  1. observer不会在deallocated时自动移除;
  2. 使用-removeObserver:forKeyPath:context:
    通过注释:你应该尽可能使用-removeObserver:forKeyPath:context:而不是-removeObserver:forKeyPath:因为它允许你更精确地指定你的意图。当同一个观察者为相同的键路径多次注册,但每次都使用不同的上下文指针时,-removeObserver:forKeyPath:必须在决定删除什么内容时猜测上下文指针,而且它可能猜错了。
2)不移除观察者会出现的问题

创建MySubPerson单例代码如下:

@implementation MySubPerson

static MySubPerson* _instance = nil;
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}
//- (void)personInstanceOne {
//    NSLog(@"%s",__func__);
//}

+ (void)personClassOne {
    NSLog(@"%s",__func__);
}

@end

分别在2(MyController1)、3(MyController2)级页面为其添加观察者,2级页面dealloc时不移除监听,运行工程:

kvo.gif

报错如下:
image.png

原因分析:
1:person是个MySubPerson单例对象,给它添加了2个监听,但是有一个未移除,person对象一直还在内存中;
2:再次进入页面又添加了一个监听,当属性改变时,需要给两个监听都发消息,但是只能找到现有的对象,原来的那个找不着了是个野指针了,它便导致崩溃。

3、开关 KVO

  1. 系统方法
    MyPerson类中将自动打开设为NO(默认是YES):
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

再次运行工程,监听不到属性变化了。

  1. 手动打开
    代码如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}
- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

再次运行工程,可正常监听到变化。

4、监听集合类型

以数组为例.

 // 数组
    self.person.mDataArray = [NSMutableArray array];
    [self.person addObserver:self forKeyPath:@"mDataArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"newkey=%@ - oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
//    self.person.name = [NSString stringWithFormat:@"%@-",self.person.name];
    [self.person.mDataArray addObject:@"one"];
}

并未监听到变化,查看苹果文档所给信息

image.png

KVO是基于KVC的,我们对代码进行如下修改:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    self.person.name = [NSString stringWithFormat:@"%@-",self.person.name];
    [[self.person mutableArrayValueForKey:@"mDataArray"] addObject:@"one"]; //[self.person.mDataArray addObject:@"one"];
}

运行工程,监听结果:

2020-10-28 18:47:03.337414+0800 DemoEmpty_iOS[9449:394437] {
    kind = 1;// NSKeyValueChangeSetting
    new = "(null)-";
    old = "<null>";
}
2020-10-28 18:47:03.338002+0800 DemoEmpty_iOS[9449:394437] {
    indexes = "<_NSCachedIndexSet: 0x60000396e700>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;// NSKeyValueChangeInsertion
    new =     (
        one
    );
}

KVO部分代码:

/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
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 API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

二、 KVO 底层原理

1、属性和成员

MyController2 中主要代码如下:

self.person = [[MyPerson alloc]init];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
    [self.person addObserver:self forKeyPath:@"ivarName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"赋值前 name=%@ - ivarName=%@",self.person.name,self.person->ivarName);

    self.person.name = @"名字";
    self.person->ivarName = @"昵称";
    
    NSLog(@"赋值后 name=%@ / ivarName=%@",self.person.name,self.person->ivarName);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"observer %@",change);
    NSLog(@"newkey=%@ / oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}

运行工程,输出:

2020-10-28 23:07:35.530041+0800 DemoEmpty_iOS[9847:515045] 赋值前 name=(null) - ivarName=(null)
2020-10-28 23:07:35.530594+0800 DemoEmpty_iOS[9847:515045] observer {
    kind = 1;
    new = "\U540d\U5b57";
    old = "<null>";
}
2020-10-28 23:07:35.530846+0800 DemoEmpty_iOS[9847:515045] newkey=名字 / oldKey=<null>
2020-10-28 23:07:35.531069+0800 DemoEmpty_iOS[9847:515045] 赋值后 name=名字 / ivarName=昵称

KVO只对属性进行观察 --> setter 方法。
下面对KVO原理进行探究。

2、KVO原理

1)运行工程,查看self.personisa:

self.person = [[MyPerson alloc]init];// MyPerson
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];// NSKVONotifying_MyPerson
/*
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x000000010d02317a "MyPerson"
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $2 = 0x00006000023768e0 "NSKVONotifying_MyPerson"
*/

添加KVO监听后,self.personisa指向由MyPerson变成了一个中间类 - NSKVONotifying_MyPerson.NSKVONotifying_MyPerson是个什么类?
在监听前后分别输出打印下所有类子类 与 方法:

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls {
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];// 先把 cls 自己放进去
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"controller:classes = %@", mArray);
}

输出结果:

2020-10-28 23:30:45.280579+0800 DemoEmpty_iOS[9977:527858] controller:classes = (
    MyPerson,
    MySubPerson
)
2020-10-28 23:30:51.984501+0800 DemoEmpty_iOS[9977:527858] controller:classes = (
    MyPerson,
    "NSKVONotifying_MyPerson",
    MySubPerson
)

NSKVONotifying_MyPersonMyPerson的一个派生子类。

2)MyPersonNSKVONotifying_MyPerson的方法

2.1、分别输出2者的全部方法
#pragma mark - 遍历方法   - ivar - property
- (void)printClassAllMethod:(Class)cls {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

MyPerson 的所有方法:

2020-10-28 23:42:31.320759+0800 DemoEmpty_iOS[10015:533265] nick-0x10bc2aa50
2020-10-28 23:42:31.320985+0800 DemoEmpty_iOS[10015:533265] setNick:-0x10bc2aa80
2020-10-28 23:42:31.321095+0800 DemoEmpty_iOS[10015:533265] personInstanceOne-0x10bc2a980
2020-10-28 23:42:31.321274+0800 DemoEmpty_iOS[10015:533265] mDataArray-0x10bc2aac0
2020-10-28 23:42:31.321421+0800 DemoEmpty_iOS[10015:533265] setMDataArray:-0x10bc2aae0
2020-10-28 23:42:31.321563+0800 DemoEmpty_iOS[10015:533265] .cxx_destruct-0x10bc2ab60
2020-10-28 23:42:31.321688+0800 DemoEmpty_iOS[10015:533265] name-0x10bc2a9e0
2020-10-28 23:42:31.321800+0800 DemoEmpty_iOS[10015:533265] setName:-0x10bc2aa10
2020-10-28 23:42:31.321922+0800 DemoEmpty_iOS[10015:533265] age-0x10bc2ab20
2020-10-28 23:42:31.322032+0800 DemoEmpty_iOS[10015:533265] setAge:-0x10bc2ab40

NSKVONotifying_MyPerson 的所有方法:

2020-10-28 23:50:47.534742+0800 DemoEmpty_iOS[10055:538121] setName:-0x7fff25721c7a
2020-10-28 23:50:47.534892+0800 DemoEmpty_iOS[10055:538121] class-0x7fff2572073d
2020-10-28 23:50:47.534990+0800 DemoEmpty_iOS[10055:538121] dealloc-0x7fff257204a2
2020-10-28 23:50:47.535088+0800 DemoEmpty_iOS[10055:538121] _isKVOA-0x7fff2572049a
2.2、问题:NSKVONotifying_MyPerson是继承还是重写呢?

新建继承自MyPerson的类MySubPerson,输出其全部方法
--> 没有任何输出!
--> 在MySubPerson中实现任一方法(例:setNick:),输出结果如下:

2020-10-29 00:08:21.050077+0800 DemoEmpty_iOS[10196:549729] setNick:-0x103515530

对比NSKVONotifying_MyPerson,完全不同。
可知NSKVONotifying_MyPerson的方法不是继承来的,而是自己重写的,且方法一定是有实现的。

3)setName

问题:setter方法是如何重写了父类的属性的呢?

3.1、添加观察点

addObserver前断点,给观察的属性name添加观察点 watchpoint set variable self->_person->_name,添加成功:

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

放开断点,继续执行,当name发生变化时,watchpoint 1 hit

Watchpoint 1 hit:
old value: 0x0000000000000000
new value: 0x000000010b493108

此时堆栈信息见下图:

image.png

查看2、3、4的内容信息:
2中信息如下:

image.png

更详细内容:

    0x7fff257273ea <+457>: leaq   -0x3a7e(%rip), %r8        ; NSKeyValueWillChangeBySetting
    0x7fff257273f1 <+464>: movq   -0x590(%rbp), %r9
    0x7fff257273f8 <+471>: pushq  $0x0
    0x7fff257273fa <+473>: pushq  %rbx
    0x7fff257273fb <+474>: leaq   0x4da(%rip), %rax         ; NSKeyValuePushPendingNotificationLocal
    0x7fff25727402 <+481>: pushq  %rax
    0x7fff25727403 <+482>: callq  0x7fff257275d2            ; NSKeyValueWillChange
    0x7fff25727408 <+487>: addq   $0x20, %rsp
    0x7fff2572740c <+491>: incq   %r12
    0x7fff2572740f <+494>: cmpq   %r12, %r14
    0x7fff25727412 <+497>: jne    0x7fff257273c6            ; <+421>
    0x7fff25727414 <+499>: cmpq   $0x0, -0x570(%rbp)
    0x7fff2572741c <+507>: je     0x7fff2572747f            ; <+606>
    0x7fff2572741e <+509>: movb   $0x0, -0x540(%rbp)
    0x7fff25727425 <+516>: decq   %r14
    0x7fff25727428 <+519>: js     0x7fff2572747f            ; <+606>
    0x7fff2572742a <+521>: leaq   -0x560(%rbp), %rbx
    0x7fff25727431 <+528>: leaq   -0x3ac5(%rip), %r12       ; NSKeyValueWillChangeBySetting
    0x7fff25727438 <+535>: movq   -0x598(%rbp), %rax
... 信息太多 省略一部分 ...
    0x7fff25727469 <+584>: leaq   0x46c(%rip), %rax         ; NSKeyValuePushPendingNotificationLocal
    0x7fff25727470 <+591>: pushq  %rax
    0x7fff25727471 <+592>: callq  0x7fff257275d2            ; NSKeyValueWillChange
    0x7fff25727476 <+597>: addq   $0x20, %rsp
    0x7fff2572747a <+601>: decq   %r14
    0x7fff2572747d <+604>: jns    0x7fff25727438            ; <+535>
    0x7fff2572747f <+606>: movq   -0x550(%rbp), %rax
    0x7fff25727486 <+613>: movq   %rax, -0x578(%rbp)
    0x7fff2572748d <+620>: movq   -0x548(%rbp), %r14
    0x7fff25727494 <+627>: movq   -0x588(%rbp), %rbx
    0x7fff2572749b <+634>: movq   0x10(%rbp), %rdi
    0x7fff2572749f <+638>: testq  %rdi, %rdi
    0x7fff257274a2 <+641>: je     0x7fff257274a7            ; <+646>
    0x7fff257274a4 <+643>: callq  *0x10(%rdi)
->  0x7fff257274a7 <+646>: testq  %r14, %r14
    0x7fff257274aa <+649>: jle    0x7fff2572750a            ; <+745>
    0x7fff257274ac <+651>: leaq   -0x560(%rbp), %rax
    0x7fff257274b3 <+658>: movq   -0x578(%rbp), %rcx
    0x7fff257274ba <+665>: movq   %rcx, (%rax)
    0x7fff257274bd <+668>: movq   %r14, 0x8(%rax)
    0x7fff257274c1 <+672>: xorl   %ecx, %ecx
    0x7fff257274c3 <+674>: movq   %rcx, 0x20(%rax)
    0x7fff257274c7 <+678>: movq   %rcx, 0x18(%rax)
    0x7fff257274cb <+682>: movq   %rcx, 0x10(%rax)
    0x7fff257274cf <+686>: movq   -0x568(%rbp), %rcx
    0x7fff257274d6 <+693>: movq   %rcx, 0x28(%rax)
    0x7fff257274da <+697>: subq   $0x8, %rsp
    0x7fff257274de <+701>: leaq   -0x39ff(%rip), %rcx       ; NSKeyValueDidChangeBySetting
    0x7fff257274e5 <+708>: leaq   0x77f(%rip), %r9          ; NSKeyValuePopPendingNotificationLocal
... 信息太多 省略一部分 ...
    0x7fff25727501 <+736>: callq  0x7fff25727a10            ; NSKeyValueDidChange
    0x7fff25727506 <+741>: addq   $0x10, %rsp
    0x7fff2572750a <+745>: movq   -0x568(%rbp), %rdi
    0x7fff25727511 <+752>: callq  *0x5b23a951(%rip)         ; (void *)0x00007fff51411000: objc_release
    0x7fff25727517 <+758>: testq  %r13, %r13
    0x7fff2572751a <+761>: je     0x7fff25727534            ; <+787>
    0x7fff2572751c <+763>: xorl   %ebx, %ebx
    0x7fff2572751e <+765>: movq   0x5b23a943(%rip), %r14    ; (void *)0x00007fff51411000: objc_release
    0x7fff25727525 <+772>: movq   (%r15,%rbx,8), %rdi

通过上面信息可知,断点在0x7fff257274a7 <+646>: testq %r14, %r14处,在此前后流程,汇编代码流程中分别有 NSKeyValueWillChangeNSKeyValueDidChange,而NSKeyValueDidChange前又走进了父类的setter方法;
可得知 流程 -->NSKeyValueWillChange--> 父类[MyPerson setName]-->NSKeyValueDidChange-->objc_release
这里即可验证父类的属性值为何会变化。

4)问:NSKVONotifying_MyPerson会被移除吗?isa是否会指回MyPerson?

- (void)dealloc {
    [self printClassAllMethod:objc_getClass("NSKVONotifying_MyPerson")];

    [self.person removeObserver:self forKeyPath:@"name" context:NULL];

    [self printClassAllMethod:objc_getClass("NSKVONotifying_MyPerson")];
}

析构函数中的removeObserver前后依次输出信息如下:

2020-10-29 00:29:47.516214+0800 DemoEmpty_iOS[10221:558663] setName:-0x7fff25721c7a
2020-10-29 00:29:47.516397+0800 DemoEmpty_iOS[10221:558663] class-0x7fff2572073d
2020-10-29 00:29:47.516561+0800 DemoEmpty_iOS[10221:558663] dealloc-0x7fff257204a2
2020-10-29 00:29:47.516654+0800 DemoEmpty_iOS[10221:558663] _isKVOA-0x7fff2572049a

(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000060000324bc00 "NSKVONotifying_MyPerson"
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x000000010e1ca17c "MyPerson"

2020-10-29 00:30:03.485147+0800 DemoEmpty_iOS[10221:558663] setName:-0x7fff25721c7a
2020-10-29 00:30:03.485293+0800 DemoEmpty_iOS[10221:558663] class-0x7fff2572073d
2020-10-29 00:30:03.485441+0800 DemoEmpty_iOS[10221:558663] dealloc-0x7fff257204a2
2020-10-29 00:30:03.485540+0800 DemoEmpty_iOS[10221:558663] _isKVOA-0x7fff2572049a
(lldb) 

返回到其他页面再次输出,NSKVONotifying_MyPerson仍是存在的。
答:由上可得知,在移除监听的观察者后,isa指回了MyPerson。而NSKVONotifying_MyPerson会一直在内存中并不会移除。这里若下次再进来不必再次开辟加载,节省了时间性能。
KVO Demo 地址

总结:

  1. KVO 的实现使用了isa-swizzing
  2. 中间类NSKVONotifying_clsNameclsName的子类,并重写了方法;
  3. NSKVONotifying_clsName不会被销毁,一直存在在内存中,生命和普通类相同;
  4. 析构 removeObserver后,对象的isa会指回clsName

三、自定义 KVO

模拟系统,自定义实现 KVO
思路:

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