Objective-C-(三) Block实现解析

最近研究了下Block的实现代码,解惑了以前一直好奇的Block捕获外部变量,__block,Block回调等特性,在此记录下Block的实现原理。

最简单的没有变量捕获的block

准备工作:在工程中创建了一个Block.c的文件,在里面写了一个名为blockMain的函数,实现了一个简单的没有变量捕获的block::

void blockMain() {

    void(^MyBlock)(void) =  ^{
            printf("block test");
        };
    MyBlock();
    
}

在这个文件目录下执行clang -rewrite-objc Block.c,会生成一个.cpp文件,打开文件就可以看到block的实现代码如下:

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

struct __blockMain_block_impl_0 {
  struct __block_impl impl;
  struct __blockMain_block_desc_0* Desc;
  __blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
       printf("block test");
}

static struct __blockMain_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};

void blockMain() {
    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

可以看到这个没有捕获变量的Block的底层实现主要是有三个结构体和一个函数。

__blockMain_block_impl_0就是我们在上面blockMain函数中写的MyBlock的底层实现,是一个结构体。这个结构体的命名是以block所在的函数名为开头,block在函数中出现的顺序为结尾,拼上block_impl组成,也即:__blockMain + block_impl +0,(下面的函数名和结构体名称也是这样命名的)。这个结构体是由两个结构体成员变量struct __block_impl impl、struct __blockMain_block_desc_0* Desc和一个构造函数__blockMain_block_impl_0所组成:

__block_impl impl

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
  • isa:isa指针,表明block也是一个对象,指向block所属的类型 (_NSConcreteGlobalBlock, _NSConcreteStackBlock, _NSConcreteMallocBlock)
    • _NSConcreteGlobalBlock:全局静态block,不会访问任何外部变量
    • _NSConcreteStackBlock:栈区block,当出了函数作用域后被销毁
    • _NSConcreteMallocBlock:堆去block,当引用计数为0时被销毁
  • Flags: 标志位,表示一些block的附加信息
  • Reserved:保留变量
  • FuncPtr:指向block实现函数的函数指针(block回调就是通过它进行回调的)

__blockMain_block_desc_0

static struct __blockMain_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};
  • reserved:保留字段
  • Block_size:block的大小

并且初始化一个__blockMain_block_desc_0_DATA的结构体实例,后面给block赋值的时候直接传入。

__blockMain_block_func_0

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
       printf("block test");
}

这是block的实现函数,在block的构造函数中被赋值给了FuncPtr函数指针。

__blockMain_block_func_0 函数中有一个__cself的参数,这个函数是结构体指针,指向的就是block自身的结构体实例。也即:在block执行的时候,block会将自身结构体当做参数传入执行函数,这也是为什么在执行block的时候,能够将block捕获的外部变量读取出来的原因:因为__cself指向的就是block结构体实例,而block结构体中追加了捕获的外部变量,所以就可以通过__cself获取到捕获的变量。这个例子没有用到__cself,下面会介绍。

block的赋值和执行:

void blockMain() {

    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

看到这些C++(其实是纯C)结构体和函数之间的互相转换时我是有些懵逼的,后来请教了一个C++比较好的同学,给出了如下解释:对于C语言来说,可以从内存层面重新解释内容,所以任何类型转换都可以转换,只要转换后在寻找函数或者变量的时候能够成功寻址就行。
所以对于上面的赋值和执行去掉类型转换,可以简单理解为如下:

//创建一个__blockMain_block_impl_0类型的结构体,并且赋值给并且赋值给MyBlock结构体指针结构体指针
struct __blockMain_block_impl_0 *MyBlock = &__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA));

//调用 MyBlock
*MyBlock->impl.FuncPtr(MyBlock)

所以上面就很好理解了,第一个方法给block赋值的时候,通过block的赋值函数,第一个参数传入block的实现函数__blockMain_block_func_0,赋值给block内部的void FuncPtr函数指针,让FuncPtr指向block的实现函数。第二个参数传入__blockMain_block_desc_0_DATA结构体实例,赋值给block内部的Desc成员变量,然后将block结构体实例的指针返回给MyBlock保存。

当执行block的时候,通过*MyBlock结构体指针获取到FuncPtr函数指针,然后执行block的实现函数。

上面的block实现比较简单,没有捕获任何的变量,下面来看看捕获变量的block是什么样子的。

捕获外部变量可以根据外部变量的类型分为四种情况:

  • 基本类型的变量(int,float)
  • 静态全局变量
  • 静态局部变量
  • 对象类型

捕获基本类型变量的block

void blockMain() {
    
    int a = 1;
    int b = 2;
    void(^MyBlock)(void) =  ^
        int c = a + b;
        printf("%d", c);
    };
    MyBlock();
    
}

clang rewrite 转换之后代码如下:

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

