Key-Value Observing Programming Guide

官方文档点这里:
Key-Value Observing Programming GuideNSKey​Value​Observing
Key-Value Coding Programming GuideNSKey​Value​Coding


Introduction


Introduction to KVO Programming Guide


Key-value observing 是一个机制,允许在别的对象属性发生变化时,收到通知。

为了理解 kvo,要先了解 kvc。


At a Glance

kvo 对 Model 层和 controller 层之间的通信有重要作用。控制器通过 kvo 观察模型对象,视图层也通过控制器来观察模型对象。另外,模型对象也可能观察模型对象。

kvo 可以观察任意的对象属性,包括 simple attributes, to-one relationships, 和 to-many relationships。attributes 可以理解为标量,比如 int,或者 NSNumber 对象。to-one relationships 可以理解为一个对象,比如一个学生类对象。to-many relationships 可以理解为集合对象,比如 NSArray 对象。观察 to-many relationships 的观察者可以被通知变化的类型和发生变化的对象。

要使用 kvo,首先要确认被观察的对象实现了 kvo。NSObject 和子类实现了 kvo。也可以手动实现 kvo,具体看 KVO Compliance 部分。

其次,被观察者调用 addObserver:forKeyPath:options:context: 方法注册和添加观察者。当观察者收到通知的时候,会调用 observeValueForKeyPath:ofObject:change:context: 函数进行处理。

最后,观察者不想接收通知了,在被释放内存之前,要调用 removeObserver:forKeyPath: 方法,移除观察者。


Registering for Key-Value Observing



使用 kvo 的3个步骤:

  • 被观察者调用 addObserver:forKeyPath:options:context: 方法注册和添加观察者。
  • 观察者实现 observeValueForKeyPath:ofObject:change:context: 方法,收到通知的时候会调用。
  • 不再接收通知,要在释放内存之前,调用 removeObserver:forKeyPath: 方法移除观察者。


Registering as an Observer

- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(void *)context;


// demo 
// 假设B有个数组属性
@property (nonatomic, strong) NSMutableArray *mArray;
// 注册观察者A。注意,如果 keyPath 是".mArray"会抛出字符串 NSRangeException 异常。
[B addObserver:A forKeyPath:@"mArray" options: NSKeyValueObservingOptionNew context: NULL];
  • 注册观察者,接收 kvo 通知。
  • observer,观察者,必须实现 *observeValueForKeyPath: ofObject: change: context: *方法,收到通知时会调用。
  • key​Path,发生变化的属性。不能为 nil。
  • options,定义 kvo 通知要传递的内容,有 NSKeyValueObservingOptionNew、Old、Initial、Prior。比如 New 意味着通知的 change 字典包含变化之后的值,具体看 NSKey​Value​Observing​Options 部分。
  • context,随意的数据,传给观察者的 observeValueForKeyPath: ofObject: change: context: 方法。
  • 不会 retain 观察者和被观察者(Neither the object receiving this message, nor observer, are retained.)。记得调用 remove Observer: for Key Path: 或 remove Observer: for Key Path: context: 移除观察者。


Options

影响通知提供给观察者的 change dictionary 中的内容,以及通知的方式。

使用 NSKeyValueObservingOptionOld 可以获得旧值,NSKeyValueObservingOptionNew 可以获得新值。可以两个同时使用。

使用 NSKeyValueObservingOptionInitial 可以获得初始值,被观察者会立即发送一个通知(在 addObserver:forKeyPath:options:context: 返回之前)。NSKeyValueObservingOptionInitial 不单独使用,也不和 OptionOld 一起使用,而是和 OptionNew 一起使用,因为对于观察者来说,第一次通知时,属性的值就是新的。

使用 NSKeyValueObservingOptionInitial 代码如下所示:

// 注册观察者时,创建 option
NSKeyValueObservingOptions option = NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew;

// 观察者处理通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 输出属性初始值
    NSLog(@"new = %@", change[NSKeyValueChangeNewKey]);
    // 输出 null
    NSLog(@"old = %@", change[NSKeyValueChangeOldKey]);
}

