简单深入了解block

用了block很长时间,也能避免相关的使用问题,想研究下大体底层实现,看了很多的优秀博客,这里写一下自己的理解。

首先block用Apple文档的话来说,“A block is an anonymous inline collection of code, and sometimes also called a "closure".
个人理解:block就是一个匿名内联的代码集合,有时也叫他"closure"<闭包>
闭包是啥:看到网上一句经典的描述,闭包就是能够读取其他函数内部的函数。

通常来说,block都是一些简短代码片块的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调。
比如:
多线程的相关的操作,GCD苹果都是以block的形式,等等
数组,字典的相关的遍历,等等
网络请求的相关回调之类的,界面跳转传值,等等
当前也有很多的三方框架也应用很多的block,通过block进行异步传值,进行事件响应回调。

使用 clang -rewrite-objc 来深入了解一下block吧。
clang提供的中间代码可以带我们简单的了解一下block。clang可以将我们写完的OC代码编译成C++代码。
1、首先进入程序的目录

Paste_Image.png

2、执行clang


Paste_Image.png

3、生成相关的cpp文件


Paste_Image.png

但是带有
#import <UIKit/UIKit.h>

的类好像是转不不成cpp文件的。

编译的结果内容比较多,一个简单的main.m文件简单的几句代码,编译之后就大概有10万多行。
很多语言都可以只实现编译器前端,生成C中间代码,然后利用现有的很多C编译器后端。

通过代码进行编译查看

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^myBlock1)() = ^{
            printf("hello world1111");
        };
        myBlock1();
        
        void (^myBlock2)() = ^{
            printf("hello world2222");
        };
        myBlock2();

    }
    return 0;
}

clang编译结果如下

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

            printf("hello world1111");
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_1(struct __main_block_impl_1 *__cself) {

            printf("hello world2222");
        }

static struct __main_block_desc_1 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);

        void (*myBlock2)() = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
        ((void (*)(__block_impl *))((__block_impl *)myBlock2)->FuncPtr)((__block_impl *)myBlock2);

    }
    return 0;
}

<1>、首先出现的结构体就是__main_block_impl_0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。
下面还有一个__main_block_impl_1,应该是按照出现序列的(第1个)进行命名的。

<2>、__main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是__block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。从impl.isa = &_NSConcreteStackBlock;

<3>、接着出现的是 __main_block_func_0 函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。
再下面__main_block_desc_0描述的是block的相关信息,大小。

<4>、最下面展示的就是block的相关的调用和实现了。

   void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);

        void (*myBlock2)() = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
        ((void (*)(__block_impl *))((__block_impl *)myBlock2)->FuncPtr)((__block_impl *)myBlock2);

执行的时候实际上就是把block的相关信息传了进去,也就是上面介绍的
__main_block_impl_0
__main_block_func_0
__main_block_desc_0

void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
&__main_block_desc_0_DATA));

以上是一个block的基本。


block可以访问局部变量,甚至可以修改局部变量,那么我们来看一下是怎么实现的。
先看一个直接访问局部变量的示例

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int val = 0;
        void (^myBlock1)() = ^{
            printf("val === %d",val);
        };
        myBlock1();
        
    }
    return 0;
}

clang -rewrite-objc main.m 之后得到的转化代码

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy

            printf("val === %d",val);
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int val = 0;
        void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
        ((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);

    }
    return 0;
}

可以看到__main_block_impl_0中多了一个变量,val
在实现中__main_block_func_0多了一个

int val = __cself->val; // bound by copy

__cself->val,应该是类似于self.val。直接把val作为一个属性来进行操作。 bound by copy这个注释标识代表了,val是被拷贝过来的,内存地址和上面的val 是不一样的。

int val = 0;
        printf(" \nblock外面 %p",&val);
        void (^myBlock1)() = ^{
            printf("\nval === %d",val);
            printf("\nblock里面 %p",&val);
            
        };
        myBlock1();
        printf("\nblock外面 %p\n",&val);

打印的结果

block外面 0x7fff5fbff78c
val === 0
block里面 0x1005000f0
block外面 0x7fff5fbff78c

里外的val的对应的内存地址已经发生变化了,但是当block调用完毕之后,val的地址没有发生变化,也就是说,在block里面使用val的时候复制了一个新的地址进行使用了。也就能理解在block内部修改val是修改不了的,因为val在block的地址和外面的不一样,里面是一个临时copy的一个全新地址的参数。

但是想直接修改局部变量的时候报错了,提示要用__block来进行修饰val。

Paste_Image.png

那么,为什么这个时候不能给val进行赋值呢?


摘自网上的一段解释,理解一下。
因为main函数中的局部变量val和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量val还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就……


