iOS-Blocks

花了一段时间对Block深入的研究了一下,以下是我边研究边写的笔记记录,其中大部分内容都是从多线程和内存管理那本书中而来,并加入了自己的说明与总结,对Block有了比较深入的理解。如果哪里有问题,还望留言指出。

正文

什么是blocks,blocks是C语言的扩展功能。概括为:带有(截获)自动变量(局部变量)的匿名函数。

扩展
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由os回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放。
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
5、程序代码区—存放函数体的二进制代码。

一、 数据结构

为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是
clang -rewrite-objc block.c

block的数据结构定义如下:
第一种:

struct __block_impl{
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
struct __main_block_desc_0{
    unsigned long reserved;
    unsigned long Block_size;
};
struct __main_block_impl_0{
    struct __block_impl impl;
    struct __main_block_desc_0 *desc;
};

第二种:

struct __funcName_block_impl_index{
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    struct __funcName_block_desc_index *Desc;
    /* Imported variables. */
}

实际上,代码转化后是第一种的结构体的形式,但是为了便于理解,我们可以直接理解为第二种形式,不过仅是结构体的嵌套方式不一样,而且他们在内存上是完全一样的。解释:如下 2 个结构体 SampleA 和 SampleB 在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息。(此处解释,来源于唐巧一篇文章中对该结构的解释)

struct SampleA {
    int a;
    int b;
    int c;
};
struct SampleB {
    int a;
    struct Part1 {
        int b;
    };
    struct Part2 {
        int c;
    };
};

结构体的理解:

