格物致知类与对象

转载自https://mp.weixin.qq.com/s/B4Z-7YQ8CMhZ0kCrxe3XAg



欲诚其意者,先致其知;致知在格物。物格而后知至,知至而后意诚。现代汉语词典中将格物致知解释为: "推究事物的原理,从而获得知识"。

在编程中我们接触最多的也是最基本的就是类和对象,当我们在创建类或者实例化对象时,是否考虑过类和对象到底是什么?理解其本质才能真正掌握一门语言。本文将从结构类型角度并结合实际应用探讨下Objective-C的类和对象。

在Objective-C中,对象是广义的概念,类也是对象,所以严谨的说法应该是类对象和实例对象。既然实例对象所属的类称为类对象,那类对象有所属的类吗?有,称之为元类(Metaclass)。

类对象(Class)

类对象是由程序员定义并在运行时由编译器创建的,它没有自己的实例变量,这里需要注意的是类的成员变量和实例方法列表是属于实例对象的,但其存储于类对象当中的。我们在/usr/include/objc/objc.h下看看Class的定义:

/// An opaque type that represents an Objective-C class.

typedef struct objc_class *Class;

可以看到类是由Class类型来表示的,它是一个objc_class结构类型的指针。我们接着来看objc_class结构体的定义:

struct objc_class {

    Class                      isa;           // 指向所属类的指针(_Nonnull)

    Class                      super_class;   // 父类                  

    const char                *name;          // 类名(_Nonnull)

    long                       version;       // 类的版本信息(默认为0)

    long                       info;          // 类信息(供运行期使用的一些位标识)

    long                       instance_size; // 该类的实例变量大小

    struct objc_ivar_list     *ivars;         // 该类的成员变量链表

    struct objc_method_list * *methodLists;   // 方法定义的链表

    struct objc_cache         *cache;         // 方法缓存

    struct objc_protocol_list *protocols;     // 协议链表

};

isa指针是和Class同类型的objc_class结构指针,类对象的指针指向其所属的类,即元类。元类中存储着类对象的类方法,当访问某个类的类方法时会通过该isa指针从元类中寻找方法对应的函数指针

super_class为该类所继承的父类对象,如果该类已经是最顶层的根类(如NSObject或NSProxy), 则 super_class为NULL

ivars是一个指向objc_ivar_list类型的指针,用来存储每一个实例变量的地址

info为运行期使用的一些位标识,比如:

CLS_CLASS (0x1L)表示该类为普通类, CLS_META (0x2L)则表示该类为元类

methodLists用来存放方法列表,根据info中的标识信息,当该类为普通类时,存储的方法为实例方法;如果是元类则存储的类方法

cache用于缓存最近使用的方法。系统在调用方法时会先去cache中查找,在没有查找到时才会去methodLists中遍历获取需要的方法

实例对象

实例对象是我们对类对象alloc或者new操作时所创建的,在这个过程中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法。同样的,我们也来看下其定义:

/// Represents an instance of a class.

struct objc_object {

    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

};

可以看到,这个结构体只有一个isa变量,指向实例对象所属的类。任何带有以指针开始并指向类结构的结构都可以被视作objc_object, 对象最重要的特点是可以给其发送消息. NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。

另外我们常见的id类型,它是一个objc_object结构类型的指针。该类型的对象可以转换为任何一种对象,类似于C语言中void *指针类型的作用。其定义如下所示:

/// A pointer to an instance of a class.

typedef struct objc_object *id;

元类对象(Metaclass)

元类就是类对象的类,每个类都有自己的元类,也就是objc_class结构体里面isa指针所指向的类. Objective-C的类方法是使用元类的根本原因,因为其中存储着对应的类对象调用的方法即类方法。

类存储示意图.png

所以由上图可以看到,在给实例对象或类对象发送消息时,寻找方法列表的规则为:

当发送消息给实例对象时,消息是在寻找这个对象的类的方法列表(实例方法)

当发送消息给类对象时,消息是在寻找这个类的元类的方法列表(类方法)

元类,就像之前的类一样,它也是一个对象,也可以调用它的方法。所以这就意味着它必须也有一个类。所有的元类都使用根元类作为他们的类。比如所有NSObject的子类的元类都会以NSObject的元类作为他们的类。

根据这个规则,所有的元类使用根元类作为他们的类,根元类的元类则就是它自己。也就是说基类的元类的isa指针指向他自己。

我们可以通过代码来实际验证下, Runtime提供了object_getClass函数:

Class _Nullable object_getClass(id _Nullable obj)

来获取对象所属的类,看到这个函数你也许会好奇这个和我们平常接触的NSObject的[obj class]有什么区别?

