iOS中的Runtime详解1(附面试题) - 底层原理总结

在学习Runtime之前首先要对isa和superclass有一定了解,关于isa和superclass,可以看一下我的另一篇文章iOS中OC对象的本质详解(附面试题) - 底层原理总结

每个OC对象都有一个isa指针,在arm64架构之前,isa仅仅是一个指针,保存着对象在内存中的地址,实例对象通过isa可以直接拿到类对象,类对象通过isa可以直接拿到元类对象。而在arm64架构之后,苹果对isa进行了优化,变成了一个共用体结构(union),同时使用位域来存储更多的信息。这时候要想通过实例对象要想通过isa拿类对象,就要通过位运算从isa中获取类对象在内存中的地址,类对象中的isa也是如此。下面具体来看一下。

一、共用体 - 苹果用它在二进制层面优化isa

源码中的isa

struct objc_object {
private:
    isa_t isa;
}

isa内部

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA
# if __arm64__      
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__     
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };

# else
#   error unknown architecture for packed isa
# endif
#endif

源码中的isa_t是union类型,union表示共用体。什么是共用体呢?先来看看比较拗口的官方定义,看完可能有点懵,不过没关系,接着往下看就会搞明白的。

共用体:在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。

二、模仿底层使用共用体

2.1 很多时候,我们在浪费存储空间

在arm64之前,OC对象中的isa仅仅是一个普通的指针,存着对象的地址。在arm64之后,苹果对isa进行了优化,变成了一个共用体结构,同时使用位域来存储更多的信息。

同样的内存空间,共用体如何做到存储更多的信息?我们来看看下面的代码。

@interface Person : NSObject
@property (nonatomic, assign, getter = isTall) BOOL tall;
@property (nonatomic, assign, getter = isRich) BOOL rich;
@property (nonatomic, assign, getter = isHansome) BOOL handsome;
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *ps = [[Person alloc] init];
        ps.tall = YES;
        ps.rich = YES;
        ps.handsome = NO;
        NSLog(@"%d %d %d",ps.isTall,ps.isRich,ps.isHansome);
    }
    return 0;
}

上面的代码中Person有3个BOOL类型的属性,每个BOOL类型的属性,占用1个字节内存空间,3个BOOL类型也就是3个字节内存空间。

1个字节内存空间有8个二进制位,这3个BOOL类型开辟了24个二进制位!然而实际上,BOOL类型只需要1个二进制位0或1存储就足够了!也就是说这3个BOOL类型的属性浪费了21个二进制位的存储空间,这简直是一种极大的浪费!就我们平时编写代码来说,可能没什么,但是从操作系统层面来看,这可能积累起很大的浪费。

2.2 使用一个char类型来存储以上3个BOOL类型的值

char类型占据一个字节内存空间,即8个二进制位,我们可以添加一个char类型的成员变量,使用它的其中3个二进制位来存储3个BOOL类型的值。

@interface Person()
{
    char _tallRichHandsome;
}

例如_tallRichHandsome的值为 0b 0000 0000,那么可以只使用8个二进制位中的最后3个,分别为其赋值0或1来代表tall、rich、handsome的值。如下图

二进制位的存储

问题是我们该如何对其进行取值赋值操作呢?

取值

假如char类型的成员变量中存储的二进制位 0b 0000 0010,如果想将倒数第二位也就是rich的值取出来,可以使用&(按位与)运算,将它取出来。

&:按位与,同真为真,其他都为假。

// 示例
// 取出倒数第三位 tall
  0000 0010
& 0000 0100
------------
  0000 0000  // 取出倒数第三位的值为0,其他位都置为0

// 取出倒数第二位 rich
  0000 0010
& 0000 0010
------------
  0000 0010 // 取出倒数第二位的值为1,其他位都置为0

// 取出倒数第一位 handsome
  0000 0010
& 0000 0001
------------
  0000 0000 // 取出倒数第一位的值为0,其他位都置为0

按位与可以用来取出特定的位,想取出哪一位就将那一位置为1,其他位都置为0,然后同原数据进行按位与,即可取出特定的位。

那么我们就可以通过以下这种方式重写3个BOOL类型的getter方法来取值

#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1

- (BOOL)tall
{
    return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
    return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
    return !!(_tallRichHandsome & HandsomeMask);
}

上面的代码使用2个!!(非),是将按位与取出的值改为BOOL类型。一个!可以将按位与的值改为BOOL类型,但是取反了,所以再使用一个!取反,得到我们真正想要的结果。比如

// 取出倒数第二位 rich
  0000 0010  // _tallRichHandsome
& 0000 0010 // RichMask
------------
  0000 0010 // 取出rich的值为1,其他位都置为0