当时看完了有几个比较疑惑的点。
1、"main函数中的局部变量val和函数__main_block_func_0不在同一个作用域中"。为什么不在一个作用域。
val 的作用域是main函数,__main_block_func_0是全局的

static void __main_block_func_0

通过这个可以看出。
val只是一个局部变量
2、"main函数栈还没展开完成",这句话啥意思?展开什么意思?
展开就是释放的意思,就是说,main函数被释放了。
3、"当然,在上面代码中,我们可以通过指针来实现局部变量的修改",这个怎么实现

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int val = 0;
        int *bbb = &val;
        printf(" \nblock外面 %p   val = %d",&val,val);
        void (^myBlock1)() = ^{
            *bbb = 30;
            printf("\nblock里面 %p   val = %d",&val,val);
            
        };
        printf("\nblock外面111 %p   val = %d",&val,val);
        myBlock1();
        printf("\nblock外面222 %p   val = %d\n",&val,val);
        
    }
    return 0;
}

打印的结果

block外面 0x7fff5fbff76c   val = 0
block外面111 0x7fff5fbff76c   val = 0
block里面 0x100107508   val = 0
block外面222 0x7fff5fbff76c   val = 30

最终的结果可以看到 val的地址没有发生变化,除了在block里面<block里面 0x100107508 val = 0>,这个打印的其实不是真正的val,是block copy的val,所以还是0。
修改了val的值,并且没有修改val的地址。


来看一下我们熟悉的解决方法
添加__block修饰val对象。并且探索一下,__block怎么实现的。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        
        __block int val = 0;
        printf(" \nblock外面 %p   val = %d",&val,val);
        void (^myBlock1)() = ^{
            
            printf("\nblock里面 %p   val = %d",&val,val);
            
        };
        printf("\nblock外面111 %p   val = %d",&val,val);
        myBlock1();
        printf("\nblock外面222 %p   val = %d\n",&val,val);
        
    }
    return 0;
}

打印结果,内存地址彻底的被修改了。执行void (^myBlock1)()的时候就被修改了

block外面 0x7fff5fbff738   val = 0
block外面111 0x100102088   val = 0
block里面 0x100102088   val = 0
block外面222 0x100102088   val = 0

通过clang看一下情况

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref

            printf("\nblock里面 %p   val = %d",&(val->__forwarding->val),(val->__forwarding->val));

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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; 

        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
        printf(" \nblock外面 %p   val = %d",&(val.__forwarding->val),(val.__forwarding->val));
        void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
        printf("\nblock外面111 %p   val = %d",&(val.__forwarding->val),(val.__forwarding->val));
        ((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
        printf("\nblock外面222 %p   val = %d\n",&(val.__forwarding->val),(val.__forwarding->val));

    }
    return 0;
}

来看一下和val在没有添加__block修饰的时候的clang出来的代码的主要区别

1、由第一个成员__isa指针也可以知道** __Block_byref_val_0也可以是NSObject。
第二个成员
__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,只能说有时候需要指向另一个__Block_byref_val_0结构。后面我们揭晓__forwarding**。
最后一个成员是目标存储变量val。

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

2、__main_block_impl_0中也发生了变化
int val;变成了现在的*__Block_byref_val_0 val; // by ref

3、** __main_block_func_0中和之前也不一样了
int val = __cself->val; // bound by copy
变成了现在的
__Block_byref_val_0 val = __cself->val; // bound by ref
__Block_byref_val_0指针类型变量val通过其成员变量
__forwarding
*指针来操作另一个成员变量。

4、** __main_block_desc_0**中多了两个东西

 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);

从上面的clang编译出来的代码可以看到,block被转化成了__main_block_impl_0结构体实例,该实例持有__Block_byref_val_0结构体实例的指针。
看一下 val 在block中的调用的情况

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

            printf("\nblock里面 %p   val = %d",&(val->__forwarding->val),(val->__forwarding->val));

        }

通过__cself找到结构体__Block_byref_val_0,然后通过__forwarding找到结构体的成员val。成员变量val是该实例自身持有的变量,指向的是原来的局部变量。如图所示:
<a href="http://www.cocoachina.com/ios/20150106/10850.html">图片来自</a>

Paste_Image.png

但是还是存在问题
<1>、__Block_byref_val_0类型的变量对应的val仍然在栈上,当block执行回调的时候,val所对用的栈被释放了怎么办?
<2>、为什么访问val还要通过__forwarding?不直接修修改或者访问val呢?

存储域
上面的clang出的代码可以看出。 isa指向的是 _NSConcreteStackBlock,还有另外的两个类似的
_NSConcreteStackBlock 保存在栈中的block,出栈时会被销毁
_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量
_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁

上面我们的代码,blcok是在栈上生成的,现在创建一个_NSConcreteGlobalBlock类型的block

#import <Foundation/Foundation.h>

