Block底層實(shí)現(xiàn)原理

原文出處:http://www.cnblogs.com/chenxianming/

前言

要探索Block前先說一下我對(duì)Block的理解,我把它理解為:能夠捕獲它所在函數(shù)內(nèi)部的變量的函數(shù)指針义屏、匿名函數(shù)或者閉包。注意紅色部份說的是它的精髓所在蝶怔。希望看我這篇文章的人能夠跟我說的步驟去做踢星,做起來也比較簡(jiǎn)單,基本上是手把手五督,這樣會(huì)有更好的效果充包,更深刻基矮,當(dāng)然如果只看文章就能夠讓讀者明白家浇,那是我更加希望的灌具。

一、首先譬巫,我們準(zhǔn)備一個(gè).m文件咖楣。我這里是main.m。內(nèi)容如下:

int main(int argc, char * argv[]) {
    void (^test)() = ^(){
    };
    test();
}

接下來我要用到一個(gè)命令clang src.m -rewrite-objc -o dest.cpp.這個(gè)意思是用clang編譯器對(duì)源文件src.m中的objective-c代碼轉(zhuǎn)換成C代碼放在dest.cpp文件芦昔。其實(shí)xode編譯時(shí)也會(huì)幫我們轉(zhuǎn)換诱贿。我們這樣就可以dest.cpp在看到我們定義和調(diào)用的block轉(zhuǎn)換成C是怎么樣的。執(zhí)行命令后查看這個(gè)dest.cpp會(huì)發(fā)現(xiàn)有一大堆代碼。下面我把對(duì)我們有用并能夠說清楚原理的關(guān)鍵貼上來并加以注釋:

//__block_imp: 這個(gè)是編譯器給我們生成的結(jié)構(gòu)體珠十,每一個(gè)block都會(huì)用到這個(gè)結(jié)構(gòu)體

struct __block_impl {
  void *isa;         //對(duì)于本文可以忽略
  int Flags;          //對(duì)于本文可以忽略
  int Reserved;       //對(duì)于本文可以忽略       
  void *FuncPtr;       //函數(shù)指針料扰,這個(gè)會(huì)指向編譯器給我們生成的下面的靜態(tài)函數(shù)__main_block_func_0
};

/*__main_block_impl_0:

是編譯器給我們?cè)趍ain函數(shù)中定義的block

void (^test)() = ^(){
};

生成的對(duì)應(yīng)的結(jié)構(gòu)體

*/
struct __main_block_impl_0 {
struct __block_impl impl;          //__block_impl 變量impl
struct __main_block_desc_0* Desc;    //__main_block_desc_0 指針,指向編譯器給我們生成的結(jié)構(gòu)體變量__main_block_desc_0_DATA __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {  //結(jié)構(gòu)體的構(gòu)造函數(shù)
    impl.isa = &_NSConcreteStackBlock;  //說明block是棧blockimpl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;}};
//__main_block_func_0:

編譯器根據(jù)block代碼生成的全局態(tài)函數(shù)焙蹭,會(huì)被賦值給impl.FuncPtr

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    }

//__main_block_desc_0: 編譯器根據(jù)block代碼生成的block描述,主要是記錄下__main_block_impl_0結(jié)構(gòu)體大小

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)}; //這里就生成了__main_block_desc_0的變量__main_block_desc_0_DATA
//這里就是main函數(shù)了
int main(int argc, char * argv[]) {
    void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //下面單獨(dú)講
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);                          //下面單獨(dú)講
}

what the hell is that!!!! 沒錯(cuò)撰豺,這也是我一開始看到這堆東西的感受。因?yàn)楹芏嗳酥vBlock原理都貼這個(gè)而且沒有注釋或很少注釋小作,我也從網(wǎng)上搜出來看了幾個(gè)变抽。接下來就要說明白這堆代碼。一定要有耐心,首先阳谍,對(duì)著上面代碼注釋過幾遍main函數(shù)前系統(tǒng)給我們生成的這些結(jié)構(gòu)體函數(shù)之間的關(guān)系训貌,如果一次能明白自然是好,過了幾遍都沒明白也沒關(guān)系儒飒,但如果不明一定回頭要再理清圣猎。

