《Effective Objective-C 2.0》笔记

1 了解 Objective-C 起源

Objective-C 使用“消息结构”而非“函数调用”。

使用“消息结构”的语言,其运行时所执行的代码由运行环境来决定。
使用“函数调用”的语言,则由编译器决定。

分配在堆内存必须直接管理,而分配在栈上用于保存变量的内存则会在栈帧弹出时自动清理。
Objective-C 将堆内存管理抽象出来了,不需要 malloc 及 free 来分配或释放对象所占内存。
这部分工作抽象为一套内存管理架构:引用计数。

2 在类的头文件中尽量少引入其他头文件

建议:

  1. “向前声明”该类 @class XXClass
  2. 若无法使用“向前声明”,尽量将其挪到实现文件当中。

3 多用字面量语法

即使用语法糖,常用在创建字符串、数组、字典。
当使用语法糖,创建数组、字典时,要避免值中有 nil,否则会有异常。

4 多用类型常量,少用 #define 预处理指令

不要用预处理指令定义常量,因为这样出来的常量,没有类型信息。如果有人重新定义了常量值,编译器也不会报警。
在实现文件中,使用 static const 来定义“只在编译单元内可见的常量”(translation-unit-specific constant),一般以 k 开头。
在头文件中用 extern 声明全局变量,并在实现文件中定义其值,这样的常量会出现在全局符号表中,通常以类型为前缀。

5 用枚举表示状态、选项、状态码

主要使用 NS_ENUM 和 NS_OPTIONS 来定义枚举类型。

在处理枚举类型的 switch 语句中不要实现 default,这样添加类型时,就会收到编译器警告。

6 理解“属性”这一概念

可以使用 @property 来定义对象中所封装的数据。

通过“特质”来指定存储数据所需要的语义,特质有4种:

  • 原子性
  • 读写权限
  • 内存管理语义
  • 方法名

在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。

7 在对象内部尽量直接访问实例变量

在对象内部,读取实例变量时,除非是“懒加载”,否则尽量直接访问实例变量,若是写入数据,应通过属性来写。

在初始化以及 dealloc 方法中,应该直接访问实例变量。
除了此时实例不稳定外,有可能子类会重写 Setter 方法,这样会抛出异常。

8 理解“对象等同性”

若想检测对象的等同性,需要提供 isEqual 和 hash 方法。
相同的对象具有相同的 hash 码,但2个相同 hash 码的对象未必相同。
编写 hash 方法时,应使用计算速度快且 hash 碰撞率低的算法。

9 以“类族”模式隐藏实现细节

常见的有 NSString。

可从类族的公共抽象基类中继承子类,但要格外注意,若有开发文档,应先仔细阅读文档。

10 在既有类中使用关联对象存放自定义数据

当需要给某个对象存放数据,一般是继承该类,然后改用子类。

但有些时候,类是由某种特殊机制产生的,开发者无法使用这种机制创建子类。
这时,可以使用关联对象方式,关键方法:

// 根据给定的键和策略为某对象设置关联对象值
void objc_setAssociatedObject(id object, void*key, id value, objc_AssociationPolicy)
// 根据 key 从某对象中获取相应的关联对象值
id objc_getAssociatedObject(id object, void*key)
// 移除对象的所有关联对象
void objc_removeAssociatedObjects(id object)

通常使用静态全局变量作为 key。

关联类型:

关联类型 等效的 @property 属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

只有在其他方式无法实现时,才考虑使用关联对象,因为它可能会引用难以发现的 bug。

11 理解 objc_msgSend 的作用

对象调用方法,在 Objective-C 上称为方法调用(pass a message)。

“动态消息派发系统”(dynamic message dispatch system)会查找对应方法,并执行相应代码。

原型如下:

void objc_msgSend(id self, SEL cmd, ...)

objc_msgSend 会根据接收者与选择子的类型来调用适当方法,它会在接收者的方法列表(list of methods)中寻找与选择子名称相符的方法,如果找不到,就执行“消息转发”(message forwarding)。