  1. isa 指针,所有对象都有该指针,用于实现对象相关的功能。指向 _NSConcreteStackBlock、_NSConcreteMallocBlock或_NSConcreteGlobalBlock类。
  2. flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
  3. reserved,保留变量。
  4. FuncPtr,函数指针,指向具体的 block 实现的函数调用地址。Block使用的匿名函数部分,实际上被转化为简单的c语言函数来处理。
  5. descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
  6. variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

二、 实质

一个Block实际是个结构体实例(机构体的结构如上所示),block被复制给一个Block变量,该变量实际是个结构体指针,指向该结构体。这与oc对象的本质就一样了,oc对象都是个结构体指针,所以block其实也是个oc对象。

扩展

objc/runtime.h中objc_class和objc_object结构体的定义如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;
```objc
typedef struct objc_class *Class;

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。

typedef struct objc_object *id;,id是一个objc_object结构类型的指针。
可以看出block和id类型很相似。

三、 截获自动变量值

1. 自动变量(局部变量)

Block语法表达式中使用自动变量被当做成员变量追加到了__main_block_impl_0结构体中。

struct __main_block_impl_0{
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    //下边两个即为
    int val;
    const char fmt;
}
  1. Block语法表达式中没有使用的自动变量不会被追加,Blocks的自动变量截获只针对Block中使用的自动变量。
  2. 结构体内声明的成员变量类型与自动变量类型完全相同。(如:const那个仍为const)。

2. 可在block内修改的情况

1.c语言中的变量类型:静态局部变量、全局变量(包括静态全局变量)
  1. 静态局部变量,静态变量的指针被追加到结构体中:
int val;
struct __main_block_impl_0{
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    //下边两个即为
    int *val;
}
  1. 全局变量,不会被添加到结构体中,因为在全局区,使用的时候直接使用,与转化前完全相同,直接修改。
2. 使用__block存储域类说明符,__block说明符类似于static、auto说明符。

在自动变量前加__block :

__block int val = 10;
    void (^blk)(void) = ^{
        val = 3;
    };

转化后的代码为:

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;
};

我们可以看到,加上__block的自动变量竟然变成了结构体实例,这个结构体实例包含原val(val是该实例自身持有的变量,它相当于原自动变量),而block结构体实例中持有__block变量生成的结构体实例指针。所以block结构体中对__block变量的修改实际是对其指针的操作

3. Block存储域

通过之前可知,Block也是oc对象。将Block当做oc对象来看时,该Block的类为_NSConcreteStackBlock/_NSConcreteMallocBlock/_NSConcreteGlobalBlock。

  • _NSConcreteStackBlock该类的实例对象Block设置在栈上。
  • _NSConcreteMallocBlock类的实例对象设置在堆上。
  • _NSConcreteGlobalBlock类的实例对象和全局变量一样,设置在全局区(静态区)。

以下情况,Block为_NSConcreteGlobalBlock类的对象:

  • 全局变量的地方有Block语法。
  • Block语法的表达式中不使用截获的自动变量时。

实际上当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成“将Block从栈上复制到堆上”的代码。

1.以下情况,编译器会自动将Block从栈上复制到堆上:

  • Block作为函数返回值时。
  • 向方法或函数的参数传入Block时,并且在方法或函数中适当的复制了Block。
  • Cocoa框架的方法名中含有usingBlock等时。
  • GCD的API。

2.以下情况,编译器判断不了,不会自动从栈复制到堆:

  • Block作为函数参数时,并且在方法中没有复制Block。

例子:NSArray在使用enumerateObjectsUsingBlock方法时不用手动复制,相反的,在NSArray类的initWithObjects实例方法上传递Block时需要手动复制,因为此时编译器不能判断是否需要复制。

- (id)getBlockArray
{
    NSArray *array = [[NSArray alloc] initWithObjects:
                      [^{NSLog(@"block");} copy],
                      [^{NSLog(@"block");} copy],
                      nil];
    return array;
}

3.下面看一下,对这三种Block分别执行copy的复制结果:

  • 栈上Block,copy,结果:从栈上复制到堆上。
  • 堆上Block,copy,结果:引用计数增加。
  • 全局Block,copy,结果:什么也不做。

不管在何处,用copy方法复制Block都不会产生任何问题。所以在不确定的时候调用copy即可。

4.随意copy可以吗?

但是,将Block从栈复制到堆上是相当消耗cpu的,所以还是搞清楚什么栈上的才需要copy,避免cpu资源浪费。

那么堆上的Block多次调用copy会是什么结果呢?
如下代码:

blk = [[[blk copy] copy] copy];

该代码可解释如下:

{
        blk_t tmp = [blk copy];
        blk = tmp;
    }
    {
        blk_t tmp = [blk copy];
        blk = tmp;
    }
    {
        blk_t tmp = [blk copy];
        blk = tmp;
    }

由此可以看出,ARC有效时完全没有问题。

4. __block变量存储域

  1. 使用__block变量的Block从栈上复制到堆上时,__block变量也会受到影响:
  • Block从栈复制到堆时,__block变量从栈复制到堆,此时Block持有(强引用)__block变量。
  • Block已经在堆上时,复制Block,对__block变量不会产生任何影响。
  1. 当一个__block变量,在多个Block中使用,这些Block从栈复制到堆上时,第一个Block被复制到堆上时,__block变量也会一并被复制到堆上,并被该Block持有(强引用),剩下的Block从栈复制到堆时,被复制的Block持有该__block变量,增加__block变量的引用计数。

  2. __block变量与oc引用计数式内存管理完全相同。使用__block变量的Block持有__block变量。如果Block被废弃,它所持有的__block变量也就被释放。

  3. 前边提到过使用__block修饰的变量,会被转化为一个结构体实例,该结构体结构如下:

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

那么,如果__block变量跟随Block一起被复制到堆上了,如果在Block外修改__block变量他们修改的是同一个值吗?是怎么回事呢?原来栈上的__block变量结构体呢?
上代码:

__block int val = 0;
    void (^blk)(void) = [^{val++;} copy];
    val++;
    blk();

一步一步,分析,首先__block修饰的变量,会在栈上生成个结构体实例,这个结构体实例包含了原来的val变量。然后,Block执行copy的时候,Block结构体实例和__block结构体实例一起又被复制到了堆上,Block语法表达式中使用的val是堆上的结构体实例,Block语法外使用的val是栈上生成的那个结构体实例。(注意关键点来了,__block变量的结构体实例中有个__forwarding,正常情况下一直都是指向该结构体自己的)当__block变量的结构体实例被复制到堆上的时候,之前栈上的那个__block变量结构体实例的__forwarding指针会指向堆中新生成的__block变量的结构体实例,堆中的指向它自己。
所以Block表达式中的val++ 和Block外val++,都是调用的

(val.__forwarding->val)++;

即堆中的那个__block变量生成的结构体实例。

所以,无论是在Block语法中、语法外使用__block变量,还是__block配置在栈上或堆上,都可以顺利访问到同一个__block变量。

四、 截获对象

前边谈的都是Block对普通C语言自动变量的截获,下面来看看Block中使用oc对象时,是如何实现的。

    Model *model = [[Model alloc] init];
    model.name = @"lili";
    void(^blk)(void) = ^{
        NSLog(@"%@",model.name);
    };
    blk();

转化后的代码为:

struct __test1__test_block_impl_0 {
  struct __block_impl impl;
  struct __test1__test_block_desc_0* Desc;
  Model *model;
  __test1__test_block_impl_0(void *fp, struct __test1__test_block_desc_0 *desc, Model *_model, int flags=0) : model(_model) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static struct __test1__test_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __test1__test_block_impl_0*, struct __test1__test_block_impl_0*);
  void (*dispose)(struct __test1__test_block_impl_0*);
} __test1__test_block_desc_0_DATA = { 0, sizeof(struct __test1__test_block_impl_0), __test1__test_block_copy_0, __test1__test_block_dispose_0};

static void __test1__test_block_copy_0(struct __test1__test_block_impl_0*dst, struct __test1__test_block_impl_0*src) {_Block_object_assign((void*)&dst->model, (void*)src->model, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __test1__test_block_dispose_0(struct __test1__test_block_impl_0*src) {_Block_object_dispose((void*)src->model, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void _I_test1_test(test1 * self, SEL _cmd) {
    Model *model = ((Model *(*)(id, SEL))(void *)objc_msgSend)((id)((Model *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Model"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)model, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6j_k3qjqfy94gl_jnc3llgm1nvr0000gn_T_test1_58f22c_mi_0);
    void(*blk)(void) = ((void (*)())&__test1__test_block_impl_0((void *)__test1__test_block_func_0, &__test1__test_block_desc_0_DATA, model, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
  1. 我们会发现,Block生成的结构体实例中多了个成员变量,为对象即指针,它与Block引用的原对象类型完全一致(包括所有权修饰符),指向对象在堆中生成的内容。(我们在ARC的时候提到过,结构体中不能含有oc对象,但是此处oc的运行时能够准确的把握Block从栈复制到堆上以及堆上Block被废弃的时机。我的理解是我们写进去的,编译器不知道怎么处理,此处是编译器自己写进去的,它完全知道)
  2. copy函数和dispose函数
  • copy函数中的_Block_object_assign函数相当于retain实例方法,将对象赋值在结构体成员变量中的对象类型。
  • dispose函数中的_Block_object_dispose函数相当于release实例方法,释放赋值在结构体成员变量中的对象类型。
  1. copy函数和dispose函数调用的时机
  • copy函数:当栈上的Block被复制到堆时,会生成新的Block结构体实例,此时会调用copy函数,将原对象赋值在结构体成员变量中的对象类型。
  • dispose函数:当堆上的Block结构体被废弃时,会调用dispose函数,释放赋值在结构体成员变量中的对象类型。

另外,堆上的Block结构体都是通过从栈上复制过来的,也就是有堆上的结构体之前一定先有栈上的结构体。我们可以打印一下引用计数:

Model *model = [[Model alloc] init];
    NSLog(@"retainCount:%d",_objc_rootRetainCount(model));
    model.name = @"lili";
    void(^blk)(void) = ^{
        NSLog(@"%@",model.name);
    };
    NSLog(@"retainCount:%d",_objc_rootRetainCount(model));
    blk();

打印出来结果为:

2017-09-06 14:07:54.002 BlockTest[42344:3847518] retainCount:1
2017-09-06 14:07:54.002 BlockTest[42344:3847518] retainCount:3

可以发现经过Block之后,mode对象的引用计数增加了两次,应该栈上的Block结构体引用一次,复制到堆上的时候堆上的结构体又引用一次。栈上的结构体引用时直接通过model对象指针赋值给Block结构体成员变量中的对象类型。堆上的Block结构体是通过copy方法来完成了成员变量的对象引用。
(最后这里只是我的个人理解,如果理解有问题,望留言指正。)

前边说了,copy函数是栈上Block复制到堆上时执行,那么问题来了。

  1. 什么时候栈上的Block会复制到堆呢?(划重点)
  • 调用Block的copy实例方法时
  • Block作为函数返回值时
  • 将Block赋值给__strong修饰符的id类型或Block类型的成员变量时
  • 向方法名中含有usingBlock的Cocoa方法或GCD的API中传递Block时

所以,在Block中使用对象类型自动变量时,除了以上情形外,推荐调用Block的copy实例方法。

五、__block变量和对象

__block说明符可以指定任何类型的自动变量。下面就看一下指定oc对象类型。
看代码:

__block id obj = [[NSObject alloc] init];

通过clang转化如下:

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id obj;
};

static void _I_test11_test(test11 * self, SEL _cmd) {
    __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {(void*)0,(__Block_byref_obj_0 *)&obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
}

可以看到,
1.通过__block修饰的对象类型也变成了一个结构体实例(这与__block修饰的普通C语言自动变量一样),原对象实例(直接想成指针更好理解)作为一个成员变量被包含于该结构体实例中。
2.当Block被复制到堆时,该__block结构体实例也会被复制到堆中,堆上新建的__block变量的结构体实例,通过copy函数(_Block_object_assign函数)持有赋值给它的结构体实例中的对象。当堆上的__block变量被废弃的时候,使用_Block_object_dispose释放赋值给__block变量的对象。

  • 所以可以看出,在使用"对象所指向的内容"时与不加__block修饰符时相同,即加不加__block效果都一样(下边有补充的更通俗的说法),加了以后只不过多了一层结构体的包装。
  • __weak和__unsafe_unretained时,加不加__block也一样。
  • __autorelease 和__block一起时,会编译错误。

补充,__block修饰oc对象的时候和__block修饰普通C语言自动变量的时候还有个区别,就是对象本身其实是指针,它其实包含两个元素:
比如:

NSMutableArray *array = [[NSMutableArray alloc] init];
  1. 指针本身,即array。
  2. 还有一个就是指针指向的值,即生成的数组空间。
    所以,对于oc对象来说,
  • __block修饰之前,指针在栈上,值在堆上,所以指针指向不能修改,值可以修改,比如addObject增加内容。
  • __block修饰之后,指针指向和值内容,都在堆上,都可以修改了。

六、循环引用

如果Block中使用了__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆的时候,该对象被Block所持有。这样容易引起循环引用。
可以使用__weak修饰对象,这样Block不会持有对象。
也可以使用__block修饰符,比如:

__block Model *tmp = self;
    _myBlock = ^{
        NSLog(@"name:%@",tmp.name);
        tmp = nil;
    };
    _myBlock();

只要_myBlock()执行以后,Block不再持有self,则不会有问题了。

Block与使用__weak的比较:

优点:

  • 使用__block可以控制对象的持有期间。只要Block持有该对象,则该对象就不会释放。不想使用的时候再释放,可保证使用的时候该对象一定存在。
  • 在不想使用__weak的时候,不用不得已的去使用__unsafe_unretained修饰符(不必担心悬垂指针,野指针),直接用__block就行。

缺点:

  • 必须执行Block才能避免循环引用。

七、ARC无效时

  1. ARC无效时,一般需要手动将Block从栈复制到堆,由于ARC无效,所以肯定要释放。这里我们使用copy实例方法来复制,使用release实例方法来释放。
  2. Blocks是C语言的扩展,所以在C语言中使用Block语法,此时使用Block_copy和Block_release函数来代替实例方法。
  3. __block可以在ARC无效时避免循环引用
    这是由于ARC无效时,当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符的id类型或对象类型的自动变量,则被retain。
    代码如下:
__block Model *tmp = self;
    _myBlock = ^{
        NSLog(@"name:%@",tmp.name);
    };

可以看出,ARC下通过在Block中置空对象来实现避免循环引用;非ARC下仅加个修饰符即可。

注意:ARC有效和无效时,__block的作用差别很大,所以一定要弄清楚。

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

推荐阅读更多精彩内容