这里我们取出的值为 0000 0010,也就是十进制的2。但是我们需要返回的是一个BOOL类型的值0或1。这里_tallRichHandsome的倒数第2位为1,所以取出rich的值应为1,我们通过1个!将2转为0,再通过!将0转为1,即!!(_tallRichHandsome & TichMask)的结果为1,我们做到了获取正确的值。

掩码:上述代码中定义了3个宏,用来分别进行按位与运算而取出相应的值。一般的,用来进行按位与(&)运算的值我们称之为掩码。

为了可读性,上述3个宏的定义可以使用 <<(左移) 来优化。如 0000 0001 就是让1左移0位,用 1<<0 表示,0000 0010 就是让1左移1位,用 1<<1表示,0000 0100 就是让1左移2位,用 1<<2 表示。那么上述宏定义可以这样优化

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1

设值

设值就是将char类型8位中的某一位设值为0或1,可以使用|(按位或) 运算设值。

&:按位或,只要有一个为1即为1,否则为0。

如果要将某一位置为1的话,那么将原本的值与掩码进行按位或即可,比如我们将tall置为1

// 将倒数第三位 tall置为1
  0000 0010  // _tallRichHandsome
| 0000 0100  // TallMask
------------
  0000 0110 // 将tall置为1,其他位值都不变

如果要将某一位置为0的话,需要将掩码~(按位取反),然后再与原来的值进行按位与。

// 将倒数第二位 rich置为0
  0000 0010  // _tallRichHandsome
& 1111 1101  // RichMask按位取反
------------
  0000 0000 // 将rich置为0,其他位值都不变

所以对于之前的3个BOOL类型,setter方法可以如下实现

- (void)setTall:(BOOL)tall
{
    if (tall) { // 如果需要将值置为1  // 按位或掩码
        _tallRichHandsome |= TallMask;
    }else{ // 如果需要将值置为0 // 按位与(按位取反的掩码)
        _tallRichHandsome &= ~TallMask; 
    }
}
- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome |= RichMask;
    }else{
        _tallRichHandsome &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome |= HandsomeMask;
    }else{
        _tallRichHandsome &= ~HandsomeMask;
    }
}

取值和赋值都完成了,我们来验证一下是否真的做到了我们想要实现的结果。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *ps = [[Person alloc] init];
        ps.tall = YES;
        ps.rich = YES;
        ps.handsome = NO;
        NSLog(@"%d %d %d",ps.isTall,ps.isRich,ps.isHansome);
    }
    return 0;
}
2019-12-18 11:31:48.375172+0800 Runtime[53571:649155] 1 1 0
Program ended with exit code: 0

可以看到,经过验证,我们上述做法实现了使用char类型8个二进制位的后3位,实现3个BOOL类型的赋值取值。但是上述代码的可读性有待提高,维护效率也有待提高。接下来使用结构体的位域来优化上述代码。

位域

位域声明:位域名:位域长度

位域注意事项:

1.如果1个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以直接使某位域从下一单元开始。
2.位域的长度不能大于数据类型本身的长度,比如int类型就不能超过32位二进制位。
3.位域可以无位域名,这时它只用来做填充或调整位置。无名的位域是不能使用的。

前面的代码使用结构体位域优化之后

@interface Person()
{
    struct {
        char handsome : 1; // 位域,代表占用一位空间
        char rich : 1;  // 按照顺序只占一位空间
        char tall : 1; 
    }_tallRichHandsome;
}

setter、getter方法中可以直接通过结构体赋值和取值

- (void)setTall:(BOOL)tall
{
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
    _tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
    return _tallRichHandsome.tall;
}
- (BOOL)rich
{
    return _tallRichHandsome.rich;
}
- (BOOL)handsome
{
    return _tallRichHandsome.handsome;
}

下面我们进行验证一下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person  = [[Person alloc] init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
    }
    return 0;
}

在NSLog出打个断点,通过p/xx查看_tallRichHandsome内存储的值

_tallRichHandsome内存储的值

_tallRichHandsome占据一个内存空间,也就是8个二进制位,我们通过Mac电脑自带的计算器将05这个十六进制数转化为二进制数查看

05在二进制中

可以看到,倒数第3位也就是tall的值为1,倒数第2位也就是rich的值为0,倒数第1位也是就handsome的值为1,和前面代码中我们设置的值是一样的。说明我们成功做到了。

但是打印的结果似乎有点问题,tall和handsome居然为-1。设值的时候,我们设置的是YES,应该打印1才对,为什么变成-1了呢?

2019-12-18 11:31:48.375172+0800 Runtime[53571:649155] tall:-1,rich:0,handsome:-1
Program ended with exit code: 0

来到getter方法内部,通过打印断点查看获取到的值。

- (BOOL)handsome
{
    BOOL ret = _tallRichHandsome.handsome;
    return ret;
}

打印ret的值

打印ret的值

