iOS-block2-copy操作、对象类型的auto变量、__block

上篇文章只是简单讲了MRC环境下block的copy操作。

一. ARC环境下,block的copy操作

接下来我们讲的都是在ARC环境下。

观察如下代码:

typedef void (^MJBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        //block变量强引用着右边的block
        MJBlock block = ^{
            NSLog(@"---------%d", age);
        };

        block();
        NSLog(@"%@", [block class]);
    }
    return 0;
}

打印:

---------10
__NSMallocBlock__

上文我们说过如果block访问了atuo变量就是__NSStackBlock__,存放在栈区,栈区的内存系统自动管理,那么在{}结束后block就被销毁了,这时候再访问block就是很危险的事,上面block也没有进行copy操作,但是现在为什么可以打印呢?

这是因为我们现在在ARC环境下,并且将block赋值给强指针指着了,编译器帮我们做了copy操作,将栈上的block复制到堆上,所以上面的打印才是__NSMallocBlock__类型。

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  1. block作为函数返回值时
  2. 将block赋值给__strong指针时
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时
  4. block作为GCD API的方法参数时

前两种情况比较好理解,就不解释了,后面两种情况看如下代码:

NSArray *arr = @[];
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    //block作为Cocoa API中方法名含有usingBlock的方法参数时
}];

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    //block作为GCD API的方法参数时
});

二. 对象类型的auto变量

1. 通过例子引出结论

① MRC 不copy

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MJBlock block;

        {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;

            block = ^{
                NSLog(@"---------%d", person.age);
            };
            
            //如果是MRC person离开{}之前要进行release
            [person release];
        }

        NSLog(@"------"); //此处打断点,block还在,person被销毁了
    }
    return 0;
}

打印: MJPerson - dealloc ,person被释放。可以发现block还在,但是离开{}之后person就被释放掉了

② MRC copy

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MJBlock block;

        {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;

            block = [^{
                NSLog(@"---------%d", person.age);
            } copy];
            
            //如果是MRC person离开{}之前要进行release
            [person release];
        }

        NSLog(@"------");//打断点
    }
    return 0;
}

没打印,person没被释放。所以我们猜想在MRC环境下,copy操作之后,block内部对person做了[person retain]操作,所以person没被销毁。

③ ARC环境

typedef void (^MJBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MJBlock block;
        
        {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"---------%d", person.age);
            };
        }
        
        NSLog(@"------");//打断点
    }
    return 0;
}

ARC环境下,在NSLog处打断点,发现执行到NSLog,person对象没有调用dealloc方法,person没被释放。
这是因为:上面的block捕获了auto变量(MJPerson *person,ARC环境下默认是强引用的,如下所示)所以是NSStackBlock,在栈空间。又因为是ARC环境并且block有强指针指着,所以编译器把block自动copy了一下,变成了NSMallocBlock,在堆空间,堆空间的block就不会随便被销毁了,所以block会一直存在,又因为block内部又有捕获的person指针指向person对象,如下,所以走到断点的时候,person对象不会被释放。

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    MJPerson *__strong person; //ARC环境下,默认强引用
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

小总结:不管是MRC还是ARC,栈空间的block是不会保住捕获的变量的命,堆空间的block可以保住捕获的变量的命。

④ ARC __weak

typedef void (^MJBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MJBlock block;
        
        {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;
            
            __weak MJPerson *weakPerson = person;
            block = ^{
                NSLog(@"---------%d", weakPerson.age);
            };
        }
        
        NSLog(@"------");//打断点
    }
    return 0;
}

打印: MJPerson - dealloc ,person被释放。
ARC环境下,使用__weak修饰,发现person又被释放了,相信看完上面的各种例子也有点懵了,下面进行大总结:

大总结:

无论MRC、ARC,当block内部访问了对象类型的auto变量时(这时就是__NSStackBlock__,放在栈区)

  1. 如果block是在栈上,将不会对auto变量产生强引用
  2. 如果栈上的block被拷贝到堆上(自己拷贝的或者ARC下系统自动拷贝的)
    会调用block内部的copy函数
    copy函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  3. 如果堆上的block被移除
    会调用block内部的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放引用的auto变量(或者release)

2. 验证结论

下面将代码转成C++代码,验证刚才的大总结:

typedef void (*MJBlock)(void);

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MJPerson *__strong person;//捕获的变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    //函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
    _Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    //函数会自动释放引用的auto变量(release)
    _Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; //block大小
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
//初始化传入了__main_block_copy_0函数和__main_block_dispose_0函数的地址

我们主要看__main_block_desc_0,可以发现当捕获的是个对象时,这个结构体就多了三、四两个成员,初始化的时候,第三个成员传入__main_block_copy_0函数的地址,第四个成员传入__main_block_dispose_0函数的地址。为什么当捕获的是个对象就会多着两个函数呢?这也比较容易理解,既然捕获了对象,就要有内存管理相关了,所以这两个函数就需要了。这两个函数的作用可看上面注释,验证了我们上面的大总结:

3. 小题目

下面用几个小题目测试p什么时候释放。

  1. 案例一
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    MJPerson *p = [[MJPerson alloc] init];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"-------%@", p);
    });

    NSLog(@"touchesBegan:withEvent:");
}