回歸一開始我對(duì)block的理解,先忽略它能夠捕獲所在函數(shù)內(nèi)部的變量,那么它就是一個(gè)函數(shù)指。

void (^test)() = ^(){
    };

就對(duì)應(yīng)著

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

這個(gè)總的來說就是定義一個(gè)函數(shù)指針指向一個(gè)地址祠丝,但是這個(gè)地址并不是我樣平常的函數(shù)的入口地址

轉(zhuǎn)換后代碼的要一段段從后往前組合分析:

__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))就是創(chuàng)建了一個(gè)__main_block_impl_0結(jié)構(gòu)體的一個(gè)實(shí)例

&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))取這個(gè)實(shí)例的地址

((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))把實(shí)例地址強(qiáng)轉(zhuǎn)為一個(gè)函數(shù)地址

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

那么這整句就是說定義一個(gè)函數(shù)指針指向一個(gè)新創(chuàng)建的__main_block_impl_0實(shí)例的地址叠蝇。注意創(chuàng)建這個(gè)實(shí)例時(shí)構(gòu)選函數(shù)傳的兩個(gè)參數(shù)单芜,

正是編譯器幫我們生成的靜態(tài)函數(shù)__main_block_func_0及__main_block_desc_0的變量__main_block_desc_0_DATA

test();

對(duì)應(yīng)著

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

總的來說意思就是通過函數(shù)指針test調(diào)用函數(shù)FnucPtr,傳的參數(shù)為指針test本身。

雖然能夠理解這句的意思,但這里有點(diǎn)隱晦稚疹,還是要進(jìn)行說明一下

1柳沙、調(diào)用時(shí)不是應(yīng)該這樣調(diào)才對(duì)嗎 test(test它指向__main_block_impl_0)->impl.FuncPtr,其實(shí)它跟((__block_impl *)test)->FuncPtr)是同等作用数初。

2寺谤、FuncPtr(即__main_block_func_0)的參數(shù)類型不是__main_block_impl_0 ,為什么clang編譯出來后是__block_impl泵琳。其實(shí)這里不管類型是什么蛔垢,它還是傳了test作為參數(shù)進(jìn)去,所是不會(huì)有錯(cuò)的饭聚。

好了講到這里,就可以進(jìn)行一個(gè)中途簡(jiǎn)單性的總結(jié):忽略中間的復(fù)雜分支,留下主線,當(dāng)我們聲明一個(gè)block變量a并為它賦值時(shí),其實(shí)就是創(chuàng)建一個(gè)函數(shù)指針ptrA,再根據(jù)block a賦值的代碼生成一個(gè)靜態(tài)函數(shù)节槐,而指針ptrA就指向這個(gè)靜態(tài)函數(shù)揍庄。block a調(diào)用時(shí)就是使用函數(shù)指ptrA調(diào)用生成的靜態(tài)函數(shù)蒂破。

講到這里第一部分就結(jié)束了喇伯,接下來進(jìn)行第二部分箩朴。

二埠居、這部分就要開始講精髓的部分绎橘,捕獲它所在函數(shù)內(nèi)部的變量,接下來的部分都不會(huì)像第一部分那樣寫那么詳細(xì)的注釋慌盯,只會(huì)在關(guān)鍵和不一樣的地方加上注釋匠楚。并且通過觀看不同變化,從實(shí)踐中得出結(jié)論并明白它實(shí)現(xiàn)原理。即然要說捕獲它所在函數(shù)內(nèi)部的變量廓潜,那么接下來我們就把main.m修改一下,加個(gè)變量(基本類型變量)唄钮呀。代碼變成這樣:

int main(int argc, char * argv[]) {
    int value = 1;
    void (^test)() = ^(){
        int valueTest = value;
    };
    test();
}

那么經(jīng)過clang轉(zhuǎn)換之后會(huì)變成這樣, 與第一部份不一樣的地方我會(huì)把它變成粗體哪痰,仔細(xì)對(duì)比第一部分并思考抑诸,應(yīng)該不難理解号醉。

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;
  int value;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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

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, char * argv[]) {
    int value = 1;
    void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}

