Key-Value Coding Programming Guide

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


Getting Started


About Key-Value Coding


Key-value coding 允许间接访问实现了 NSKeyValueCoding 非正式协议的对象的属性,间接访问的功能是由直接访问功能提供的。间接访问是指通过字符串,直接访问是指实例变量和访问器,比如 getter。

通过变量名访问实例变量,或者通过访问器访问属性,是比较直接的,但随着属性数量的增加或者修改,访问这些属性的代码也要跟着修改。与之相比,kvc 通过字符串访问属性就简洁多了。


Using Key-Value Coding Compliant Objects

直接或间接继承自 NSObject 的对象,都遵循 NSKeyValueCoding 协议。后面的章节会详细介绍,使用这些对象可以做下列事情:

  • Access object properties.
  • Manipulate collection properties.
  • Invoke collection operators on collection objects.
  • Access non-object properties.
  • Access properties by key path.


Adopting Key-Value Coding for an Object

自定义对象实现 NSKeyValueCoding 协议就可以使用 kvc。 NSObject 的子类已经实现该协议。为了使 kvc 能正常工作,要保证访问器和实例变量遵守一些定义好的规则(文档并没有说是哪些规则)。


Key-Value Coding with Swift

在 Swift 中,NSObject 的子类都能使用 kvc。和 OC 不一样,Swift 的属性自动遵循 kvc 的一些规则。有些特点不适合 Swift,比如 Swift 的属性都是对象,kvc 中处理非对象属性的规则就不适用于 Swift。


Other Cocoa Technologies Rely on Key-Value Coding

kvc 是很多功能的基础,详情请看官方文档:

  • Key-value observing.
  • Cocoa bindings.
  • Core Data.
  • AppleScript.


Key-Value Coding Fundamentals


Accessing Object Properties


对象头文件中定义的属性,属于下列中的一种:

  • Attributes. These are simple values, such as a scalars, strings, or Boolean values. Value objects such as NSNumber and other immutable types such as as NSColor are also considered attributes.
  • To-one relationships. These are mutable objects with properties of their own. An object’s properties can change without the object itself changing. For example, a bank account object might have an owner property that is an instance of a Person object, which itself has an address property. The owner’s address may change without changing the owner reference held by the bank account. The bank account’s owner did not change. Only her address did.
  • To-many relationships. These are collection objects. You commonly use an instance of NSArray or NSSet to hold such a collection, although custom collection classes are also possible.
Listing 2-1Properties of the BankAccount object

@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
 
@end


Identifying an Object’s Properties with Keys and Key Paths

key 是一个字符串,和属性同名。key 必须使用 ASCII 编码,不能包含空格,通常是小写字母开头。

// 这两句效果是一样的,使用 kvc 更灵活。
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
[myAccount setCurrentBalance:@(100.0)];

key path 是一个用点和 keys 连接成的字符串,比如 owner.address.street。


Getting Attribute Values Using Keys

遵循 NSKeyValueCoding 协议的对象可以使用 kvc 。NSObject 已经实现了该协议,因此它的子类都可以使用 kvc。至少可以使用下列函数:

  • valueForKey: ,通过字符串获取属性值。如果找不到属性,会调用 valueForUndefinedKey: 函数,该函数默认实现是抛出 NSUndefinedKeyException 异常。
  • valueForKeyPath:,默认调用 valueForKey:。
  • dictionaryWithValuesForKeys:,传入一个字符串数组,返回一个包含所有 key 和对应 value 的字典。默认调用 valueForKey:。

注意
NSArray, NSSet, and NSDictionary 不能包含 nil,使用 NSNull 对象代替。 NSNull 提供单一实例,代表 nil 对象。dictionaryWithValuesForKeys: and the related setValuesForKeysWithDictionary:会自动转换 nil 和 NSNull。

NSArray *array = @[@123, [NSNull null], @456];
// nil, Nil, NULL, NSNull, [NSNull null] 的区别?
// nil 用于对象,Nil 用于类,NULL 用于非对象类型的变量,[NSNull null] 用于数组和字典。