// NSObject.h

- (Class)class;

+ (Class)class;

我们继续从runtime的源码里面寻找答案:

Class object_getClass(id obj) {

    return _object_getClass(obj);

}

object_getClass实际调用的是_object_getClass函数,我们接着看其实现:

static inline Class _object_getClass(id obj) {

    #if SUPPORT_TAGGED_POINTERS

    if (OBJ_IS_TAGGED_PTR(obj)){

        uint8_t slotNumber = ((uint8_t)(uint64_t) obj) & 0x0F;

        Class isa = _objc_tagged_isa_table[slotNumber];

        return isa;

    }

    #endif

        if (obj) return obj->isa;

        else return Nil;

}

显然_object_getClass函数就是返回对象的isa指针,也就是返回该对象所指向的所属类。我们接着看[obj class]的具体实现(包括类方法和实例方法两种):

+ (Class)class {

    return self; // 返回自身指针

}

- (Class)class {

    return object_getClass(self); // 调用'object_getClass'返回isa指针

}

从代码中可以看出+ (Class)class返回的是其本身,而- (Class)class则等价于object_getClass函数。

我们来写个测试代码,看看这些函数的实际返回值是否和上面的所述保持一致,比如我们有个RJObject继承与NSObject:

RJObject *obj = [RJObject new];

Class clsClass0 = [RJObject class];     // 返回RJObject类对象的本身的地址

Class objClass0 = [obj class];          // isa指向的RJObject类对象的地址

Class ogcClass0 = object_getClass(obj); // isa指向的RJObject类对象的地址

NSLog(@"clsClass0 -> %p", clsClass0); // -> 0x10fb22068

NSLog(@"objClass0 -> %p", objClass0); // -> 0x10fb22068

NSLog(@"ogcClass0 -> %p", ogcClass0); // -> 0x10fb22068

打印结果可以看出,当obj为实例变量时, object_getClass(obj)与[obj class]输出结果一致,均返回该对象的isa指针,即指向RJObject类对象的指针。而[RJObject class]则直接返回RJObject类对象本身的地址,所以与前面两者返回的地址相同。

// 'objClass0'为RJObject类对象(RJObject Class)

Class objClass1 = [objClass0 class];          // 返回RJObject类对象本身的地址

Class ogcClass1 = object_getClass(objClass0); // isa指向的RJObject元类的地址

NSLog(@"objClass1 -> %p", objClass1); // -> 0x10fb22068

NSLog(@"ogcClass1 -> %p", ogcClass1); // -> 0x10fb22040

此时objClass0为RJObject的类对象,所以类方法[objClass0 class]返回的objClass1为self, 即RJObject类对象本身的地址,故结果与上面的地址相同。而ogcClass1返回的为RJObject元类的地址。

// 'ogcClass1'为RJObject的元类(RJObject metaClass)

Class objClass2 = [ogcClass1 class];          // 返回RJObject元类对象的本身的地址

Class ogcClass2 = object_getClass(ogcClass1); // isa指向的RJObject元类的元类地址

NSLog(@"objClass2 -> %p", objClass2); // -> 0x10fb22040

NSLog(@"ogcClass2 -> %p", ogcClass2); // -> 0x110ad9e58

同理,这边ogcClass2为RJObject元类的元类的地址,那问题来了,某个类它的元类的元类的是什么类呢?这样下去岂不是元类无穷尽了?擒贼先擒王,我们先来看看根类NSObject的元类和它元类的元类分别是什么:

Class rootMetaCls0 = object_getClass([NSObject class]); // 返回NSObject元类(根元类)的地址

Class rootMetaCls1 = object_getClass(rootMetaCls0);     // 返回NSObject元类(根元类)的元类地址

NSLog(@"rootMetaCls0 -> %p", rootMetaCls0); // -> 0x110ad9e58

NSLog(@"rootMetaCls1 -> %p", rootMetaCls1); // -> 0x110ad9e58

看到结果就一目了然了,根元类的isa指针指向自己,也就是根元类的元类即其本身。另外,可以发现ogcClass2的地址和根元类isa的地址相同,说明任意元类的isa指针都指向根元类,这样就构成一个封闭的循环。

另外,我们可以通过class_isMetaClass函数来判断某个类是否是元类,比如:

NSLog(@"ogcClass0 is metaClass: %@", class_isMetaClass(objClass0) ? @"YES" : @"NO");

NSLog(@"ogcClass1 is metaClass: %@", class_isMetaClass(ogcClass1) ? @"YES" : @"NO");

输出结果为:

LearningClass[58516:3424874] ogcClass0 is metaClass: NO