首先我們可以看到的變化點(diǎn)有:1、__main_block_impl_0結(jié)構(gòu)體中多了個(gè)value娇澎,其實(shí)它就是用來保存main函數(shù)中的value,還有它的構(gòu)造函數(shù)多了一個(gè)參數(shù)2、__main_block_func_0這個(gè)函數(shù)的實(shí)現(xiàn)會(huì)新增一個(gè)變量value并被賦值乏苦。

從對(duì)比中我們可以知道拢驾,變量其實(shí)是在構(gòu)造__main_block_impl_0實(shí)例時(shí)傳進(jìn)去了并被保存架忌,當(dāng)回調(diào)時(shí)通過把test(其實(shí)就是指向一個(gè)__main_block_impl_0實(shí)例)作為參數(shù)傳進(jìn)來破加,通過它拿到了變量。這樣就實(shí)現(xiàn)了捕獲局部變量汽久。當(dāng)block要捕獲多個(gè)變量時(shí)會(huì)是怎么的呢窜管?其實(shí)不難猜纬乍,有N個(gè)變量要被捕__main_block_impl_0結(jié)構(gòu)體中就會(huì)有N個(gè)變量用于保存蜓氨,它的構(gòu)造函數(shù)就會(huì)有N個(gè)參數(shù)是用來傳這N個(gè)變量進(jìn)來保存缠借〗形冢回調(diào)時(shí)通過test(指向__main_block_impl_0實(shí)例)一一拿到。這里就不貼代碼了寞奸,有興趣可以自己驗(yàn)證一下。

原來捕獲函數(shù)內(nèi)部變量其實(shí)就是這樣實(shí)現(xiàn)的呀饥伊。有了上面的基礎(chǔ)杖剪,你是否會(huì)想那么__block修飾的變量是怎么樣的?變量是個(gè)NSObject對(duì)象是怎么樣的糕伐?

回調(diào)傳參又是怎么樣的训唱?還有人們經(jīng)常說的對(duì)self的引用什么的會(huì)是怎么樣摊册?接下來就進(jìn)入第三部分茅特。有了前面兩部份的基礎(chǔ)熬荆,后面的基本就是看代碼得結(jié)論符相,會(huì)少很多文字說明了硼一。所以前面兩部份一定要理解好。

三具伍、 在寫這部份前想著翅雏,不就像前面一樣用clang一下圈驼,對(duì)比一下代碼就可以知道了嗎人芽。先來簡(jiǎn)單一點(diǎn)的。

(1)帶有參數(shù)和返回值的block.

把main.m改成這樣

int main(int argc, char * argv[]) {
    int (^test)(int a) = ^(int a){
        return a;
    };
    test(1);
}

接著它轉(zhuǎn)換后的:

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 int __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
        return a;
    }

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, char * argv[]) {

    int (*test)(int a) = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)test)->FuncPtr)((__block_impl *)test, 1);
}

這里應(yīng)該沒什么難度绩脆,不難理解萤厅,就是__main_block_func_0函數(shù)多了個(gè)參數(shù)和返回值,就不細(xì)說了靴迫。

(2)加上了__block修飾符的基本變量時(shí):

把main.m代碼改成這樣:

int main(int argc, char * argv[]) {
    __block int value = 1;
    void (^test)() = ^(){
        value = 2;
    };
    test();
    int value1 = value;
}

轉(zhuǎn)換后就變成:(接下來會(huì)稍微有點(diǎn)復(fù)雜惕味,不要緊,只要耐心點(diǎn)也是可以明白的)

//這個(gè)是導(dǎo)出的一些接口玉锌,用于管理__block變量value內(nèi)存的一些接口

extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);

extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);


//根據(jù)帶__block修飾符的變量value名挥,編譯器給我們生成了個(gè)結(jié)構(gòu)體
struct __Block_byref_value_0 {
  void *__isa;
__Block_byref_value_0 *__forwarding;   //這個(gè)會(huì)指向被創(chuàng)建出來的__Block_byref_value_0實(shí)例
int __flags;
 int __size;
 int value;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value;  //保存__Block_byref_value_0變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__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_value_0 *value = __cself->value; // bound by ref

        (value->__forwarding->value) = 2;
    }