如果 key path 访问的是 to-many relationship,那么返回值是一个 collection,包含 key 对应的所有的值。比如 transactions.payee,返回一个数组,包含每一个 transaction 的 payee。这个对多层数组也适用,比如 accounts.transactions.payee,返回一个数组,包含所有的 account 的 transaction 的 payee。


Setting Attribute Values Using Keys

NSObject 实现了 NSKeyValueCoding 协议的几个 setter 方法:

  • setValue:forKey:,自动对 NSValue 和 NSNumber 拆箱,然后给 标量和结构体属性赋值。详情请看 Representing Non-Object Values 章节。如果找不到 key 对应的属性,会调用 setValue:forUndefinedKey:,该函数默认抛出 NSUndefinedKeyException 异常。
  • setValue:forKeyPath:,默认调用 setValue:forKey:。
  • setValuesForKeysWithDictionary: ,默认调用 setValue:forKey:,使用 NSNull 对象代替 nil。

对非对象属性设置 nil 值,系统会调用 setNilValueForKey:,该函数默认抛出 NSInvalidArgumentException 异常,详情请看 Handling Non-Object Values 章节。


Using Keys to Simplify Object Access

使用 kvc 精简代码的例子:在 macOS 中,NSTableView 和 NSOutlineView 的每一列都有一个 identifier 字符串相对应。在数据源方法中,根据 identifier 判断是给哪一列赋值,然后获取数据模型对应的属性值,代码如 Listing 2-2 所示。

Listing 2-2 Implementation of data source method without key-value coding

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    id result = nil;
    Person *person = [self.people objectAtIndex:row];
 
    if ([[column identifier] isEqualToString:@"name"]) {
        result = [person name];
    } else if ([[column identifier] isEqualToString:@"age"]) {
        result = @([person age]);  // Wrap age, a scalar, as an NSNumber
    } else if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        result = [person favoriteColor];
    } // And so on...
 
    return result;
}

在代码 Listing 2-3 中,省略了对 identifier 的判断,直接用 kvc 获取 identifier 对应的属性值。更重要的是,以后增加了列名和数据模型属性,这里的代码也不用修改了,因为是通过 identifier 来获取对应的属性值。

Listing 2-3Implementation of data source method with key-value coding

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}


Accessing Collection Properties


对于数组和集合,也可以使用 valueForKey: 和 setValue:forKey: 方法获取和设置。但如果要修改其中的元素,使用代理方法更加高效。

三种代理方法:

  • mutableArrayValueForKey: 和 mutableArrayValueForKeyPath:
    返回一个代理对象,行为类似 NSMutableArray object。
  • mutableSetValueForKey: 和 mutableSetValueForKeyPath:
    返回一个代理对象,行为类似 NSMutableSet object。
  • mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath:
    返回一个代理对象,行为类似 NSMutableOrderedSet object。

假设有一个属性,不可变的数组对象:

@property (nonatomic, strong) NSArray *array;

// 初始化数组
self.array = @[@1,@2,@3];

使用 kvc 修改数组元素,比如增删改,方法一:

NSArray *array = [self valueForKey:@"array"];
// 用 array 的值创建一个新的数组
array = ...
[self setValue:array forKey:@"array"];

方法一比较麻烦,要先获取原来的数组,然后创建新的数组,最后再通过 kvc 赋值。

使用代理就简单很多,方法二:

NSMutableArray *ma = [self mutableArrayValueForKey:@"array"];
ma[0] = @998; // 变化类型是 NSKeyValueChangeReplacement

这种方法代码简单,效率更高,而且有额外的 kvo 好处,也就是当数组发生增删改时,观察者可以收到通知:

// 注册观察者
[self addObserver:self forKeyPath:@"array" options: NSKeyValueObservingOptionNew context:NULL];

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

对可变和不可变的数组、集合都有用,有时候比可变数组或集合自带的增删改方法都要高效。当数组元素发生改变想收到通知时,可以使用这些代理方法。


Using Collection Operators