使用 NSKeyValueObservingOptionPrior 可以在值改变之前和改变之后收到通知。官方文档:“You instruct the observed object to send a notification just prior to a property change (除此之外,正常的通知在值改变之后发送,in addition to the usual notification just after the change) by including the option NSKeyValueObservingOptionPrior”。值改变之前的通知的 change 字典里面有一个 NSKeyValueChangeNotificationIsPriorKey,对应的值是用 @(YES) 装箱的 NSNumber 对象。利用提前发送的通知,可以在手动实现 kvo 的时候,有机会调用 - willChangeValueForKey: 方法。

使用 NSKeyValueObservingOptionPrior 代码的断点调试:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 此处有断点。会执行两次。
    // NSLog(@"%@", change[NSKeyValueChangeNotificationIsPriorKey]); // 输出 1

}

值改变之前的输出结果:
(lldb) po change
{
    kind = 1;
    notificationIsPrior = 1;
}

值改变之后的输出结果:
(lldb) po change
{
    kind = 1;
}


Context

用于观察者判断通知来源。可以传 NULL,只根据 key path 来判断。但是这样在某些情况会引起问题,比如对同一个 key path 注册多次观察者,或者父类也对这个 key path 注册了观察者。可以根据 context 来判断是否要调用 super 的方法来处理通知。子类在移除观察者的时候也可以根据 context 来选定要移除的是自己而不是父类。

独一无二的静态变量的地址可以作为不错的 context。可以为一个类定义一个 context,也可以为每个属性都定义一个 context,这样可以免去对 key path 的比较判断,提高效率。

创建 context 的例子:

Listing 1  Creating context pointers

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

Person、Account 是类名,balance、interestRate 是属性。void *是一个指针,变量创建的时候就会分配一块内存,用&取地址后赋值给指针变量就行,不用管内存里保存的值是多少。

Listing 2 显示使用 context 注册观察者的例子:

Listing 2  Registering the inspector as an observer of the balance and interestRate properties

- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

使用 self 作为 context 可以吗?试了两种情况,第一种失败了:

void *context = &self; // 编译器会警告
if (context == &self) {
  // 不会进来,因为比较结果是 nil。
}

第二种是成功的,使用桥接:

// 定义 context
void *context = (__bridge void *)(self);

// 处理通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == (__bridge void *)(self)) {
        NSLog(@"use __bridge void *self");
    }
}

注意
addObserver:forKeyPath:options:context: 方法不会 maintain 观察者、被观察者和 context。


Receiving Notification of a Change

发生变化时,观察者会收到 observeValueForKeyPath:ofObject:change:context: 消息,所有的观察者都要实现这个函数。

change 字典包含了变化的信息,context 是注册观察者的时候提供的。

change 字典可能包含的 key 有:NSKeyValueChangeKindKey、NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKeyValueChangeIndexesKey、NSKey​Value​Change​Notification​Is​Prior​Key,分别对应变化类型、变化后的值、变化前的值、发生变化的元素下标、是否变化前后都发送通知。

NSKeyValueChangeKindKey 肯定是有的,表示变化类型,对应的值可以是 NSKeyValueChangeSetting、NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement。

NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKey​Value​Change​Notification​Is​Prior​Key,由注册观察者时使用的 NSKeyValueObservingOptions 决定。NSKeyValueObservingOptions 的值可以是 NSKeyValueObservingOptionOld、NSKeyValueObservingOptionNew、NSKey​Value​Observing​Option​Initial、NSKey​Value​Observing​Option​Prior。注册观察者时,NSKeyValueObservingOptionOld 对应 change 字典的 NSKeyValueChangeOldKey,NSKeyValueObservingOptionNew 对应 NSKeyValueChangeNewKey,NSKey​Value​Observing​Option​Prior 对应 NSKey​Value​Change​Notification​Is​Prior​Key。

NSKeyValueChangeIndexesKey 在被观察的是集合对象(比如数组)时,字典会包含有,表示发生变化的元素下标。注意是数组的元素发生变化,而不是数组的元素的属性发生变化。比如数组里放的是 UIView 对象 view,是 view 被删除或者替换掉,而不是 view 的背景色改变颜色。

