第十五篇:Block底层原理探究

常见的几种block:堆block,栈block,全局block
区分原则:

1.block没有使用外部的变量,或者只使用静态变量 或者全局变量----全局block

2.block使用外部的变量,不是静态变量或者全局变量。再看赋值给强引用--堆blcok,赋值给弱引用--栈block

3.block在创建的时候,其内存是分配到栈上的,分配到栈上内存是系统控制的,可能随时被释放掉,所以需要用copy拷贝到堆上面,用strong也是一样的

下面是打印后是一个全局的block。

   void (^block)(void) = ^{

    };
    block();
    NSLog(@"%@",block);

2022-06-07 21:13:17.617622+0800 Block的类型[45485:579489] <__NSGlobalBlock__: 0x104a240d8>

下面是打印后是一个堆block。

  int a = 1;
    void (^block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
    NSLog(@"%@",block);
2022-06-07 21:16:47.024473+0800 Block的类型[45620:584134] <__NSMallocBlock__: 0x600000e8c480>

下面是打印后是一个栈block。

    int a = 1;
    void (^__weak block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
    NSLog(@"%@",block);
2022-06-07 21:18:16.848433+0800 Block的类型[45687:586771] <__NSStackBlock__: 0x16bd77e48>

下面打印是其计数为1,3,4,为什么是这个顺序呢,我们带着这个问题去探究下?

- (void)blockDemo1{
    
    NSObject *objc = [NSObject new];
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    
//下面是堆block,其实在栈block上copy来的,也就是栈block +1,copy堆上+1
    void(^block1)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    block1();

//下面是一个栈block,进行捕获+1
    void(^__weak block2)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    block2();
    void(^block3)(void) = [block1 copy];//因为已经在堆上了copy就不会变
    block3();
    
    void(^block4)(void) = [block2 copy];//在栈上copy会加1
    block4();

}

2022-06-07 22:06:28.254857+0800 Block的类型[46306:621069] ---1
2022-06-07 22:06:28.254907+0800 Block的类型[46306:621069] ---3
2022-06-07 22:06:28.254942+0800 Block的类型[46306:621069] ---4
2022-06-07 23:56:08.430500+0800 Block的类型[48642:710535] ---4
2022-06-07 23:56:08.430536+0800 Block的类型[48642:710535] ---5

首先我们在代码中添加如下block,然后用clang命令生成.cpp文件,查看里面的源码。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    
    NSObject *objc = [NSObject new];
    void (^block)(void) = ^ {
        NSLog(@"%@",objc);
    };
    
    return NSApplicationMain(argc, argv);
}
int main(int argc, const char * argv[]) {
    static int a = 10;
    __attribute__((__blocks__(byref))) __Block_byref_objc_0 objc = {(void*)0,(__Block_byref_objc_0 *)&objc, 33554432, sizeof(__Block_byref_objc_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_0,(objc.__forwarding->objc));
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a, (__Block_byref_objc_0 *)&objc, 570425344));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_2,block);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return NSApplicationMain(argc, argv);
}

其实block就是一个结构体函数,从impl.isa = &_NSConcreteStackBlock这里看出block在创建的时候是放在栈里面的。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  __Block_byref_objc_0 *objc; // 我们再程序里定义的变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, __Block_byref_objc_0 *_objc, int flags=0) : a(_a), objc(_objc->__forwarding) {
    impl.isa = &_NSConcreteStackBlock; 
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们在代码里定义的是一个堆block,那上面cpp里看出其为栈block,那是在什么时候进行转换的呢,

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    
    NSObject *objc = [NSObject new];
    void (^block)(void) = ^ {//堆block
        NSLog(@"%@",objc);
    };
    
    return NSApplicationMain(argc, argv);
}

打断点调试其会进入汇编指令,其有个_Block_copy


WechatIMG2057.jpeg

下面是对block_copy源码的分析,这里分析也就是block的原理实现了。

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    // 如果 arg 为 NULL,直接返回 NULL

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        // 将 aBlock 内存上的数据全部复制新开辟的 result 上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

当我们被__weak去修饰一个block的时候,也就是栈block的时候,系统不会修饰进行一个copy的操作。不会调用retain_copy


WechatIMG2058.jpeg

copy拷贝从栈拷贝到堆是浅拷贝

我们看其调用的是copy方法,然后调用的是_Block_object_assign ,然后是通过指针指向, *dest = object也就是一个浅拷贝。