有些“边界情况(edge case)”需要交由 Objective-C 运行环境中的另外一些函数来处理:

  • objc_msgSend_stret
    如果待发送消息要返回结构体,那么可交由此函数处理。
    只有当 CPU 的寄存器能容纳消息返回类型时,该函数才能处理此信息。
    若是无法容纳,那么由另外一个函数执行派发,此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  • objc_msgSend_fpret
    如果消息返回的是浮点数,可交由此函数处理。
    在某些架构的 CPU 上调用函数时,需要对“浮点数寄存器”做特殊处理。
  • objc_msgSendSuper
    如果是给超类发消息,可交由此函数处理。
    也有与上述2个函数等效的方法,用于处理发给 super 的相应消息。

objc_msgSend 等函数一旦找到应该调用的方法实现后,就会“跳转过去”,之所以能这样,是因为 Objective-C 对象的每个方法都可以视为简单的 C 函数,原型如下:

<return type> Class_seletor(id self, SEL _cmd, ...)

每个类里都有一张表格,其中的指针指向这种函数,而选择子的名称正是查表时所用的“键”。

而且原型的样子和 objc_msgSend 函数很像,是为了利用“尾调用优化”技术。

结果缓存在快速映射表(fast map)中,每个类都有这样一块内存,若是稍后还向该类发送同样信息,执行起来就会很快。

12 理解消息转发机制

当对象接收到无法解读的消息后,就会启动“消息转发”机制。

分为2个阶段:

  1. 动态方法解析(dynamic method resolution)
    先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”。
  2. 涉及完整的消息转发机制(full forwarding mechanism)。
    首先,请接收者看看有没有其他对象能处理这条消息。若有,则 runtime 会把消息转给那个对象,于是消息转发过程结束。
    若没有“备援的接收者”(replacemoent receiver),则启动完整的消息转发机制,runtime 系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,让接收者设法解决当前未处理的这条消息。

动态方法解析

// 对象在收到无法解读的消息后,调用下列方法
+ (BOOL)resolveInstanceMethod:(SEL)selector;
// 若未实现的是类方法,则调用以下方法:
+ (BOOL)resolveClassMethod:(SEL)selector;

使用这种方法的前提,相关方法的实现代码已经写好,只等着运行的时候动态插在类里面即可。
此方案常用来实现 @dynamic 属性。

备援接收者

runtime 询问是否还有别的接收者来处理这条消息,对应的处理方法:

- (id)forwardingTargetForSelector:(SEL)selector;

若当前接收者能找到备援对象,就将其返回,若找不到,就返回 nil。

通过此方案,我们可用“组合(composition)”来模拟出“多重继承”的某些特性。

完整的消息转发

创建 NSInvocation 对象,把尚未处理的那条消息相关的全部细节封于其中:选择子、目标(target)及参数。
在触发 NSInvocation 对象时,消息派发系统(message-dispatch system)把消息指派给目标对象。
调用以下方法:

- (void)forwardInvocation:(NSInvocation *)invocation;

较好的实现方式:
在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子。

实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样,继承体系中的每个类都有机会处理此调用请求,直到 NSObject。

如果最后调用了 NSObject 类的方法,那么该方法还会继而调用 doesNotRecognizeSelector: 以抛出异常,此异常表明最终未能处理。

13 用“方法调配技术”调试黑盒方法

核心方法如下,用来交换两个方法的实现。

void method_exchangeImplementations(Method m1, Method m2)

在运行期,可向类中新增或替换选择子所对应的方法实现。

一般只在调试时使用,不宜滥用这种方法。

14 理解“类对象”含义

从开源的 objc4-723 中可以找到下列声明

 // objc.h
 /// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

// runtime.h
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

如上所述,每个实例都有一个指向 Class 对象的指针,用以表明类型,而这些 Class 对象则构成类的继承体系。

类型信息查询方法:isKindOfClass, isMemberOfClass
尽量使用类型信息查询方法确定对象类型,不要直接比较类对象,因为有些类对象可能实现了消息转发功能。

15 用前缀避免命名空间冲突

使用 Objective-C 的常识,不再赘述。

16 提供“全能初始化方法”

即“指定初始化方法”。

若此方法与超类的不同,则需覆写超类中的对应方法。
若超类的初始化方法不适用于子类,则应覆写这个超类方法,并在其中抛出异常。

17 实现 description 方法

自定义某个对象的打印信息。

若想在(使用 lldb)调试时打印出更详细信息,则应实现 debugDescription 方法。

18 尽量使用不可变对象

尽量创建不可变对象。

若某属性仅可用于对象内部修改,则在“class-continuation 分类”中将其由 readonly 属性扩展为 readwrite 属性。