打印如下:

2019-11-29 16:15:44.964850+0800 Interview03-测试[65454:6090594] touchesBegan:withEvent:
2019-11-29 16:15:47.965174+0800 Interview03-测试[65454:6090594] -------<MJPerson: 0x600002a43e50>
2019-11-29 16:15:47.965517+0800 Interview03-测试[65454:6090594] MJPerson - dealloc

点击空白之后,发现p不是立马被释放,而是3秒之后被释放了。为什么呢?

因为ARC环境下dispatch_after会默认对block进行Copy操作,从栈区Copy到堆区的时候,block内部会调用_Block_object_assign,又因为p默认是强引用,所以_Block_object_assign函数会对p进行retain操作,所以3秒后block销毁的时候p才会销毁。

  1. 案例二
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    MJPerson *p = [[MJPerson alloc] init];

    __weak MJPerson *weakP = p;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1-------%@",p);

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-------%@", weakP);
        });
    });

    NSLog(@"touchesBegan:withEvent:");
}

person对象在1s后释放

2019-12-02 09:12:06 touchesBegan:withEvent:
2019-12-02 09:12:07 1-------<MJPerson: 0x600001960630>
2019-12-02 09:12:07 MJPerson - dealloc
2019-12-02 09:12:09 2-------(null)

因为外面的block捕获了p,并且是强引用,所以p会在外面的block执行完毕释放,所以是1s后。里面的block捕获了weakP,但是因为是使用__weak修饰的,所以对象并不会retain,1s后,对象被释放掉,由于weakP和p指向的是同一个对象,所以再过2s后打印是null。

  1. 案例三
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    MJPerson *p = [[MJPerson alloc] init];

    __weak MJPerson *weakP = p;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1-------%@", weakP);

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-------%@", p);
        });
    });

    NSLog(@"touchesBegan:withEvent:");
}

因为里面的block捕获了p,并且是强引用,所以p会在里面的block执行完毕再释放,所以是3s后。

person对象在3s后释放

2019-12-02 09:13:29 touchesBegan:withEvent:
2019-12-02 09:13:30 1-------<MJPerson: 0x600003806760>
2019-12-02 09:13:32 2-------<MJPerson: 0x600003806760>
2019-12-02 09:13:32 MJPerson - dealloc

三. __block修饰变量

先看一个小案例:

typedef void (^MJBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //__block int age = 20;
        int age = 20;

        MJBlock block2 = ^{
            age = 30;
            NSLog(@"age is %d", age);
        };

        block2();
    }
    return 0;
}

如上代码,运行会报错,Variable is not assignable (missing __block type specifier),意思是“变量不可赋值,缺少__block修饰”。

为什么不能改?
从上面我们分析C++代码可知,block里面的代码是在__main_block_func_0函数里面执行的,而age是定义在main函数里面的,两个函数的栈空间都不一样,肯定不能改。如果要改也只能改block结构体里面的age,但是main函数里面的age还是改不了啊。

1. 那如何才能改?

① 使用static修饰:

上面的代码加static修饰“static int age = 20;”,发现可以修改,那为什么使用static修饰就可以改呢?还是查看C++代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *age = __cself->age; // bound by copy

            (*age) = 30;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_42e634_mi_0, (*age));
        }

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        static int age = 20;

        MJBlock block2 = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));


        ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);

    }
    return 0;
}

可以发现,结构体中使用了int *age来保存age的地址,在调用block的时候,内部执行__main_block_func_0函数,__main_block_func_0函数再访问age指针,再通过age指针将age值修改“int *age = __cself->age; (*age) = 30;”。

总结:指针传递,block内部可以修改外部成员变量的值。

② 使用全局变量

这个就更不用解释了,block结构体不会捕获全局变量,拿到全局变量直接改就是了。

有时候我们只是想临时改一下,并不想让变量一直在内存中,(如果使用static修饰变量会一直在内存中,全局变量也会一直在内存中),可以使用__block修饰。

③ 使用__block修饰

  1. __block可以用于解决block内部无法修改auto变量值的问题,编译器会将__block变量包装成一个对象。
  2. __block不能修饰全局变量、静态变量(static)(因为__block的作用就是上句👆)。

使用__block修饰“__block int age = 20;”,就能在block内部修改外部变量的值,而且不会修改变量的性质(还是auto变量)。

2. 为什么__block修饰的auto变量可以修改变量值?

那么为什么__block修饰的可以修改呢?还是看C++代码

struct __Block_byref_age_0 {
  void *__isa; //isa指针(指向类对象)
__Block_byref_age_0 *__forwarding; //自己类型的指针,后面可知道是指向自己
 int __flags;
 int __size; //自己的大小
 int age; //age的值
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; //指向__Block_byref_age_0结构体的指针
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