使用 NSKeyValueChangeIndexesKey 的示例代码:

// 初始化数组
self.mArray = [NSMutableArray new];
[self.mArray addObject:[UIView new]];

// 注册观察者
[self addObserver:self forKeyPath:@"mArray" options: option context:contex];

// 修改数组。这里会引发通知,变化类型是 NSKeyValueChangeReplacement,
// change 字典里面有 NSKeyValueChangeIndexesKey 及其对应的 NSIndexSet 对象。
NSMutableArray * mArray = [self mutableArrayValueForKeyPath:@"mArray"];
mArray[0] = @"99"; 

// 在处理通知的函数里面断点调试,获取 NSIndexSet 对象,用 po 输出 change 字典
NSIndexSet *indexSet = change[NSKeyValueChangeIndexesKey];
(lldb) po change
{
    indexes = "<_NSCachedIndexSet: 0x60800003b360>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 4;
    new =     (
        99
    );
}

// 注意,如果是这样修改数组,不会引发通知。
self.mArray[0] = @"99";

// 如果是这样修改,会引发通知,类型是 NSKeyValueChangeSetting
self.mArray = nil;
// 断点输出
(lldb) po change
{
    kind = 1;
    new = "<null>";
}

change 字典中的 NSKeyValueChangeKindKey 提供了变化类型。如果是属性的值发生变化,NSKeyValueChangeKindKey 对应的值就是 NSKeyValueChangeSetting,那么字典中可能会有 NSKeyValueChangeOldKey、NSKeyValueChangeNewKey,两个key 分别对应属性变化前和变化后的值。字典是否包含这两个 key,由注册观察者时的 NSKeyValueObservingOptions 决定,比如 NSKeyValueObservingOptionOld、NSKeyValueObservingOptionNew。

如果观察的属性是一个 to-many relationship(比如数组),当它的元素发生变化时,NSKeyValueChangeKindKey 对应的值可能是 NSKeyValueChangeInsertion、NSKeyValueChangeRemoval 或者 NSKeyValueChangeReplacement,分别是插入、删除、替换的意思。

字典的 NSKeyValueChangeIndexesKey 是一个 NSIndexSet 对象,表示发生变化的元素的位置。如果注册观察者的时候,使用了 NSKeyValueObservingOptionNew 或者 NSKeyValueObservingOptionOld,那么字典的 NSKeyValueChangeOldKey 或者 NSKeyValueChangeNewKey 对应一个数组,包含发生变化前或者变化后的相关元素。

Listing 3 显示了处理通知的例子,一个 Person 类对象观察 Account 类对象的 balance、interestRate 属性:

Listing 3  Implementation of observeValueForKeyPath:ofObject:change:context:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

上面的例子可以看到 context 的用处,不用比较字符串,而且可以区分子类和父类。

注意
如果通知传递到了类层的顶端,NSObject 会抛出一个 NSInternalInconsistencyException 异常,因为子类注册观察者收到的通知,没有进行处理。因为不是父类观察的,所以肯定也不会处理。


Removing an Object as an Observer

Listing 4 演示移除观察者:

Listing 4  Removing the inspector as an observer of balance and interestRate

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

移除观察者的注意事项:

  • 移除非观察者会引发异常。如果不确定移除次数,可以放在 try/catch 块处理。
  • 观察者在内存中释放时,不会自动移除,被观察者会继续发送通知。和其他消息一样,发送给释放掉的对象,会引发内存访问异常。
  • 一般是在 init 或者 viewDidLoad 注册观察者,在 dealloc 移除。


KVO Compliance


kvo 有两种方式发送通知:自动和手动。子类通过重写 automaticallyNotifiesObserversForKey: 方法,控制自动发送通知。


Automatic Change Notification

Listing 1 显示自动通知:

Listing 1  Examples of method calls that cause KVO change notifications to be emitted

// Call the accessor method.
[account setName:@"Savings"];
 
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
 
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>; // 伪代码
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];


Manual Change Notification

有些情况需要手动通知,比如尽量减少不必要的通知数量,或者把一些类型的变化合并到一个通知来发送。