在使用 valueForKeyPath:时,可以在 key path 中加一个@和集合操作符。

如图 4-1所示,中间是操作符,左边是目标集合,右边是集合中的元素的属性。如果是给集合对象发送消息 valueForKeyPath:,比如数组,左边可以省略。

Figure 4-1Operator key path format
// demo
// @avg 是求平均值。对数组 transactions 中的元素的 amount 属性求平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];


Sample Data

BankAccount 类:

Listing 2-1Properties of the BankAccount object

@interface BankAccount : NSObject

@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation

@end

Transaction 类:

Listing 4-1Interface declaration for the Transaction object

@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
 
@end

数组 transactions 数据:

Table 4-1Example data for the Transactions objects


Aggregation Operators

用于 array 和 set。

@avg

求平均值。读取集合中的每一个元素,转换成 double 类型(用 0 代替 nil 值),求平均值后返回一个 NSNumber 对象。

// @avg 是操作符,amount 是属性名。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];

用 Table 4-1的数据算的结果是 $456.54。


@count

求个数,返回一个 NSNumber 对象。

NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];

结果是 13。


@max

求最大值。比较的过程使用 compare: 函数,忽略 nil 值。

NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];

结果是 Jul 15, 2016。


@min

求最小值。比较的过程使用 compare: 函数,忽略 nil 值。

NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];

结果是 Dec 1, 2015。


@sum

求和。读取集合中的每一个元素,转换成 double 类型(用 0 代替 nil 值),求和后返回一个 NSNumber 对象。

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];

结果是 $5,935.00。


Array Operators

创建并返回一个数组。注意,key path 中有 nil 值会引起异常。


@distinctUnionOfObjects

创建并返回一个数组,包含 right key path 指定的属性。会去掉重复的属性。

NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

数组 distinctPayees 里面都是 payee,分别是 Car Loan, General Cable, Animal Hospital, Green Power, Mortgage。


@unionOfObjects

创建并返回一个数组,包含 right key path 指定的属性。保留重复的属性。

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

数组 payees 里面都是 payee,分别是 Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital。


Nesting Operators

用于嵌套集合(集合的元素也是集合)。注意,key path 中有 nil 值会引起异常。

使用 Table 4-2 的数据再创建一个数组 moreTransactions:

NSArray* moreTransactions = @[< transaction data >]; // 右边是伪代码
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
Table 4-2 Hypothetical Transaction data in the moreTransactions array


@distinctUnionOfArrays

创建并返回一个数组,数组包含所有数组的元素的属性。会去掉重复的属性。

NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];

数组 arrayOfArrays 有两个元素,分别是数组 self.transactions 和 moreTransactions。结果 collectedDistinctPayees 包含上述两个数组的所有 payee:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power。


@unionOfArrays

创建并返回一个数组,数组包含所有数组的元素的属性。保留重复的属性。

NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];

结果 collectedDistinctPayees 包含上述两个数组的所有 payee:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop。


@distinctUnionOfSets

创建并返回一个set,包含所有set的元素的属性。和 @distinctUnionOfArrays 是类似的,只不过是对 NSSet 中的 NSSet 进行操作,并且返回的是 NSSet。

假设用的数据和数组的一样,那么结果和 @distinctUnionOfArrays 是一样的。


Representing Non-Object Values

由于 Swift 所有的属性都是对象,所以本章节只适用于 Objective-C properties。

kvc 自动转换对象和非对象属性。非对象属性比如 int、float 等标量,以及结构体。

kvc 默认是调用属性的访问器。当 getter 发现返回值不是对象时,会先转换成 NSNumber 对象(对于标量,比如 int) 或者 NSValue 对象(对于结构体),然后再返回。类似的,当 setter 发现属性不是对象时,会先调用 <type>Value 方法(比如 intValue)拆箱,再给非对象属性进行赋值。

注意
kvc 给非对象属性赋值 nil 时,会调用 setNilValueForKey: ,该函数默认抛出 NSInvalidArgumentException 异常。子类可以重写该函数。详情请看 Handling Non-Object Values 章节。