struct __blockMain_block_impl_0 {
  struct __block_impl impl;
  struct __blockMain_block_desc_0* Desc;
  int a;
  int b;
  __blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  int b = __cself->b; // bound by copy

        int c = a + b;
        printf("%d", c);
    }

static struct __blockMain_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};

void blockMain() {

    int a = 1;
    int b = 2;
    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, a, b));
    ((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

跟上面的没有捕获外部变量的block相比,这个block的结构体内部多了两个成员变量int a和int b,类型和名称跟外部的变量一模一样,然后在对block结构体赋值的时候,将捕获的这两个外部变量作为参数传入block的赋值函数,通过block的赋值函数将这两个成员变量进行了赋值。

可以看到,block捕获的外部变量其实是内部追加了跟外部变量同样的成员变量,然后对这些成员变量进行赋值,然后在__blockMain_block_func_0执行函数中,通过__cself(上面有介绍过这个参数的作用)读取出捕获的成员变量。所以block捕获外部的基本变量是一个值拷贝的过程,即使在block内部修改了这个变量的值,也不会影响外部的变量,修改了也没用,所以当我们对这个捕获的变量进行重新赋值的时候编译器会直接报错提醒我们。

捕获全局变量和静态局部变量的block

分别定义几种不同类型的变量,关于这几种变量的区别就不介绍了:

static int a = 1;       //静态全局变量
int b = 2;              //全局变量

void blockMain() {
    
    static int c = 3;   //静态局部变量
    int d = 4;          //自动变量
    
    void(^MyBlock)(void) =  ^{
        a = 100;        //静态全局变量,全局变量静,静态局部变量都可以在block内部修改
        b = 100;
        c = 100;
        int e = a + b + c + d;
        printf("%d", e);
    };
    MyBlock();
    
}

转换如下:

static int a = 1;  
int b = 2;        

struct __blockMain_block_impl_0 {
  struct __block_impl impl;
  struct __blockMain_block_desc_0* Desc;
  int *c;       
  int d;        
  __blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int *_c, int _d, int flags=0) : c(_c), d(_d) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
  int *c = __cself->c; // bound by copy
  int d = __cself->d; // bound by copy

        a = 100;
        b = 100;
        (*c) = 100;   
        int e = a + b + (*c) + d;
        printf("%d", e);
    }

static struct __blockMain_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};

void blockMain() {

    static int c = 3;
    int d = 4;

    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, &c, d));
    ((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

可以看到,首先静态变量和全局变量都是声明在函数外部的,作用域本来就是全局的,所以在block内部可以直接使用而且可以修改。

主要是看静态变量static int c,block在捕获这个静态变量的时候,如同普通基本类型的变量一样,被追加到block内部保存,不同的是,block内部保存的不是静态变量的值,而是静态变量的指针int *c;,在赋值的时候如下:

//结构体构造函数赋值
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, &c, d));

//也即:
int *c = &c

也即,block捕获的是静态变量的指针,所以在block内部不仅可以访问到这个静态变量,还可以进行修改,不需要加__block。

捕获对象类型的block

    NSObject *obj = [NSObject new];
    __strong NSObject *obj1 = obj;
    void(^MyBlock)(void) =  ^{
        NSLog(@"obj = %@", obj1);
    };
    MyBlock();
}

转换如下:

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

struct __blockMain_block_impl_0 {
  struct __block_impl impl;
  struct __blockMain_block_desc_0* Desc;
  NSObject *obj1;
  __blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, NSObject *_obj1, int flags=0) : obj1(_obj1) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
  NSObject *obj1 = __cself->obj1; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_s_d05ccd4qv0y698mt2mcqp80000gn_T_MallockBlock_7ea9ad_mi_0, obj1);
    }
    