// 调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    // 取得 block 中的 Block_descriptor_2
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    // 如果没有 Block_descriptor_2,就直接返回
    if (!desc) return;

    // 调用 desc 中的 copy 方法,copy 方法中会调用 _Block_object_assign 函数
    (*desc->copy)(result, aBlock); // do fixup
}
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
        // 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
        _Block_retain_object(object);
        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

循环引用

下面这个会发生循环引用,这个是因为block被self所持有,当block释放的时候,其self也要释放,self释放的话其里的block也要被释放掉。这里可以用__weak来解决循环引用。

 self.block = ^{
        self.name = @"hpw";
    };

 self.block = ^{//这里还会循环引用是因为这里的block还是捕获的self
        _name= @"hpw";
    };

通过下面的__weak可以解决循环引用问题,

   __weak typeof(self) weakSelf = self;
    self.block = ^{
        weakSelf.name = @"hpw";
    };

下面通过这个weakSelf进行修饰后,发现打印weakSelf.name竟然为null,这是因为被释放了,我们需要加个 __strong typeof(weakSelf) strongSelf = weakSelf,起到一个强弱共舞的效果,来解决这个问题。

-(void)method1 {
    self.name = @"hpw";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
// __strong typeof(weakSelf) strongSelf = weakSelf;这个strongSelf是在block释放完后再释放
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name);
            //NSLog(@"%@",strongSelf.name);

        });
    };
    self.block();
}

2022-06-08 21:33:45.338769+0800 Block的循环引用[4362:1496699] -[HPWViewController dealloc]
2022-06-08 21:33:47.097669+0800 Block的循环引用[4362:1496699] (null)

通过另外一种方式也可以解决循环引用,通过__block定义 __block HPWViewController *vc = self,然后name是vc里的一个属性,通过 NSLog(@"%@",vc.name)这种方式输出。最后需要设置vc=nil,这个是因为vc本身被捕获了,然后需要置nil。

  self.name = @"hpw";
    __block HPWViewController *vc = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
    };
    self.block();

2022-06-08 21:46:42.824182+0800 Block的循环引用[5321:1512113] hpw
2022-06-08 21:46:42.824486+0800 Block的循环引用[5321:1512113] -[HPWViewController dealloc]

通过变量传值也可以避免循环引用,

typedef void(^LGBlock1)(HPWViewController *);

@property (nonatomic, copy) LGBlock1 block1;

    self.name = @"hpw";
    self.block1 = ^(HPWViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.block1(self);

2022-06-08 22:05:54.408852+0800 Block的循环引用[7192:1542507] lg
2022-06-08 22:05:54.409131+0800 Block的循环引用[7192:1542507] -[HPWViewController dealloc]

题型一

下面代码我们运行后,发现dealloc方法没有被调用,说明这个会发生循环引用。 这个原因是因为,static是全局的在程序的生命周期里都会用到,程序停止运行了才会释放。_staticSelf会一直指向self,使得其引用计数加1,所以内存不会释放。

static HPWViewController *_staticSelf;
-(void)test1 {
    __weak typeof(self) weakSelf = self;
    _staticSelf = weakSelf;
}

题型二

下面会产生内存泄漏
strongSelf在跳出block标注1会进行销毁,引用计数会减一。block2里捕获的strongSelf会持有对象,增加内存的引用计数。

- (void)test2 {
    __weak typeof(self) weakSelf = self;
    self.block1 = ^{//block标注1
        __strong typeof(weakSelf) strongSelf = weakSelf;//strongSelf出了这个作用域会被销毁
        weakSelf.block2 = ^{
            NSLog(@"%@", strongSelf);
        };
        weakSelf.block2();
    };
}

Block为什么要用 block()进行调用

下面我们思考为什么在写block时候,要写一句 block(),这个是因为void (^__weak block)(void) 这个调用的源码只是对block的一个保存,通过block()调用下才会被执行。

int main(int argc, const char * argv[]) {
   
    int a = 10;
    void (^__weak block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
  
    return NSApplicationMain(argc, argv);
}

下面这个例子我们知道,全局变量和静态变量可以直接在block里进行修改。下面打印后为11,21,31,这个之所以可以修改是因为其内部压根都没有捕获b,c变量。在block内部可以修改全局变量和静态变量的值,但是不允许修改局部变量的
值。block内部修改局部变量的值需要⽤__block修饰。

int b = 20;
static int c = 30;

int main(int argc, const char * argv[]) {
    
    static int a = 10;
    __block NSObject *objc = [NSObject new];
    NSLog(@"%@",objc);
    void(^__weak block)(void) = ^{
        a ++;
        b ++;
        c ++;
        NSLog(@"%d %d %d %@",a,b,c,objc);
    };
    NSLog(@"%@",block);
    block();
    
    return NSApplicationMain(argc, argv);
}
WechatIMG2062.jpeg

下面是使用__block来修饰局部变量,使得其能被修改。为什么添加__block就可以呢,我们探究下。

   __block int a = 10;
    void (^block)(void) = ^{
        a = 3;
        NSLog(@"%d",a);
    };
    block();

2022-06-09 15:29:03.752914+0800 Block[31123:328829] 3

下面是clang命令后函数输出:

int main(int argc, const char * argv[]) {
    static int a = 10;
    __attribute__((__blocks__(byref))) __Block_byref_objc_0 objc = {(void*)0,(__Block_byref_objc_0 *)&objc, 33554432, sizeof(__Block_byref_objc_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_0,(objc.__forwarding->objc));
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a, (__Block_byref_objc_0 *)&objc, 570425344));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_2,block);
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return NSApplicationMain(argc, argv);
}
int b = 20;
static int c = 30;
struct __Block_byref_objc_0 {
  void *__isa;
__Block_byref_objc_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *objc;
};