打印出来ret的值为255,也就是1111 1111,在一个字节时,有符号数则为-1,无符号数则为255。因此我们在打印的时候出现了-1。

我们通过结构体获取到handsome的值为一个字节8个二进制位中的一位,而BOOL类型占据一个字节8个二进制位,当仅有1位的值扩展为8位的时候,其余空位就会根据前面一位的值全部部位成1。所以当我们使用一个二进制位1的handsome给BOOL赋值的时候,导致BOOL中的8个二进制位0000 0000全被映射为了1,即1111 1111

为了解决这个问题,我们可以将tall、rich、handsome的值设置为占据2个二进制位。这样当我们使用2个二进制位01的handsome给8个二进制位的BOOL类型赋值的时候,前面的空值就会自动根据前面一位补全为0,即 0000 0001,因此这时打印出来的值为1。

同样的,上述问题也可以使用!!来解决问题,达到我们想要的效果。

使用结构体位域优化之后的代码

@interface Person()
{
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    }_tallRichHandsome;
}
@end

@implementation Person

- (void)setTall:(BOOL)tall
{
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
    _tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
    return !!_tallRichHandsome.tall;
}
- (BOOL)rich
{
    return !!_tallRichHandsome.rich;
}
- (BOOL)handsome
{
    return !!_tallRichHandsome.handsome;
}

上述代码中使用了结构体的位域,则不再需要使用掩码,使代码可读性增强了不少,但是效率相比使用位运算要低,如果想要高效率又想高可读性,那么就要使用到共用体了。

共用体

为了使代码存取高效率的同时,又有较强的可读性,可以使用共用体来增强代码的可读性,同时使用位运算来提高数据存取的效率

使用共用体优化的代码

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1

@interface Person()
{
    union {
        char bits;
       // 结构体仅仅是为了增强代码可读性,无实质用处
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}
@end

@implementation Person

- (void)setTall:(BOOL)tall
{
    if (tall) {
        _tallRichHandsome.bits |= TallMask;
    }else{
        _tallRichHandsome.bits &= ~TallMask;
    }
}
- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome.bits |= RichMask;
    }else{
        _tallRichHandsome.bits &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome.bits |= HandsomeMask;
    }else{
        _tallRichHandsome.bits &= ~HandsomeMask;
    }
}
- (BOOL)tall
{
    return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)rich
{
    return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)handsome
{
    return !!(_tallRichHandsome.bits & HandsomeMask);
}

上面的代码中使用位运算这种高效的方式存取值,使用union共用体来对数据进行存储。增加存取效率的同时提高了代码可读性。

其中_tallRichHandsome共用体只占用一个字节,因为结构体中tall、rich、handsome都只占1个二进制位空间,所以结构体只占一个字节空间,而char类型的bits也只占用一个字节空间,它们都在共用体中,它们共用同一块内存空间,即共用一个字节的内存空间即可。

并且在getter、setter方法中并没有用到共用体,结构体仅仅为了增加代码可读性,指明共用体重存储了哪些值,以及这些值占多少个二进制位内存空间。同时存取值使用位运算来提高效率,存储使用共用体,存放的位置还是通过掩码进行位运算来控制。

至此,优化工作就完成了,优化后的代码不仅高效,可读性也高。这时,我们在回到isa_t共用体的源码中看看。

三、isa_t源码

精简过的isa_t源码

// 精简过的isa_t共用体
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
#endif
};

经过上面对位运算、位域以及共用体的分析,现在再来看源码我们就可以很好的理解其中的内容。源码中通过共用体存储了64个二进制位的值,这些值在结构体中被展示出来,通过对bits进行位运算而取出相应位置的值。

这里主要关注一下shiftcls,它里面存储着类对象、元类对象的内存地址,实例对象的isa指针需要和ISA_MASK进行&(按位与)运算才能得到真正的类对象在内存中的地址。

isa指针需要经过按位与得到类对象的地址

我们再来看看ISA_MASK的值0x0000000ffffffff8ULL,使用Mac电脑上的编程计算器将其转化为二进制数

ISA_MASK在二进制中

可以看到ISA_MASK的值转化为二进制后,其中有33位都为1,此时我们已经知道,用这个33个1通过&(按位与)可以取出对应位的值。即通过ISA_MASK可以取出类对象或元类对象在内存中的地址值。

值得注意的是,ISA_MASK最后3位的值为0,所以任何数通过ISA_MASK按位与得到的数,最后3位必定都为0,转化为十六进制末位必定为8或0。

四、isa中存储的信息及作用

isa中存储的信息及作用

struct {
    // 0代表普通的指针,存储着Class,Meta-Class对象的内存地址。
    // 1代表优化后的使用位域存储更多的信息。
    uintptr_t nonpointer        : 1; 

   // 是否有设置过关联对象,如果没有,释放时会更快
    uintptr_t has_assoc         : 1;