static void __blockMain_block_copy_0(struct __blockMain_block_impl_0*dst, struct __blockMain_block_impl_0*src) {_Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __blockMain_block_dispose_0(struct __blockMain_block_impl_0*src) {_Block_object_dispose((void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __blockMain_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __blockMain_block_impl_0*, struct __blockMain_block_impl_0*);
  void (*dispose)(struct __blockMain_block_impl_0*);
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0), __blockMain_block_copy_0, __blockMain_block_dispose_0};

void blockMain() {

    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
    __attribute__((objc_ownership(strong))) NSObject *obj1 = obj;
    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, obj1, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

可以看到block的结构跟之前的差不多,只是多了一个__blockMain_block_copy_0函数和__blockMain_block_dispose_0函数。这两个函数是负责管理block捕获的对象的生命周期的,分别负责持有对象和释放对象。__blockMain_block_copy_0是在block从栈区拷贝到堆区的时候被调用,同时会retain它内部捕获的对象,__blockMain_block_dispose_0是在block被销毁时调用,同时会release它内部捕获的对象。

如同block捕获基本类型变量一样,对于对象类型的变量,block内部是追加一个同样内存修饰符的变量指向捕获的对象。具体来讲就是,如果外部是__strong NSObject *obj1 = obj;,block内部的成员变量就是__strong NSObject *obj1;,如果外部是__weak NSObject *obj1 = obj;,block内部的成员变量就是__weak NSObject *obj1;。所以block是否强引用捕获的对象,取决于捕获的这个变量的内存语义修饰符。这也是为什么通过__weak可以解决block循环引用的根本原因。

之前我在想,为什么这里不是直接使用对象的引用,这样的话就可以在block内部即能使用对象又可以修改对象。后来想了下,如果这样的话,就不能retain外部的对象了,这样的话对象的引用计数不会加1,那么如果对象出了当前方法的作用域后可能就会被释放了,这样是没有意义的。

__block

前面介绍了block的结构以及block是如何捕获外部变量值。由于block捕获的外部变量值不能修改,所以OC提供了__block修饰符让我们能够修改外部变量值。来看下__block是如何实现改变外部变量的。

void blockMain() {
    
    __block int a = 1;
    void(^MyBlock)(void) =  ^{
        a = 10;
        printf("%d", a);
    };
    MyBlock();
    
    a = 2;
    printf("%d", a);
    
}

转换后:

// __block结构体
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;  //指向自身结构体实例的指针
 int __flags;
 int __size;
 int a; //保存的外部变量
};

struct __blockMain_block_impl_0 {
  struct __block_impl impl;
  struct __blockMain_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref
        //通过__forwarding找到自身实例,然后改变内部的变量a
        (a->__forwarding->a) = 10;
        printf("%d", (a->__forwarding->a));
    }
    
static void __blockMain_block_copy_0(struct __blockMain_block_impl_0*dst, struct __blockMain_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __blockMain_block_dispose_0(struct __blockMain_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __blockMain_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __blockMain_block_impl_0*, struct __blockMain_block_impl_0*);
  void (*dispose)(struct __blockMain_block_impl_0*);
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0), __blockMain_block_copy_0, __blockMain_block_dispose_0};

void blockMain() {
    
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

    (a.__forwarding->a) = 2;
    printf("%d", (a.__forwarding->a));
}

代码跟之前的block略有不同,新增了一个__Block_byref_a_0的结构体和两个管理__block变量引用的函数__blockMain_block_copy_0和__blockMain_block_desc_0(作用类似于上一篇提到的block捕获对象类型变量)。可以看到我们声明的__block变量就变成了这样的结构体实例。byref 其实字面意思就是通过引用。同时block结构体内部会持有这样一个结构体指针指向这个__block变量结构体实例。

//通过 __block 声明,将之前的变量 int a 变成了 __Block_byref_a_0 a 的结构体实例
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};

//将这个__block结构体实例指针赋值给block内部的 __Block_byref_a_0 *a 所持有
    void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

所以我们用的__block修饰符原来是将变量变成了类似block结构体的结构体。

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

这个结构体通过追加一个int a的成员变量保留了之前的变量 int a 的值(类似于block捕获外部变量)。然后还有一个__forwarding指针,这个指针是实现block能够更改捕获的外部变量值的核心。通过这个指针的定义和赋值可以看出来它指向的是自身结构体实例。为什么要这么做呢?

如果__block变量和block都是在栈区,那么__forwarding指针这个时候指向的是栈区的__block结构体实例,通过__forwarding指针获取到__block结构体变量,然后再获取到__block结构体变量内部的变量int a,这个时候就可以读取并且改变int a了。

但是由于栈区的变量在函数返回时就会被释放,所以如果我们想在超出变量作用域后继续使用block,会通过对block发送copy消息,将其拷贝到堆区进行持有。在block被拷贝到堆区的时候,内部持有的__block变量(例如此例中block结构体内部的struct __blockMain_block_desc_0* Desc;)也会被一并拷贝到堆区。那么这个时候在函数返回前__block结构体变量就会有两份,一份在栈区,一份是堆区。这个时候__block结构体变量内部的__forwarding指针就会指向堆区的自己,所以这个时候不论是在函数内部还是在超出函数作用域之外都能正确访问到修改后的__block变量值(即:内部的int a变量)。所以__forwarding指针的作用就是不论__block是在栈区还是堆区都能正确的访问到自身。

block的实现函数:

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

        (a->__forwarding->a) = 10;
        printf("%d", (a->__forwarding->a));
 }

这里通过__cself->a获取到__block变量结构体实例,然后通过 __block内部的__forwarding指针访问自身实例,并取出变量a 进行赋值,这样就实现了修改外部变量的目的。

block的实现大概就是这些,到这里应该就能弄清楚block到底是怎么实现的了。

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

推荐阅读更多精彩内容