LearningClass[58516:3424874] ogcClass1 is metaClass: YES

日志表明ogcClass0为类对象,而ogcClass1则为元类对象,这与我们上面的分析是一致的。

类和元类的父类指向情况也可以参照上面的步骤,通过class_getSuperclass或者[obj superClass]函数来获取分析,这边就不再赘述了。

除了isa声明了实例与所属类的关系,还有superClass表明了类和元类的继承关系,类对象和元类对象都有父类。同样,为了形成一个闭环,根类的父类为nil, 根元类的父类则指向其根类。我们可以通过一张示意图来看下三种对象之间的连接关系:

类关系示意图

总结一下实例对象,类对象以及元类对象之间的isa指向和继承关系的规则为:

规则一: 实例对象的isa指向该类,类的isa指向元类(metaClass)

规则二:类的superClass指向其父类,如果该类为根类则值为nil

规则三:元类的isa指向根元类,如果该元类是根元类则指向自身

规则四: 元类的superClass指向父元类,若根元类则指向该根类

动态创建类

Objective-C作为动态语言的优势在于它能在运行时创建类和对象,并向类中增加方法和实例变量。具体示例如下:

Class newClass = objc_allocateClassPair([NSObject class], "RJInfo", 0);

if (!class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:")) {

    NSLog(@"Add method 'report' failed!");

}

if (!class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *))) {

    NSLog(@"Add ivar '_name' failed!");

}

objc_registerClassPair(newClass);

上面代码创建了一个RJInfo的类,并分别添加了_name成员变量和report实例方法。需要注意的是,方法和变量必须在objc_allocateClassPair和objc_registerClassPair之间进行添加。所以,在运行时创建一个类只需要3个步骤:

首先是调用objc_allocateClassPair为新建的类分配内存,三个参数依次为newClass的父类,newClass的名称,第三个参数通常为0, 从这个函数名字可以看到新建的类是一个pair, 也就是成对的类,那为什么新建一个类会出现一对类呢?是的,元类!类和元类是成对出现的,每个类都有自己所属的元类,所以新建一个类需要同时创建类以及它的元类。

然后就可以向newClass中添加变量及方法了,注意若要添加类方法,需用objc_getClass(newClass)获取元类,然后向元类中添加类方法。因为示例方法是存储在类中的,而类方法则是存储在元类中。最后必须把newClass注册到运行时系统,否则系统是不能识别这个类的。

上面的代码中添加了一个成员变量_name, 我们来看下实际应用中如何获取和使用这个变量:

unsigned int varCount;

Ivar *varList = class_copyIvarList(newClass, &varCount);

for (int i = 0; i < varCount; i++) {

    NSLog(@"var name: %s", ivar_getName(varList[i]));

}

free(varList);

id infoInstance = [[newClass alloc] init];

Ivar nameIvar   = class_getInstanceVariable(newClass, "_name");

object_setIvar(infoInstance, nameIvar, @"Ryan Jin");

NSLog(@"var value: %@",object_getIvar(infoInstance, nameIvar));

我们可以通过class_copyIvarList来查看实例变量列表,注意获取的varList列表需要调用free()函数释放。当前只添加了一个变量,所以varCount为1, 在调用ivar_getName打印出变量的名字。如若对_name赋值,则需要先实例化newClass对象,并取出对象的该变量后调用object_setIvar进行赋值操作。示例代码的输出结果为:

LearningClass[58516:3424874] var name: _name

LearningClass[58516:3424874] var value: Ryan Jin

好了,验证完变量的添加,继续看方法的添加和使用。上文的示例中添加了report方法,但仅仅是做了SEL方法名的声明,我们来接着完成其IMP所指向函数ReportFunction的具体实现:

void ReportFunction(id self, SEL _cmd) {

    Class currentClass = [self class];

    Class metaClass    = objc_getMetaClass(class_getName(currentClass));


    NSLog(@"Class is %@, and super - %@.", currentClass, [self superclass]);

    NSLog(@"%@'s meta class is %p.", NSStringFromClass(currentClass), metaClass);

}

在函数实现中我们打印了类,父类以及元类的相关信息,为了运行ReportFunction, 我们需要创建一个动态实例来创建类的实例对象并调用report方法:

id instanceOfNewClass = [[newClass alloc] init];


[instanceOfNewClass performSelector:@selector(report)];

输出结果:

LearningClass[58516:3424874] Class is RJInfo, and super - NSObject.

LearningClass[58516:3424874] RJInfo's meta class is 0x600000253920.

除了给类添加方法,我们同样也可以动态修改已存在方法的实现,比如:

class_replaceMethod(newClass, @selector(report), (IMP)ReportReplacedFunction, "v@:");

这样就将report这个SEL所指向的IMP实现换成了ReportReplacedFunction. 如果类中不存在name指定的方法, class_replaceMethod则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现。

看到class_replaceMethod的解释,相信你已经发现了,这不就是Method Swizzling吗?没错,所谓的黑魔法,其实就是底层原理的应用而已!

本质探究

知其然亦知其所以然才是获取知识的正确方式,理解了类和对象的本质后,我们来看看格物致知后的理论可以引导出哪些应用和认识:

属性(Property)

在Objective-C中,属性和成员变量是不同的。那么,属性的本质是什么?它和成员变量之间有什么区别?简单来说属性是添加了存取方法的成员变量,也就是:

@property = ivar + getter + setter;

因此,我们每定义一个@property都会添加对应的ivar, getter和setter到类结构体objc_class中。具体来说,系统会在objc_ivar_list中添加一个成员变量的描述,然后在methodLists中分别添加setter和getter方法的描述。

父类对象

我们直接来看一个面试题, Father继承与NSObject, Son则继承于Father类,分别调用[self class]和[super class], 输出结果是?

@implementation Son : Father

- (instancetype)init

{

    self = [super init];

    if (self) {

        NSLog(@"%@", NSStringFromClass([self class]));

        NSLog(@"%@", NSStringFromClass([super class]));

    }

    return self;

}

@end

输出结果都为Son, 为什么[super class]的结果不是Father? 我们简单分析下就明白了。实例对象的方法列表是存放在isa所指向的类对象中的,所以调用[self class]的时候会去self的isa所指向的Son类对象中寻找该方法,在没有重载[obj class]的情况下, Son类对象是没有这个方法的,此时会接着在父类对象的方法列表中查找,最终会发现NSObject存储了该方法,所以[self class]会返回实例对象(self)所属的Son这个类对象

而[super class]则指定从父类Father的方法列表开始去查找- (Class)class这个方法,显然Father没有这个方法,最终还是要查找到NSObject类对象的方法列表中,需要注意的是不管是[self class]还是[super class], 它们都是调用的实例对象的- (Class)class方法,虽然其指向的类对象不同,但实例对象都是self本身,再强调下区分开实例对象和类对象!因而返回的也是当前self的isa所指向的Son类。

其实super是objc_super类型的结构体,它包含了当前的实例对象self以及父类的类对象。更详细的解答可以参考@iOS程序犭袁的博文。

除了用super来指向父类外,我们还可以用isKindOfClass和isMemberOfClass来判断对象的继承关系。这两个函数有什么区别呢?同样,先来看一个测试题:

BOOL r1 = [[NSObject class] isKindOfClass:[NSObject class]]; // -> YES

BOOL r2 = [[RJObject class] isKindOfClass:[RJObject class]]; // -> NO

BOOL r3 = [[NSObject class] isMemberOfClass:[NSObject class]]; // -> NO

BOOL r4 = [[RJObject class] isMemberOfClass:[RJObject class]]; // -> NO

为什么只有r1是YES? 实际上isKindOfClass是判断对象是否为Class的实例或子类,而isMemberOfClass则是判断对象是否为Class的实例。还是不明白?没关系,我们直接来看看这两个函数的源码实现,看看它们本质上是以什么作为判断标准的:

+ (BOOL)isKindOfClass:(Class)cls

{

    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {

        if (tcls == cls) return YES;

    }

    return NO;

}

- (BOOL)isKindOfClass:(Class)cls 

{

    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {

        if (tcls == cls) return YES;

    }

    return NO;

}

+ (BOOL)isMemberOfClass:(Class)cls {

    return object_getClass((id)self) == cls;  

}

- (BOOL)isMemberOfClass:(Class)cls {

    return [self class] == cls; 

}

注意上面的题目是调用的类方法,所以我们分析下类方法的实现,至于实例方法也是类似的。可以看到isMemberOfClass的判断是先调用object_getClass获取isa所指向的归属类,也就是元类,然后直接判断cls是否就是被比较的对象的元类。而[NSObject class]的元类是根元类,显然不等于[NSObject class]本身,所以r3返回NO, r4也是同理。

而isKindOfClass也是先获取当前对象的元类,但是会循环获取其isa所指向类的父类进行比较,只要该元类或者元类的父类与cls相对则返回YES. RJObject的元类,以及父元类(最终指向根元类)都不等于RJObject对象,所以r2返回NO. 那为什么r1返回YES呢?还记得上文所说的闭环吗?根元类的父类指向根类本身!显然, r1符合了isKindOfClass的判断标准。