    //先通过age指针拿到__forwarding指针(里面存的就是自己),再通过__forwarding指针拿到自己里面的值,然后修改值为30
            (age->__forwarding->age) = 30;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_ad0be7_mi_0, (age->__forwarding->age));
        }

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        //__block int age = 10;
        __Block_byref_age_0 age = {
            (void*)0,
            (__Block_byref_age_0 *)&age, //自己的地址传给__forwarding(它内部有个指针指向自己)
            0,
            sizeof(__Block_byref_age_0),// 当前结构体多大
            10
        };
        
        //声明block会把__Block_byref_age_0地址传过去
        MJBlock block2 = ((void (*)())&__main_block_impl_0(
                                                           (void *)__main_block_func_0,
                                                           &__main_block_desc_0_DATA,
                                                           (__Block_byref_age_0 *)&age,
                                                           570425344));

        //调用block
        ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
    }
    return 0;
}

首先看__main_block_impl_0函数,定义block结构体的时候第三个参数是(__Block_byref_age_0 *)&age,这是指向__Block_byref_age_0结构体的指针,由于__Block_byref_age_0有个isa,我们可以认为它是个对象,里面保存了age的值,原来代码“__block int age = 10;”代码转成C++代码,就是如下__Block_byref_age_0结构体:

//__block int age = 10;
__Block_byref_age_0 age = {
    (void*)0, //isa 传0
    (__Block_byref_age_0 *)&age, //自己的地址传给__forwarding(它内部有个指针指向自己)
    0,
    sizeof(__Block_byref_age_0),// 当前结构体多大
    10 //age的值为10
};

结构体示意图,如下所示:

__Block_byref_age_0

调用block的时候,block内部会调用__main_block_func_0函数,可以看出:

__Block_byref_age_0 *age = __cself->age; 
(age->__forwarding->age) = 30;

先通过age指针拿到__forwarding指针(里面存的就是自己),再通过__forwarding指针拿到自己里面的值,然后修改值为30。

总结:

使用__block修饰age,会将age包装成__Block_byref_age_0结构体(对象),对象里面存着isa,对象的地址,对象的大小,age的值,然后通过对象里面的__forwarding指针拿到自己,再拿到自己的age值,进行修改。如果没修改外面的变量就不要加__block,因为又包装了一层对象,等用到的时候再加。

问题:执行下面代码会报错吗?

NSMutableArray *arr = [NSMutableArray array];
MJBlock block = ^{
    [arr addObject:@"123"];
};

回答:不会。因为“ [arr addObject:@"123"];”是使用arr而不是修改它的值(例如:arr = nil)。

__block小疑问:

可能你还有一个疑问,使用__block修饰变量的确可以达到修改变量的值的目的,如果要再次访问变量,到底访问的是__Block_byref_age_0结构体还是结构体里面的“int age”呢?

为了找出答案,我们把block强转成底层实现,代码如下:

#import <Foundation/Foundation.h>

typedef void (^MJBlock) (void);

struct __Block_byref_age_0 {
    void *__isa;
    struct __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(void);
    void (*dispose)(void);
};

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_age_0 *age;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block int age = 10;
        
        MJBlock block = ^{
            age = 20;
            NSLog(@"里面访问age is %p", &age);
        };
        
        //将block转成block本质
        struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
        
        block();

        NSLog(@"外面访问%p", &age);
    }
    return 0;
}

在NSLog下面的“}”打断点,如下:

断点

可以打印出外面的age结构体的地址,但是里面的age地址却无法打印。
我们知道,结构体的地址值就是结构体第一个成员的地址值,所以我们可以分析内存计算出里面age的地址,再和打印的地址作比较,计算过程如下:

//结构体地址:0x0000000102808ff0
struct __Block_byref_age_0 {
  void *__isa; // 指针占8字节  第一个成员地址:0x0000000102808ff0 (加8算出下个地址)
__Block_byref_age_0 *__forwarding; // 8  0x0000000102808ff8 (加8算出下个地址)
 int __flags; // int占4字节  0x0000000102809000 (加4算出下个地址)
 int __size; // 4   0x0000000102809004 (加4算出下个地址)
 int age; // 0x0000000102809008 可以看出和打印的地址一样
};

打印结果如下:

里面访问age is 0x102809008

Printing description of blockImpl->age:
(__Block_byref_age_0 *) age = 0x0000000102808ff0

外面访问0x102809008

可以看出,计算出的里面的age的地址和我们打印的age的地址是一样的,而且无论是在block里面还是外面访问的都是里面的age。

当然你也可以直接打印出来,都可以验证。

lldb) p/x blockImpl->age
(__Block_byref_age_0 *) $0 = 0x0000000102808ff0

2019-12-02 14:44:26.701568+0800 Interview01-__block[71657:6467841] 0x102809008

(lldb) p/x &(blockImpl->age->age)
(int *) $2 = 0x0000000102809008

解答了我们的疑问:使用__block修饰变量,再次访问,访问的是__Block_byref_age_0里面的int age变量。

现在想一下,为什么苹果要设计成访问变量(使用了__block修饰)直接访问的就是__Block_byref_age_0里面的变量呢?
因为对于一般的开发者来说,可能不知道被__block修饰后还会包装一次,就像KVO苹果重写class方法一样,是不让开发者知道有这么个操作,所以你访问变量,就把里面那个变量的地址给你了。

Demo地址:block的copy操作和__block

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