Wrapping and Unwrapping Scalar Types

标量的装箱和拆箱。装箱是指封装成对象,拆箱是指把对象拆成标量。对于标量,kvc 使用 NSNumber 进行转换。Table 5-1 是标量与 NSNumber 转换表。Data type 是标量类型,Creation method 是创建 NSNumber 对象的方法,Accessor method 是获取 NSNumber 对象的标量值。

// demo 
int age = 18;
// 装箱
NSNumber *ageNumber = [NSNumber numberWithInt:age];
// 也可以使用语法糖
ageNumber = @(age);
ageNumber = @(18);
// 拆箱
age = ageNumber.intValue;
Table 5-1 Scalar types as wrapped in NSNumber objects

注意
在 macOS,对 BOOL 变量调用 setValue:forKey:时,不要传 @“true” 或 @“YES”,而是 @(1) 或 @(YES)。因为在给 BOOL 变量赋值前,会调用 charValue 进行拆箱,但是 NSString 没有实现这个函数,会引起运行时异常。会调用 charValue 进行拆箱是因为历史原因,BOOL 类型定义为 signed char。总之,给 BOOL 变量赋值时,传 NSNumber 对象就行,不要传 NSString 类型。
在 iOS 就没有这个问题。在 iOS 中,kvc 给 BOOl 变量赋值前,会调用 boolValue 进行拆箱,NSString 和 NSNumber 都实现了 boolValue 函数。在 macOS 中,kvc 给 BOOl 变量赋值前,会调用 charValue 进行拆箱,NSString 没有实现这个函数,NSNumber 实现了。


Wrapping and Unwrapping Structures

对于结构体,kvc 使用 NSValue 进行转换。Table 5-2 是 NSPoint, NSRange, NSRect, NSSize 结构体与 NSValue 转换表。Data type 是结构体类型,Creation method 是创建 NSValue 的方法,Accessor method 是转换成结构体的方法。

// demo
CGPoint point = CGPointMake(10, 20);
NSValue *pointValue = [NSValue valueWithCGPoint:point];
point = pointValue.CGPointValue;
Table 5-2 Common struct types as wrapped using NSValue

自定义的结构体如何转换 NSValue 呢?Listing 5-1A 定义了一个结构体和一个类。

Listing 5-1A sample class using a custom structure

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end

myClass 是类 MyClass 的对象。setValue:forKey: 先调用 NSValue 的 getValue: 方法拆箱,然后调用 setThreeFloats: 赋值。

// 结构体转 NSValue
ThreeFloats floats = {1, 2, 3};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

valueForKey: 先调用 threeFloats getter,然后装箱成一个 NSValue 对象再返回。

// NSValue 转结构体
NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats floats2;
[result getValue:&floats2];


Validating Properties


kvc 可以通过 key 或者 key path 访问属性,也可以验证属性。调用 validateValue:forKey:error: (或 validateValue:forKeyPath:error:) 时,默认实现是查找被执行 kvc 的对象,是否有类似 validate<Key>:error: 的函数(比如 validateName:error:),如果没找到,默认验证是成功的,并且默认返回 YES。如果找到了,就用调用 validate<Key>:error: 函数,并且返回它的返回值。

注意
验证属性值只适用于 Objective-C。Swift 使用自带的 willSet 和 didSet 属性可以验证任何的 run-time API。

先看一下函数的定义:

- (BOOL)validateValue:(inout id  _Nullable *)ioValue 
               forKey:(NSString *)inKey 
                error:(out NSError * _Nullable *)outError;
  • 参数 ioValue 是一个指针,指针,指针。如果验证无效,可能会让它指向一个新创建的值。
  • 参数 outError 如果不为 NULL,验证无效的时候可能会让它指向一个 NSError 对象。

验证有3种结果:

  • 值有效,返回 YES,不修改 error 和 value。
  • 值无效,但是选择不修改值。这种情况下,返回 NO,并且如果 error 不为 NULL,修改 error 指向一个 NSError 对象,里面包含无效的原因。
  • 值无效,但是创建一个新的有效值。这种情况下,返回 YES,并且不修改 error。在返回之前,会修改 value 指向新创建的有效值。一般是让 value 指向新创建的值,而不是修改原来的值,即使它是可变的。