不要把可变的 collection 作为属性公开,而应提供相关方法来修改。

19 使用清晰而协调的命名方式

基本常识,不再赘述。

20 为私有方法加前缀

不要单用一个下划线做私有方法的前缀,因为这种方法是 Apple 官方使用的。

可考虑使用 p_ 作为前缀。

21 理解 Objective-C 的错误模型

ARC 在默认情况下不是“异常安全的”,即若抛出异常,那么本应在作用域末尾释放的对象,现在却不会自动释放了。

若想生成“异常安全”的代码,可通过打开编译器的标志:-fobjc-arc-exceptions 实现,并且引入一些额外代码。

异常只用于极严重的错误,其他错误返回 nil/0 或使用 NSError。

使用 NSError,一般有2种方式:

  1. 指派“委托方法”。
  2. 把错误信息放在 NSError 中,经由“输出参数”返回给调用者。

22 理解 NSCopying 协议

若想类支持拷贝操作,就要实现 NSCopying 协议:

- (id)copyWithZone:(NSZone *)zone

出现 NSZone 的原因是:以前开发程序时,会据此把内存分成不同的区,而对象会创建在某个区里。现在只有一个默认区,所以不必担心其中的 zone 参数。

若是对象还有可变版本,则需要同时实现 NSCopying 与 NSMutableCopying 协议。

-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray

在可变对象上调用 copy 方法会返回另外一个不可变类的实例。

Foundation 中的所有 collection 类在默认情况下都执行浅拷贝,因为容器内的对象未必都能拷贝,而且调用者也未必想一并拷贝容器内对象。

复制对象时需要决定采用浅拷贝还是深拷贝,一般情况下尽量执行浅拷贝。
如果所写对象需深拷贝,那么可考虑新增一个专门执行深拷贝的方法。

23 通过 delegate 和 datasource 协议进行对象间通信

为了避免“循环引用”,delegate 属性要定义为 weak。

datasource 同样是协议,主要用来从另外一个对象获取数据。

使用 delegate,需要每次都使用 respondToSelector: 来检查对象是否可响应选择子。
如果需要频繁检测,倒不如把是否能响应某个选择子的结果缓存起来,将结果缓存起来的最佳途径:使用 bifield 数据类型:

// 数字代表位数,比如 fieldA 可以代表0-255之间的值
struct data {
    unsigned int fieldA : 8;
    unsigned int fieldB : 4;
}

如果只是缓存能否响应,那么只需要1位就可以存储结果。

24 将类的实现代码,分配到数个分类中,以便于管理

除了分成多个易于管理的小块外,也可以隐藏实现细节:将应该视为“私有”的方法归入名为 Private 的分类中。

25 总是为第三方类的分类名称加前缀

除了名称外,还有方法名,目标都是为了不与其他库发生冲突。

26 勿在分类中声明属性

虽然在技术上可以使用关联对象实现,但不建议这样做,原因:

  1. 会有很多重复代码。
  2. 在内存管理问题上容易出错,因为在为属性实现存取方法时,经常忘记遵从其内存管理语义。

27 使用“class-continuation 分类”隐藏实现细节

即常说的扩展。用法:

  1. 可用它向类中新增实例变量。
  2. 如果某个属性在主接口中声明为 readonly,那么可在类的内存声明扩展,然后再将属性声明为 readwrite
  3. 用来声明私有方法的原型,虽然新版编译器不强制使用方法前必须先声明。
  4. 如果希望所遵循的协议不为人所知,也可在其中声明。

28 通过协议提供匿名对象

可在某种程度上提供匿名对象,具体的对象类型,可以淡化成只要遵从某协议的 id 类型,协议里规定了对象需要实现的方法。

如 NSMutableDictionary

- (void)setObject:(id)object forKey:(id<NSCopying>)key

29 理解引用计数

30 以 ARC 简化引用计数

ARC 回收 Objective-C++ 对象时,待回收对象会调用所有 C++ 对象的析构函数。

31 在 dealloc 方法中只释放引用并解除监听

编译器如果发现对象里有 C++ 对象,就会生成名为:.cxx_destruct 的方法。

32 编写“异常安全代码”时留意内存管理问题

33 以弱引用避免保留环

34 以“自动释放池块”降低内存峰值