    // 是否有C++析构函数,如果没有,释放时会更快
    uintptr_t has_cxx_dtor      : 1;

    // 存储着Class、Meta-Class对象的内存地址信息
    uintptr_t shiftcls          : 33; 

    // 用于在调试时分辨对象是否未完成初始化
    uintptr_t magic             : 6;

    // 是否有被弱引用指向过。
    uintptr_t weakly_referenced : 1;

    // 对象是否正在释放
    uintptr_t deallocating      : 1;

    // 引用计数器是否过大无法存储在isa中
    // 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
    uintptr_t has_sidetable_rc  : 1;

    // 里面存储的值是引用计数器减1
    uintptr_t extra_rc          : 19;
};

验证

我们通过下面一段代码来验证一下上述注释,需要注意的是,应当在真机上运行,因为真机上才是__arm64__架构,而模拟器上是x86架构。

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    NSLog(@"%p",[person class]);
    NSLog(@"%@",person);
}

首先打印person类对象的地址,之后通过断点打印person对象的isa指针的地址。

打印结果

将类对象的地址值转换为二进制

类对象地址

将person对象ps的isa指针地址转换为二进制

person对象的isa指针地址

shiftcls:shiftcls中存储类对象的地址,通过上面两张图对比可以发现存储类对象地址的33位二进制内容完全相同。

extra_rc:extra_rc的19位中存储着的值为引用计数-1,因为此时person的引用计数为1,因此此时extra_rc的19位二进制中存储的是0。

magic:magic的6位用于在调试时标记对象是否未完成初始化,上述代码中person已经完成初始化,那么此时这6位二进制中存储的值011010即为共用体重定义的宏# define ISA_MAGIC_VALUE 0x000001a000000001ULL的值。

nonpointer:这里肯定是使用的优化后的isa,因此nonpointer的值肯定为1。

因为此时person对象没有关联对象并且没有弱指针引用过,可以看出has_assocweakly_referenced的值都为0,接着我们person对象添加弱引用和关联对象,来观察一下has_assocweakly_referenced的变化。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    NSLog(@"%p",[Person class]);
    __weak Person *weakPerson = person;
    objc_setAssociatedObject(person, @"name", @"jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"%@",person);
}

现在再重新打印person的isa指针地址,用Mac自带的计算器,将其转化为二进制可以看到has_assocweakly_referenced的值都变成了1。

has_assoc和weakly_referenced的变化

值得注意的是:只要设置过关联对象或弱引用过对象,has_assocweakly_referenced的值就会变成1,无论之后是否将关联对象置为nil或不再弱引用对象。

如果没有设置过关联对象,对象释放时会更快,这是因为对象在销毁时会判断是否有关联对象,如果有,就要去释放关联对象。

对象销毁源码

void *objc_destructInstance(id obj) 
{
    if (obj) {
        Class isa = obj->getIsa();
        // 是否有c++析构函数
        if (isa->hasCxxDtor()) {
            object_cxxDestruct(obj);
        }
        // 是否有关联对象,如果有则移除
        if (isa->instancesHaveAssociatedObjects()) {
            _object_remove_assocations(obj);
        }
        objc_clear_deallocating(obj);
    }
    return obj;
}

希望到这里大家能对isa指针有一个新的认识,__arm64__架构之后,isa指针不再只是只存储Class(类对象)Meta-Class(元类对象)的地址。而是使用共用体的方式充分利用了8个字节,64个二进制位存储了更多信息,其中shiftcls占用33个二进制位,存储了Class(类对象)Meta-Class(元类对象)的地址,需要同ISA_MASK进行&(按位与)才可以取出其地址值。

下一篇iOS中的Runtime详解2(附面试题 - 底层原理总结)

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

推荐阅读更多精彩内容

  • 感恩青岛一直阳光明媚!居然在青岛待了10天,收获满满!体验不一样的青岛,这就是我的第二故乡! 感恩陈老师创建了超凡...
    幸福储钱罐阅读 320评论 0 1
  • 少女曹七巧被家人半骗半卖,送进了大家族姜家做了二房少奶奶,二少爷是个勉强坐起来还没有三岁孩子高的软骨残疾人。在大家...
    一颗柚心阅读 455评论 4 5
  • 老赵买房遇麻烦 老王脚踏两只船 这边收了卖房钱 那边出租二十年 老赵买房付完钱,办了手续,可老王卖房迟迟不交钥匙,...
    旖旎i阅读 342评论 1 16
  • 1 “哎哎哎,你们快来看啊,这儿有一只兔子!” 蒋琳像发现了新大陆似的突然大叫起来,她兴奋的叫声把正百无聊赖东扯葫...
    孔孟之乡阅读 162评论 0 0
  • 培养一种节奏,奔向繁华。
    愉悦的右脑阅读 153评论 0 2