学以致用

到这里理论部分就结束了。那么,问题来了,理解了类和对象的本质原理有什么实际应用价值吗?可以让我们更优雅的解决项目中遇到的问题和需求吗?Talk is cheap, show me the code:

比如App常见的记录用户行为的数据统计需求,俗称埋点。具体来说假设我们需要记录用户对按钮的点击。通常情况下,我们会在按钮的点击事件里面直接加上数据统计的代码,但这样做的问题在于会对业务代码进行侵入,且统计的代码散落各处,难以维护。

当然,我们还可以创建一个UIButton的子类,在子类中重载点击事件的响应函数,并在其中加上统计数据部分的代码:

-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event

这样做是可以的,但是现有工程中所有需要支持数据统计的按钮都必须替换成该子类,而且如果哪天不需要支持埋点功能了并需要迁移复用业务代码,那还得一个个再改回去。所以,我们需要一个更优雅的实现。

我们可以利用动态创建类并添加方法的思路来实现这个需求,这边只是以埋点作为示例,你也可以利用该思路扩展任意需要处理的需求和功能。简单来说就是我们创建一个UIButton的Category, 然后在需要埋点的情况下动态生成一个新的UIButton子类,并给其添加一个可以记录数据的事件响应方法来替代默认的方法,如下所示:

//

//  UIButton+Tracking.m

//  LearningClass

//

//  Created by Ryan Jin on 07/03/2018.

//  Copyright ? 2018 ArcSoft. All rights reserved.

//

#import "UIButton+Tracking.h"

#import 

#import 

@implementation UIButton (Tracking)

- (void)enableEventTracking

{

    NSString *className = [NSString stringWithFormat:@"EventTracking_%@",self.class];

    Class kClass        = objc_getClass([className UTF8String]);


    if (!kClass) {

        kClass = objc_allocateClassPair([self class], [className UTF8String], 0);

    }

    SEL setterSelector  = NSSelectorFromString(@"sendAction:to:forEvent:");

    Method setterMethod = class_getInstanceMethod([self class], setterSelector);


    object_setClass(self, kClass); // 转换当前类从UIButton到新建的EventTracking_UIButton类


    const char *types   = method_getTypeEncoding(setterMethod);


    class_addMethod(kClass, setterSelector, (IMP)eventTracking_SendAction, types);


    objc_registerClassPair(kClass);

}

static void eventTracking_SendAction(id self, SEL _cmd, SEL action ,id target , UIEvent *event) {

    struct objc_super superclass = {

        .receiver    = self,

        .super_class = class_getSuperclass(object_getClass(self))

    };

    void (*objc_msgSendSuperCasted)(const void *, SEL, SEL, id, UIEvent *) = (void *)objc_msgSendSuper;


    // to do event tracking...

    NSLog(@"Click event record: target = %@, action = %@, event = %ld", target, NSStringFromSelector(action), (long)event.type);


    objc_msgSendSuperCasted(&superclass, _cmd, action, target, event);

}

@end

然后在添加按钮的地方,如果需要数据统计功能,则调用enableEventTracking函数来内嵌打点功能。使用示例如下:

- (void)viewDidLoad

{

    [super viewDidLoad];


    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 50, 30)];


    button.layer.borderColor   = [[UIColor redColor] CGColor];

    button.layer.borderWidth   = 1.0f;

    button.layer.cornerRadius  = 4.0f;

    button.layer.masksToBounds = YES;


    [button addTarget:self action:@selector(trackingButtonAction:)

                 forControlEvents:UIControlEventTouchUpInside];


    [self.view addSubview:button];

    [button enableEventTracking];

}

- (void)trackingButtonAction:(UIButton *)sender

{

    // to do whatever you want...

    NSLog(@"%s", __func__);

}

打印输出信息为:

LearningClass[58516:3424874] Click event record: target = , action = trackingButtonAction:, event = 0

LearningClass[58516:3424874] -[ViewController trackingButtonAction:]

浮于表面探究问题不失为一种方法,但是弄清楚本质才是真正意义上的解决疑惑。


自己心得:

1.很重要一点,所有类的根元类是NSObject的元类,NSObject的根元类是元类本身,但NSObject自身并不是NSObject的元类,NSObject是所有类的根类。但神奇的是NSObject的元类的superClass父类又指回了NSObject。这很神奇。

也就是说object_getClass([NSObject class])和object_getClass(object_getClass([NSObject class]))是一个地址,而[object_getClass([NSObject class]) superclass]和[NSObject class]又是一个地址。

参考地址:https://blog.csdn.net/ChSaDiN/article/details/51672087。

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

推荐阅读更多精彩内容