Listing 6-1 演示验证的例子:

Listing 6-1 Validation of the name property

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}

示例中的 validateValue:forKey:error: 函数,会查找 validateName:error: 函数,实现可能是这样的:

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidNameCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Name too short" }];
        }
        return NO;
    }
    return YES;
}

另一个验证标量的函数:

- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    if (*ioValue == nil) {
        // Value is nil: Might also handle in setNilValueForKey
        *ioValue = @(0);
    } else if ([*ioValue floatValue] < 0.0) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidAgeCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Age cannot be negative" }];
        }
        return NO;
    }
    return YES;
}

这里年龄无效,修改为新的值后返回一个 YES,或者不修改为新值,error 不为 NULL,就创建一个 NSError 对象,然后返回 NO。

详情请看这里:Adding Validation


Automatic Validation

kvc 一般情况下不会自动验证,如果 app 需要,可以手动验证。

In general, neither the key-value coding protocol nor its default implementation define any mechanism to perform validation automatically. Instead, you make use of the validation methods when appropriate for your app.

有些 Cocoa 技术在某些情况会自动验证,比如 Core Data 当 保存 managed object context 时会自动验证(see Core Data Programming Guide)。


Accessor Search Patterns


kvc 协议方法在寻找 accessors, instance variables, 和 related methods 时,是按一定的规则去寻找的。了解规则对跟踪 kvc 的行为或者让自定义对象实现 kvc 都有好处。

注意
<key> 是一个占位符。For example, for the getters <key> and is<Key>, the property named hidden maps to hidden and isHidden.


Search Pattern for the Basic Getter

  1. 首先按顺序查找访问器 get<Key>, <key>, is<Key>, 或者 _<key>,找到了跳到第5步,否则转第2步。
  2. 查找 countOf<Key> 和 objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) 和 <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:)。
    如果找到第一个和后两个中的至少一个,创建和返回一个能响应所有 NSArray 方法的 collection proxy 对象。否则转第3步。
    剩下这段还是看原文吧:The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.
  3. 查找 countOf<Key>, enumeratorOf<Key>, 和 memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class)。
    如果3个方法都找到了,创建并返回一个能响应所有 NSSet 方法的 collection proxy 对象。
    剩下这段看原文:This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.
  4. 如果接收者的类方法 accessInstanceVariablesDirectly 返回 YES,按顺序查找实例变量 _<key>, _is<Key>, <key>, 或者 is<Key>。如果找到了,直接获取实例变量的值并跳转第5步,否则跳转第6步。
  5. 如果取得的属性值是一个对象指针,直接返回。如果值是标量类型,转换为 NSNumber 对象再返回。如果是结构体,转换为 NSValue 对象再返回。
  6. 如果都失败了,调用 valueForUndefinedKey: 方法,该方法默认抛出一个异常。


Search Pattern for the Basic Setter

  1. 按顺序查找访问器 set<Key>: 或者 _set<Key>,如果找到了就调用(按需要自动拆箱),结束。
  2. 如果类方法 accessInstanceVariablesDirectly 返回 YES,按顺序查找实例变量 _<key>, _is<Key>, <key>, 或者 is<Key>。如果找到了就直接赋值(按需要自动拆箱),结束。
  3. 如果都找不到,调用 setValue:forUndefinedKey: 方法。该方法默认抛出一个异常。


剩下的小节有 Search Pattern for Mutable Arrays、Search Pattern for Mutable Ordered Sets、Search Pattern for Mutable Sets,感兴趣的点这里:Accessor Search Patterns


Adopting Key-Value Coding


NSObject 的子类都遵从 kvc,除非要自己定义一个类才要实现 kvc 协议,这种情况比较少就不记录了。看了一下还是有收获的,感兴趣的点这里:Adopting Key-Value Coding

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

推荐阅读更多精彩内容