__block的底层原理

⽤__block修饰的变量在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有⼀
个 __forwarding 的结构体指针,指向结构体本身。
block创建的时候是在栈上的,在将栈block拷⻉到堆上的时候,同时也会将block中捕获的对象拷
⻉到堆上,然后就会将栈上的__block修饰对象的__forwarding指针指向堆上的拷⻉之后的对象。
这样我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的
__forwarding指针将堆和栈的对象链接起来。因此就可以达到修改的⽬的。
__block不能修饰全局变量和静态变量

WechatIMG2066.jpeg

Block的本质

block的本质就是生成了Block_layout的结构体,同时里面有个isa指针,还有一个Block_descriptor_1。

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    // libffi ->
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    // 如果 arg 为 NULL,直接返回 NULL

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        // 将 aBlock 内存上的数据全部复制新开辟的 result 上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

在Block_descriptor_1里存的是copy、dispose、签名等信息。

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

下面的是signature的标识"V8@?0"


WechatIMG2067.jpeg

下面是copy和dispose的调用,也只有block捕获到外部变量才会使用到。


WechatIMG2068.jpeg

Block_descriptor_2的获取其实就是Block_descriptor_1的地址平移。

static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    // Block_descriptor_2 中存的是 copy/dispose 方法,如果没有指定有 copy / dispose 方法,则返回 NULL
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    // 先取得 Block_descriptor_1 的地址
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    // 偏移 Block_descriptor_1 的大小,就是 Block_descriptor_2 的起始地址
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

下面是block,其中flag类似与之前我们mask掩码,与上啥就会得到对应的数据。

WechatIMG2069.jpeg

题型1

下面在调用的时候会奔溃,因为strongBlock和weakBlock指向的是同一块内存空间。如果在 void(^strongBlock)(void) = weakBlock改成 void(^strongBlock)(void) = [weakBlock copy]就不会奔溃,这个是因为copy把栈上拷贝到堆上,是一个深拷贝(当我们用copy修饰block的时候是把栈上拷贝到堆上,浅拷贝),那么这样就不会影响栈上block了。

- (void)blockDemo1{
    int a = 1;
    void(^ __weak weakBlock)(void) = ^{//这里是一个栈block
        NSLog(@"-----%d", a);
    };
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;   
    void(^strongBlock)(void) = weakBlock;
    blc->invoke = nil;//block实现的指针   
    strongBlock();
}

题型2

下面这个不会奔溃

- (void)blockDemo2{
    
    int a = 1;
    void(^__weak block1)(void) = nil;
    {//blcok1
        void(^__weak block2)(void) = ^{//这是个栈block ,当跳出这个外层的block1时候不会被释放。
            NSLog(@"%d",a);
        };
        block1 = block2;
    }
    block1();
}

题型3

下面[NSObject new]是在堆上面的,因为是手动new创建的。下面打印objc会为null,这个是因为NSObject是在堆上,然后__weak block2这个是在栈上,所以出了作用域被释放了。
捕获对象就是生产一个指针指向obj。捕获对象是个浅拷贝。

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

推荐阅读更多精彩内容