手动和自动通知不是互斥的。如果想关闭自动通知,可以重写 automaticallyNotifiesObserversForKey: 方法,对某些 key 返回 NO。对于子类不处理的 key,要调用 super 来处理。

Listing 2 显示重写的例子:

Listing 2  Example implementation of automaticallyNotifiesObserversForKey:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

要实现手动通知,在变化发生前,调用 willChangeValueForKey:,变化发生后调用 didChangeValueForKey:。

Listing 3 实现手动通知:

Listing 3  Example accessor method implementing manual notification

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

可以通过检查值是否变化来减少通知的发送,如 Listing 4:

Listing 4  Testing the value for change before providing notification

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

多个值变化还可以嵌套发送通知,如 Listing 5:

Listing 5  Nesting change notifications for multiple keys

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}

对于 ordered to-many relationship(比如数组),不仅要指定 key,还要指定变化的类型,和发生变化的元素下标。变化类型是 NSKeyValueChange 定义的 NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement。下标是 NSIndexSet 对象。如 Listing 6 所示:

Listing 6  Implementation of manual observer notification in a to-many relationship

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}


Registering Dependent Keys


有些属性是相互依赖的,一个变化会引起其他属性跟着变化。通过重写 keyPathsForValuesAffectingValueForKey: 方法定义依赖属性。


To-One Relationships

对于 to-one relationship,两种方法实现属性依赖,一种是重写 keyPathsForValuesAffectingValueForKey: 方法,另一种是实现类似 keyPathsForValuesAffecting<Key> 的方法(比如 keyPathsForValuesAffectingAge)。

比如 full name 由 first 和 last name 决定:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

fullName 的观察者应该在 firstName 或者 lastName 发生变化时收到通知。

Listing 1 显示重写 keyPathsForValuesAffectingValueForKey: 方法的例子:

Listing 1  Example implementation of keyPathsForValuesAffectingValueForKey:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

重写要先调用父类的方法,然后把依赖的属性名放入一个数组,再添加到父类方法返回的 set 对象。

另一个方法是实现 keyPathsForValuesAffecting<Key> 方法,如 Listing 2 所示:

Listing 2  Example implementation of the keyPathsForValuesAffecting<Key> naming convention

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

不能在 category 中重写 keyPathsForValuesAffectingValueForKey: 方法,因为会覆盖系统方法。这种情况可以在 category 添加 keyPathsForValuesAffecting<Key> 方法实现属性依赖。


To-Many Relationships

对于 to-many relationships, 不能通过重写 keyPathsForValuesAffectingValueForKey: 方法实现属性依赖。

比如 Department 对象有个 to-many relationship (employees),存放 Employee 对象。Department 有 totalSalary 属性,依赖于 Employee 的 salary 属性。不能重写 keyPathsForValuesAffectingValueForKey: 方法返回 employees.salary 作为依赖属性的 keyPath。

两种方法,一种是使用 kvo 注册 parent (in this example, Department) 成为所有 children (Employees in this example) 的相关属性(这里指 salary)的观察者。在添加和删除 child 的时候, parent 要注册或者注销观察者。然后某个 child 的 salary 属性发生变化的时候,parent 在 observeValueForKeyPath:ofObject:change:context: 方法中更新自己的 totalSalary 属性并通知自己的观察者。如 Listing 3 所示:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    // 这里是使用 kvc 的操作符,对数组 employees 的元素求和
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

另一种方法和 Core Data 相关,If you're using Core Data, you can register the parent with the application's notification center as an observer of its managed object context. The parent should respond to relevant change notifications posted by the children in a manner similar to that for key-value observing


Key-Value Observing Implementation Details


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

自动 kvo 是通过 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.

isa 指针指向对象的 class,class 维护一张分发表,分发表包含了 class 实现的方法的方法指针和其他数据。

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.

注册观察者的时候,被观察者的 isa 指针被修改了,指向一个中间类,结果就是 isa 指针不一定能反映对象真实的 class。

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.

不要用 isa 指针来判断对象所属的 class,应该使用 class 方法 来判断。

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

推荐阅读更多精彩内容