void (^myBlock)()=^{
    
    printf("block");
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        myBlock();
    }
    return 0;
}

clang -rewrite-objc main.m之后的代码是

struct __myBlock_block_impl_0 {
  struct __block_impl impl;
  struct __myBlock_block_desc_0* Desc;
  __myBlock_block_impl_0(void *fp, struct __myBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __myBlock_block_func_0(struct __myBlock_block_impl_0 *__cself) {

    printf("block");
}

static struct __myBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __myBlock_block_desc_0_DATA = { 0, sizeof(struct __myBlock_block_impl_0)};
static __myBlock_block_impl_0 __global_myBlock_block_impl_0((void *)__myBlock_block_func_0, &__myBlock_block_desc_0_DATA);
void (*myBlock)()=((void (*)())&__global_myBlock_block_impl_0);

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

        ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    }
    return 0;
}

可以看到

 impl.isa = &_NSConcreteGlobalBlock;

分配在全局的block,当超出变量的作用域的时候,依然可以通过指针进行安全的访问。但是在栈上面的block,如果他所属于的变量的作用域结束,那么该block的作用域结束也就结束了。同样的__block修饰的变量也分配在栈上,当超过该变量的作用域时,该__block修饰的变量也会被废弃。

这个时候就需要一个在堆上面block,生命周期由我们自己控制。
_NSConcreteMallocBlock登场。
这个时候就会将block__block修饰的变量,从栈上复制到堆上面。那么,栈上的block就可以超出所属的变量的作用域了,那么复制到堆上面的block以及__block所修饰的变量仍然可以进行操作。

复制到堆上面的block也就变成

 impl.isa = &_NSConcreteMallocBlock;

而此时 结构体中的__forwarding就发挥了作用,保证能访问从栈上拷贝到堆上的__block修饰的变量。

我们一般可以使用copy方法手动将 Block 或者 __block变量从栈复制到堆上。比如我们把Block做为类的属性访问时,我们一般把该属性设为copy。有些情况下我们可以不用手动复制,比如Cocoa框架中使用含有usingBlock方法名的方法时,或者GCD的API中传递Block时。

当一个Block被复制到堆上时,与之相关的__block变量也会被复制到堆上,此时堆上的Block持有相应堆上的__block变量。当堆上的__block变量没有持有者时,它才会被废弃。(这里的思考方式和objc引用计数内存管理完全相同。)

当栈上的__block修饰的变量被复制到了堆上之后,那么之后访问堆上的变量就通过val->__forwarding->val了。

让我们来看一下上面的第4点不同,多了的那两句话,此时main_block_desc_0多了两个成员函数,分别是** copy dispose分别指向__main_block_copy__0__main_block_dispose__0**

当block从栈上被拷贝到堆上的时候,会调用__main_block_copy_0将__block类型的成员变量val从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量val。

这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_val_0结构体中的__forwarding指针也会指向堆上的结构。

<a href = "http://www.cocoachina.com/ios/20150106/10850.html">图片来自</a>

Paste_Image.png

__block可以指定任何的局部变量,上面的代码有如下代码

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

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

当block从栈上复制到堆上面的时候,会使用__main_block_copy该方法持有变量(相当于retain),当堆上面的block被废弃的时候,就会使用__main_block_dispose释放__block修饰的变量(相当于release)。

我们一开始用的demo,block一直实在栈上的,声明的局部变量也是在栈上的,他们都在一个方法中,生命周期的话都依据所持有的方法。方法释放了,局部变量和block也玩儿完,但是为什么这个时候局部变量想在block中修改还是必须得添加__block呢?

猜想,可能这是苹果指定的规则,block是作为参数传递以供后续回调执行的,block被传递出去了,局部变量持有者可能因为没啥用了就被释放了,那么局部变量也就被释放了,再在block中修改局部变量就危险了。所以,不管在什么时候,blcok中修改局部变量都得添加__block来进行修饰。

理论部分说完了,来玩一下断点看一下情况吧。

Paste_Image.png

不对啊,和之前说好的不一样啊,不应该是在栈上的吗?
不应该是clang以后的_NSConcreteStackBlock吗?难道上面的理论都不成立?网上肯定不只有我瞎扯。
看到大神的博客,安心了。
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/#NSConcreteGlobalBlock__u7C7B_u578B_u7684_block__u7684_u5B9E_u73B0
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <NSMallocBlock: 0x100109960>
。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。
由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。

如有失误请各位路过大神即时指点,或有更好的做法,也请指点一二,在下感激不尽。

参考的网址:
http://www.cocoachina.com/ios/20150106/10850.html
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
http://blog.csdn.net/jasonblog/article/details/7756763
http://blog.csdn.net/hherima/article/details/38620175
http://www.dreamingwish.com/articlelist/category/toturial

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

推荐阅读更多精彩内容