35 用“僵尸对象”调试内存管理问题

通过环境变量 NSZombieEnabled 可开启功能。

_NSZombie_ 未实现任何方法,而且是个根类,只有一个实例变量 isa,根据消息转发的规则,发给它的全部消息都要经过“完整的消息转发机制”。

系统会给每个变为僵尸的类创建一个对应的新类,它会把整个 _NSZombie_ 类结构拷贝一份,并赋予新的名字。因为如果把所有僵尸对象都归到 _NSZombie_ 类里,那么对象原来的类信息就会丢失。

系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。

僵尸对象能响应所有的选择子:打印一条消息内容及其接收者的信息,然后终止应用程序。

36 不要使用 retain count

从 29 到 36 多为内存管理相关,可看

《Objective-C 内存管理》
《Objective-C自动引用计数ARC》

37 理解 block

在声明它的范围内,所有变量都可以为其捕获,但默认情况下,被它捕获的变量,是不可以在 block 里修改的。

若是要修改,得添加 __block 修饰符。

block 会把捕获的所有(指针)变量都拷贝一份,放在其结构中。

类型

  • NSStackBlock 栈
    定义 block 时,其所占内存是分配在栈中。
  • NSMallocBlock 堆
    如果给某个 block 发送 copy 消息,就可以将其拷贝到堆上,这样它就成为一个有引用计数的对象。
  • NSGlobalBlock 全局
    像这样的 block,在编译时期就确定了所需的全部信息,那么它就作为一个全局 block。
    void (^myBlock)() = ^{
        NSLog("It is a block");
    }
    

38 为常用的 block 类型创建 typedef

39 用 handler block 降低代码分散程度

40 block 引用其所属对象时,要避免出现保留环

38 - 40 较常见,不再赘述。

41 多用派发队列,少用同步锁

  1. 同步锁,在极端情况下会导致死锁
@synchronized(id) {
     // TODO
}

频繁使用同步锁,会降低代码效率,因为共用同一个锁的那些同步块,都必须按照顺序执行。

  1. 使用 NSLock 对象
_lock = [[NSLock alloc] init];
[_lock lock];
...
[_lock unlock];

使用 NSRecursiveLock(递归锁),线程能多次持有该锁,而不会出现死锁现象。

替代方案:GCD

// 异步队列
dispatch_async(_syncQueue, ^{
    // TODO:
});

如果希望队列单独执行,可用:

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

42 多用 GCD,少用 performSelector 系列方法

performSelector 系列方法在内存管理方面容易有疏忽,它无法确定将要执行的选择子具体是什么,所以 ARC 无法添加合适的内存管理方法。

performSelector 系列方法所能处理的选择子过于局限:返回值类型及发送参数个数都有限制。

如果想把任务放在另外一个线程上执行,更应该使用 GCD 的相关方法来实现。

43 掌握 GCD 及操作队列的使用时机

NSOperation 提供一套高层的 Objective-C API,以实现纯 GCD 所具备的大部分功能,且能完成一些更为复杂的操作。

44 通过 Dispatch Group 机制,根据系统资源状况来执行任务

一系列任务可归入一个 dispatch group 中,开发者可在这组任务执行完毕时获得通知。

通过 dispatch group,可以在并发式派发队列里同时执行多项任务,此时 GCD 会根据系统资源状况来调度这些并发执行的任务。

45 使用 dispatch_once 来执行只需执行一次的线程安全代码

常见的是单例实现

+ (id)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

标记应该声明在 static 或 global 作用域内,这样,在把只需执行一次的 block 交付给 dispatch_once 函数时,传进去的标记也是相同的。

46 不要使用 dispatch_get_current_queue

此函数的行为常常与开发者所预期的不同,目前已废弃,只做调试使用。

由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”。

此函数用于解决由“不可重入”的代码所引发的死锁,然而通用此函数解决的问题,通常也能改用“队列特定数据”解决。

47 熟悉系统框架

基础知识,不再赘述。

48 多用块枚举,少用 :qfor 循环

遍历 collection 有4种方式:

  1. for 循环
  2. NSEnumerator 遍历
  3. 快速遍历:for in
  4. "块枚举法"

块枚举法本身能通过 GCD 来并发执行遍历操作

若提前知道待遍历的 collection 有何种对象,应修改块签名,指出对象具体类型。