//這兩個(gè)函數(shù)分別會(huì)在test block 被拷貝到堆和釋構(gòu)時(shí)調(diào)用的,作用是對(duì)__Block_byref_value_0實(shí)例的內(nèi)存進(jìn)行管理主守,至于怎么管理禀倔,這里就不討論了,這里就會(huì)調(diào)用上面導(dǎo)出來的接口参淫。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 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*); //回調(diào)函數(shù)指針救湖,會(huì)被賦值為__main_block_copy_0

 void (*dispose)(struct __main_block_impl_0*);            //回調(diào)函數(shù)指針,會(huì)被賦值為__main_block_dispose_0
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; /*{ 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}涎才,這句就是創(chuàng)建一個(gè)例的意思鞋既,這是結(jié)構(gòu)體的一種構(gòu)造方式。*/


int main(int argc, char * argv[]) {
    /*我們定義的__block int value轉(zhuǎn)換后并不是一個(gè)簡(jiǎn)單的棧變量,而會(huì)是新建的__Block_byref_value_0堆變量*/
     __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 1};

void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
  //最后面使這句int value1 = value;使用value時(shí)邑闺,在我們表面看到是好像是使用main函數(shù)里的一個(gè)局部棧變量跌前,其實(shí)不是,使用的是堆里面的容int value1 = (value.__forwarding->value);
}

從代碼里的注釋再加上前面兩部份這講解陡舅,應(yīng)該是可以看明白這段代碼的舒萎。簡(jiǎn)單做個(gè)說明:一開始我會(huì)猜想__block修飾的變量的值能在block代碼塊中被修改,不就是在第二部分中的傳一個(gè)變量值變成傳這個(gè)變量的地址進(jìn)去嗎蹭沛?其實(shí)這樣是有問題的臂寝,要明白如果這樣,就是相當(dāng)是傳了一個(gè)棧變量的地址進(jìn)去摊灭,函數(shù)結(jié)束這個(gè)地址就不可用了咆贬,編譯器才會(huì)給我們創(chuàng)建一個(gè)新的結(jié)構(gòu)__Block_byref_value_0

小結(jié):

本文是我在學(xué)習(xí)block的過程中,通過看別人文章帚呼,源碼并自己親自己動(dòng)手實(shí)踐得出來的結(jié)果掏缎,宗旨是讓大家更容易明白block的底層實(shí)現(xiàn)原理。

如果想了解本文以外block的知識(shí)或者更深入了解煤杀,參考:biosli眷蜈、tripleCCllvm.org沈自、clang對(duì)block的編譯規(guī)則酌儒,這里就不展開了,有時(shí)間再寫枯途。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末忌怎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子酪夷,更是在濱河造成了極大的恐慌榴啸,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晚岭,死亡現(xiàn)場(chǎng)離奇詭異鸥印,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)坦报,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門库说,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人燎竖,你說我怎么就攤上這事璃弄。” “怎么了构回?”我有些...
    開封第一講書人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵夏块,是天一觀的道長(zhǎng)疏咐。 經(jīng)常有香客問我,道長(zhǎng)脐供,這世上最難降的妖魔是什么浑塞? 我笑而不...
    開封第一講書人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮政己,結(jié)果婚禮上酌壕,老公的妹妹穿的比我還像新娘。我一直安慰自己歇由,他們只是感情好卵牍,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沦泌,像睡著了一般糊昙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谢谦,一...
    開封第一講書人閱讀 52,549評(píng)論 1 312
  • 那天释牺,我揣著相機(jī)與錄音,去河邊找鬼回挽。 笑死没咙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的千劈。 我是一名探鬼主播祭刚,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼队塘!你這毒婦竟也來了袁梗?” 一聲冷哼從身側(cè)響起宜鸯,我...
    開封第一講書人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤憔古,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后淋袖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸿市,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年即碗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了焰情。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡剥懒,死狀恐怖内舟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情初橘,我是刑警寧澤验游,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布充岛,位于F島的核電站,受9級(jí)特大地震影響耕蝉,放射性物質(zhì)發(fā)生泄漏崔梗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一垒在、第九天 我趴在偏房一處隱蔽的房頂上張望蒜魄。 院中可真熱鬧,春花似錦场躯、人聲如沸谈为。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)峦阁。三九已至,卻和暖如春耘成,著一層夾襖步出監(jiān)牢的瞬間榔昔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工瘪菌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撒会,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓师妙,卻偏偏與公主長(zhǎng)得像诵肛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子默穴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容