49 对自定义其内存管理语义的 Collection 使用无缝桥接

使用无缝桥接技术,可以在 Foudation 框架中的 Objective-C 对象与 CoreFoundation 框架中的 C 语言数据结构之间来回转换。

在 CoreFoundation 层面创建 collection 时,可以指定许多回调函数,这些函数表示此 collollection 应如何处理其元素,然后,借助无缝桥接技术,将其转换成具备特殊内存管理语义的 Objective-C collection。

50 构建缓存时使用 NSCache 而不是 NSDictionary

原因:

  1. 当系统资源将要耗尽时,NSCache 可以自动删减内存,而 NSDictionary 需要自己编写处理:在系统发出 Low Memory 通知时手工删减内存。
    且 NSCache 作为 Foundation 的一部分,它能在更深层面进行处理。
    NSCache 会先行删减“最久未使用的”对象。

  2. NSCache 不会『拷贝』键,而是保留它。
    原因:很多时候,键都是由不支持拷贝操作的对象来充当。

  3. NSCache 是线程安全的,而 NSDictionary 则绝对不具备这优势。

可以给 NSCache 对象设置上限,用以控制缓存

有2个与系统资源相关的尺度可供调整:

  1. 是缓存中的对象总数。
  2. 所有对象的“总开销”。
    对象在加入缓存时,可为其指定“开销值”。

在可用资源紧张的时候,可能会删减某个对象,所以通过调整“开销值”来迫使缓存优先删除某对象,不是个好主意。

只有在能很快计算出“开销值”的情况下,才应该考虑这个尺度,比如说 NSData 对象,可把数据大小作为“开销值”来使用,因其数据大小是已知的。

NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能。

NSPurgeableData 是 NSMutableData 的子类,而且实现了 NSDiscardableContent 协议。

当 NSPurgeableData 对象所占内存被系统丢弃时,该对象自身也会从缓存中移除。

只有那些“重新计算起来很费事的”数据,才值得被放入缓存

如从网络获取或磁盘读取的数据。

51 精简 initialize 与 load 的实现代码

执行 load 时,运行期系统处于“脆弱状态”(fragile state)

在执行子类的 load 方法之前,必定会先执行所有超类的 load 方法,如果代码依赖了其他程序库,那么程序库里相关类的 load 方法,也会被执行。
然而,根据某个给定的程序库,却无法判断其中各个类的加载顺序。
所以,在 load 方法中,使用其他类是不安全的。

关于 load 的继承规则:

  1. 如果某个类不实现 load 方法,无论超类是否有实现 load 方法,都不会调用。
  2. 分类和类都有可能出现 load 方法,若是有这种情况,系统先调用类中的,再调用分类的。

但现在已经很少使用 load 了,若是有使用,load 中的代码需要尽量精简。

initialize VS load

  1. initialize 是惰性调用的,只有程序用到相关类时,才会调用,但对 load 来说,应用程序必须阻塞并等着所有类的 load 都执行完,才能继续。
  2. 运行期系统在执行 initialize 时,是处于正常状态的,所以可以安全使用并调用任意类中的任意方法,而且运行期系统也会确保 initialize 是线程安全的,即只有执行 initialize 的线程可以操作类或其实例,其他线程会先阻塞,等待 initialize 执行完。
  3. 关于继承规则,initialize 跟其他消息一样,即使没有实现它,而其超类实现了,就会调用超类的实现代码。

精简 initialize 的原因:

  1. 大家不希望程序挂起。
    对于某个类来说,任何线程都可能成为初次用到它的那个线程,并导致其阻塞,如果那个线程碰巧是 UI 线程,就会导致程序无响应。
  2. 开发者无法控制类的初始化时机。
    运行期系统更新后,也有可能会修改类的初始化方式。
  3. 如果代码很复杂,可能会用到其他类,系统会迫使其他类初始化。
    然而,本类的初始化方法此时尚未运行完毕,其他类在执行 initialize 时,也有可能会用到本类的某些数据,而这些数据可能还未初始化好。

若某个全局状态变量无法在编译时期初始化,那么可以将它放到 initialize 来做,比如说全局 NSArray 对象。

52 别忘了 NSTimer 会保留其目标对象

反复执行任务的计时器,很容易造成循环引用。

可以给 NSTimer 添加 Block 来打破循环引用。

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

推荐阅读更多精彩内容