卷中卷1:關(guān)于「Block」的底層研究

時代背景


受到培訓(xùn)碱屁、疫情、大廠裁員的多重打擊蛾找,IT行業(yè)現(xiàn)在極度內(nèi)卷娩脾,而 IT行業(yè)里iOS是當(dāng)之無愧的卷王之王
沒啥好說的打毛,其他行業(yè)與iOS都不在一個維度里面柿赊,根本不與辯駁。

不適應(yīng)行業(yè)幻枉,就要重新選擇行業(yè)碰声。我選擇適應(yīng)行業(yè)。

為什么要研究


之所以選擇block進行研究熬甫,道理很簡單:之前對block沒有深入研究過奥邮。
因為block用起來很簡單,除了要考慮:

  • 1. 變量前加__block可以在block內(nèi)部改變值
  • 2. weak-strong搭配解決引用循環(huán)

之外罗珍,根本不用思考就可以輕松使用洽腺,而OC也是為了方便我們使用才設(shè)計出block。
初衷不是讓每個開發(fā)者都要「搞明白怎么實現(xiàn)block覆旱,才可以使用block蘸朋!」,只是很多人被迫營業(yè)了扣唱。
比如現(xiàn)在藕坯,大家都知道了而我不知道,顯得逼格很低噪沙,所以非常需要多深入了解一點炼彪。

進行底層研究之前,關(guān)于clang查看cpp源碼文件正歼,可以看下: iOS 使用Clang命令失敗的解決

哪些方面值得研究


暫時總結(jié)出這幾點:

  1. block本質(zhì)是什么辐马,怎么實現(xiàn)? 能手寫做一下實現(xiàn)過程嗎
  2. block有哪些類型局义,什么情況下會改變喜爷?
  3. block捕獲變量有哪幾種方式冗疮,__block對變量會做什么操作?
  4. block循環(huán)引用是常見的有哪些檩帐,怎么產(chǎn)生的术幔,怎么解決?

接下來的內(nèi)容是我查閱資料后湃密,加上了一些個人理解诅挑,簡明扼要的說出重點供各位同學(xué)參考。

1.「Block」的本質(zhì)與實現(xiàn)


從表層OC使用來說泛源,block本質(zhì)是個對象(Object)拔妥,擁有isa指針,可以當(dāng)作數(shù)據(jù)進行傳遞(例如放入NSDictionary中)俩由。
從底層C++實現(xiàn)來說毒嫡,block本質(zhì)是個結(jié)構(gòu)體(Struct)癌蚁,并且由多個不同作用的結(jié)構(gòu)體和函數(shù)構(gòu)成幻梯。

接下來,看一下函數(shù)內(nèi)創(chuàng)建的簡單block努释,執(zhí)行clang編譯前后的源碼對比:

OC

int main() {    // 對應(yīng)找下面的main函數(shù)
    @autoreleasepool {
        void (^IIIIIIII)(void) = ^{ // block名稱是IIIIIIII
            123456789; // 方便找clang后所在位置碘梢,如果是字符串會變成亂碼
        };
    }
    return 0;
}

C++

struct __block_impl { // block對象
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

static struct __main_block_desc_0 { // block描述信息
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

struct __main_block_impl_0 { // block結(jié)構(gòu)體創(chuàng)建
  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) { // 執(zhí)行方法
    123456789;
}

int main() {   // 對應(yīng)上面的main函數(shù)
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void (*IIIIIIII)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
                                                                    &__main_block_desc_0_DATA));
    }
    return 0;
}

思考:創(chuàng)建block結(jié)構(gòu)體的名稱「__main_block_impl_0」里的main,是不是與block所在的main函數(shù)關(guān)系呢伐蒂?
我們能夠發(fā)現(xiàn)名稱其實是「__xxx_block_impl_n」這樣的結(jié)構(gòu)煞躬,通過實踐得出:

  1. 如果是block在方法內(nèi)創(chuàng)建,則xxx為所在方法名逸邦,否則xxx為block自身名稱恩沛。
  2. 最后的數(shù)字n,在函數(shù)作用域內(nèi)有n個block時缕减,是這些block的自增id雷客,而函數(shù)外則一直為0

可以看出C++的代碼風(fēng)格桥狡,讓做iOS開發(fā)的非常不適應(yīng):長長短短下劃線搅裙,分不清究竟是干啥的。
原因在于C的命名風(fēng)格是下劃線命名法裹芝,不是駝峰命名法的部逮,我們把這個方法名轉(zhuǎn)換一下試試看:

O-C++

上面代碼中相同顏色的方框,對應(yīng)的是相同的結(jié)構(gòu)名嫂易。按照顏色兄朋,推薦的理解順序是紅綠藍白
轉(zhuǎn)變以后怜械,看起來是不是沒有那么陌生了呢蜈漓?其中的細節(jié)穆桂,我已經(jīng)在上面的代碼中逐行加了注釋。

我們能夠看出block的結(jié)構(gòu)中融虽,最得我們關(guān)注的四個結(jié)構(gòu):

  • block對象 BlockObject ? __block_impl
  • block創(chuàng)建 CreateBlock ? __main_block_impl_0
  • block執(zhí)行 BlockAction ? __main_block_func_0
  • block信息 BlockDesc ? __main_block_desc_0

合起來就是:對象-創(chuàng)建-執(zhí)行-信息享完。
其中,在block捕獲外部變量時有额,BlockDesc中會提供拷貝方法copy以及釋放方法dispose般又,后面再詳細說這里。

2.「Block」的類型


網(wǎng)上很多文章中提到的是這三種類型:

  • _NSConcreteGlobalBlock「全局block」
  • _NSConcreteStackBlock「棧block」
  • _NSConcreteMallocBlock「堆block」

或者另外三種類型:

  • __NSGlobalBlock __ 「全局block」
  • __NSStackBlock __ 「棧block」
  • __NSMallocBlock __ 「堆block」

但是我認為巍佑,拋開程序運行過程不提茴迁,單說這兩大類名稱都是不準(zhǔn)確的。
我們從OC打印和C++源碼中萤衰,可以看到:

O-Block
打印__NSGlobalBlock堕义,源碼是_NSConcreteGlobalBlock
I-Block
打印__NSGlobalBlock,源碼卻是_NSConcreteStackBlock

同一個block卻出現(xiàn)類型不同的情況脆栋,原因在于打印是運行時倦卖,源碼是編譯期
所以椿争,我們應(yīng)該通過程序運行的不同時期進行區(qū)分怕膛,而不應(yīng)將兩類名稱進行混用。

編譯期名稱

  1. 函數(shù)初始化的block均為_NSConcreteGlobalBlock
  2. 函數(shù)內(nèi)初始化的block均為_NSConcreteStackBlock
  3. 我們無法主動初始化_NSConcreteMallocBlock類型的block

運行時名稱

  1. _NSConcreteGlobalBlock在任何情況下秦踪,都會生成__NSGlobalBlock
  2. _NSConcreteStackBlock在沒有引入外部變量1的情況下褐捻,會轉(zhuǎn)為__NSGlobalBlock,在ARC下其他情況2都會自動拷貝到堆椅邓,轉(zhuǎn)為__NSMallocBlock

注釋:
1.引入外部變量:是指使用了外部變量柠逞,不論是用于打印還是作為參數(shù)。
2.其他情況:ARC下block任意使用場景都是堆狀態(tài)景馁,不必深究出現(xiàn)的樣式多少種板壮,底層來說只能算作是同一種情況。

至于為什么編譯的_NSConcreteStackBlock裁僧,運行時如何變?yōu)開_NSGlobalBlock个束,猜測可能是系統(tǒng)在運行時創(chuàng)建block過程中進行了判斷處理。

3.「Block」的變量捕獲


3.1 Block的變量捕獲有哪些方式聊疲?

研究什么是變量捕獲之前茬底,先列出我們常見的多種變量類型:全局變量、靜態(tài)變量获洲、普通數(shù)值變量阱表、OC對象以及帶上__block修飾的變量,在block的代碼塊中打印一下,看看結(jié)果:

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>

int ivar_global_int = 11111;
char * ivar_global_char = "1";

int main() {
    @autoreleasepool {
        static int ivar_static_int = 2;
        int ivar_int = 3;
        NSArray * ivar_array = @[@"1"];
        NSMutableArray * ivar_marray = [[NSMutableArray alloc] init];
        NSObject * ivar_object = [[NSObject alloc] init];

        __block int ivar_int_block = 4;
        __block NSArray * ivar_array_block = @[@"2"];
        __block NSMutableArray * ivar_marray_block = [[NSMutableArray alloc] init];
        __block NSObject * ivar_object_block = [[NSObject alloc] init];
       
        void (^IIIIIIIII)(void) = ^{
            ivar_global_int = 0;
            NSLog(@"ivar_global_int:   %d %p", ivar_global_int, &ivar_global_int);
            NSLog(@"ivar_global_char:  %s %p", ivar_global_char, &ivar_global_char);
            NSLog(@"ivar_static_int:   %d %p", ivar_static_int, &ivar_static_int);
            NSLog(@"ivar_int:          %d %p", ivar_int, &ivar_int);
            NSLog(@"ivar_int_block:    %d %p", ivar_int_block, &ivar_int_block);
            NSLog(@"ivar_array:        _ %p", &ivar_array);
            NSLog(@"ivar_array_block:  _ %p", ivar_array_block);
            NSLog(@"ivar_marray:       _ %p", ivar_marray);
            NSLog(@"ivar_marray_block: _ %p", ivar_marray_block);
            NSLog(@"ivar_object:       _ %p", ivar_object);
            NSLog(@"ivar_object_block: _ %p", ivar_object_block);
        };
        IIIIIIIII();
    }
    return 0;
}
2022-05-06 17:16:48.131846+0800 BlockClangTest[20824:284328] ivar_global_int:   0 0x100008010
2022-05-06 17:16:48.132069+0800 BlockClangTest[20824:284328] ivar_global_char:  1 0x100008018
2022-05-06 17:16:48.132090+0800 BlockClangTest[20824:284328] ivar_static_int:   2 0x100008020
2022-05-06 17:16:48.132103+0800 BlockClangTest[20824:284328] ivar_int:          3 0x10072c7a8
2022-05-06 17:16:48.132113+0800 BlockClangTest[20824:284328] ivar_int_block:    4 0x10072c7c8
2022-05-06 17:16:48.132123+0800 BlockClangTest[20824:284328] ivar_array:        _ 0x10072c770
2022-05-06 17:16:48.132133+0800 BlockClangTest[20824:284328] ivar_array_block:  _ 0x100008050
2022-05-06 17:16:48.132200+0800 BlockClangTest[20824:284328] ivar_marray:       _ 0x10072c6d0
2022-05-06 17:16:48.132269+0800 BlockClangTest[20824:284328] ivar_marray_block: _ 0x10072c720
2022-05-06 17:16:48.132289+0800 BlockClangTest[20824:284328] ivar_object:       _ 0x1007291b0
2022-05-06 17:16:48.132305+0800 BlockClangTest[20824:284328] ivar_object_block: _ 0x1007288b0
Program ended with exit code: 0

思考:OC的內(nèi)存對齊最爬?
從打印的前三行涉馁,我們能夠看出基本數(shù)值類型的內(nèi)存對齊是 8 字節(jié):
0x100008018 - 0x100008010 = 8
0x100008020 - 0x100008018 = 8 (提示:十六進制下8需要加8才可以進位變成10)
而OC對象的內(nèi)存對齊是 16 字節(jié),上圖中看不出這個結(jié)論爱致。
不過我們可以實例化一個NSObject對象object烤送,通過打印 sizeof(object) 結(jié)果為 8 以及 malloc_size((__bridge const void *)object) 結(jié)果為 16 間接得出。

進行clang編譯后糠悯,變量相關(guān)的代碼量非常多帮坚,從中找出最關(guān)鍵的block創(chuàng)建結(jié)構(gòu)體指針的源碼,修改了一下對齊格式便于閱讀互艾,如下:

        // 生成block結(jié)構(gòu)體指針
        void (*IIIIIIIII)(void) = ((void (*)())&__main_block_impl_0
                                   ((void *)__main_block_func_0,
                                    &__main_block_desc_0_DATA,
                                    &ivar_static_int,
                                    ivar_int,
                                    ivar_array,
                                    ivar_marray,
                                    ivar_object,
                                    (__Block_byref_ivar_int_block_0 *)&ivar_int_block,
                                    (__Block_byref_ivar_array_block_1 *)&ivar_array_block,
                                    (__Block_byref_ivar_marray_block_2 *)&ivar_marray_block,
                                    (__Block_byref_ivar_object_block_3 *)&ivar_object_block,
                                    570425344));
        // 調(diào)用block方法 代碼就是: IIIIIIIII()
        ((void (*)(__block_impl *))((__block_impl *)IIIIIIIII)->FuncPtr)((__block_impl *)IIIIIIIII);

從這段源碼中能夠看出试和, 所謂block捕獲變量,不過是將這些變量作為參數(shù)傳遞到結(jié)構(gòu)體內(nèi)部纫普!

仔細看看阅悍,我們還能察覺到有些變量沒有參與這個block的創(chuàng)建,它們是全局變量昨稼。而參與創(chuàng)建的變量节视,有些帶有&符號表示指針訪問,還有一些不帶的表示值訪問悦昵。
另外肴茄,帶__block修飾的變量晌畅,變成了編譯器自動生成的新結(jié)構(gòu)體 __Block_byref_xxx_n 的實例變量但指,并且?guī)в?strong>&符號進行指針訪問
這些差異充分說明不同類型的變量抗楔,被捕捉的方式是不同的棋凳。

總結(jié)一下,我們可以直觀的發(fā)現(xiàn)規(guī)律:

網(wǎng)上很多文章都只將局部變量分成了兩類:static變量auto變量连躏,并將auto變量統(tǒng)歸為值訪問剩岳。但是,我認為這個說法不是很妥當(dāng)入热,在接下來的小節(jié)3.2中拍棕,我們可以探究一下這個問題。

3.2 __block對變量會做什么操作勺良?

從上一個小節(jié)「3.1 Block的變量捕獲有哪些方式绰播?」的第一個圖的代碼中,我們提取出一行帶__block的簡單代碼進行單獨分析尚困,這行代碼會編譯成 __Block_byref_ivar_int_block_0 結(jié)構(gòu)體和__attribute __ 開頭的兩段代碼蠢箩。

   __block int ivar_int_block = 4;               // 會編譯成下面的struct結(jié)構(gòu)體和__attribute__開頭的兩段代碼

我們想要修改一個變量的值時,通常我們會在前面帶上__block修飾符。普通auto變量被__block修飾后谬泌,會被包裝成一個新的結(jié)構(gòu)體滔韵,也可以看作為是一個新的OC對象:

// block新對象
struct __Block_byref_ivar_int_block_0 {
    void *__isa;                                    // isa指針
    __Block_byref_ivar_int_block_0 *__forwarding;   // 指向自身的指針 被copy到堆以后 指向堆對象內(nèi)存地址
    int __flags;                                    // 不太清楚干啥的
    int __size;                                     // 內(nèi)存大小
    int ivar_int_block;                             // 攜帶的數(shù)值
};

這個新的結(jié)構(gòu)體 __Block_byref_ivar_int_block_0 創(chuàng)建實例變量 ivar_int_block,并使用C語言的 __attribute __ 指令來設(shè)置變量的屬性掌实,包含自身內(nèi)存地址陪蜻、內(nèi)存大小變量數(shù)值等信息贱鼻,如下:

// 相當(dāng)于 __block int ivar_int_block = 4;  這行代碼
__attribute__((__blocks__(byref))) __Block_byref_ivar_int_block_0 ivar_int_block = {
    (void*)0,                                            // 對應(yīng)__isa
    (__Block_byref_ivar_int_block_0 *)&ivar_int_block,   // 對應(yīng)__forwarding
    0,                                                   // 對應(yīng)__flags
    sizeof(__Block_byref_ivar_int_block_0),              // 對應(yīng)__size 
    4                                                    // 對應(yīng)ivar_int_block
};

思考:__forwarding指針是自身內(nèi)存地址囱皿,有什么用?
網(wǎng)上一些文章稱忱嘹,在變量a被block拷貝到以后:
1.變量a的__forwording指針地址就變成了堆中新變量b的地址
2.被拷貝到堆的新變量b箱吕,__forwording指針地址是b的自身地址
感到非常合理置谦,但是我也不知道怎么驗證。

從下面block創(chuàng)建結(jié)構(gòu)體指針的源碼中,能夠發(fā)現(xiàn)被block捕獲的不是代碼中的看似帶__block的int變量购笆,而是新的實例變量,并且新變量不像普通變量一樣進行值傳遞矫渔,而是帶上了&符號進行指針傳遞崔列。

所以,我們能夠總結(jié)出屁桑,帶有__block修飾變量應(yīng)該歸為指針訪問医寿,不應(yīng)該和普通auto變量一樣認為是值訪問。

這里有兩點需要思考:

    1. 使用__block來修飾變量蘑斧,是否為block捕獲變量過程的一個環(huán)節(jié)靖秩?
      當(dāng)然不是,沒有任何直接關(guān)系:不修飾也能捕獲竖瘾,修飾了也可以不使用block沟突。這里可以認為創(chuàng)建了一個新的變量,而這個被修飾的數(shù)值參與生成新變量的過程捕传,與接下來的block捕獲變量的過程毫無關(guān)系惠拭。
    1. 這里被block捕獲的是誰,是舊變量的數(shù)值還是新的Block變量庸论?
      從源碼中能夠分析出职辅,看似存在的“舊變量”,已經(jīng)被新的Block變量收在里面聂示,它就像是西游記里“紫金紅葫蘆”一樣域携,“舊變量”想出去是出不去的,更不能被直接拿來使用了催什,而block創(chuàng)建當(dāng)然也只能使用新變量涵亏。

如果不通過自己的理解來領(lǐng)悟事物的底層邏輯宰睡,終究還是感覺心里沒譜。在網(wǎng)上搜尋別人的看法后气筋,我從中發(fā)現(xiàn)了一些自我感覺矛盾的地方拆内,而現(xiàn)在弄明白后理解的更加深刻,實現(xiàn)了學(xué)習(xí)-理解-批判-進步這個我瞎編的過程宠默。

3.3 Block對被捕獲變量的內(nèi)存管理

關(guān)于捕獲變量相關(guān)的內(nèi)容麸恍,這個文章第38條講述的很細致:
ChenYilong/iOSInterviewQuestions · GitHub

其中主要的幾點總結(jié):

  • 1.Block不允許修改外部變量,因為進入 block 中的變量相當(dāng)于進入了另外一個函數(shù)作用域搀矫。
  • 2.在 ARC 中無論是否添加 __block抹沪,block 中的 auto 變量都會被拷貝到堆上。
  • 3.改外部變量必要條件:將 auto 變量封裝為結(jié)構(gòu)體(對象)瓤球。

眾所周知融欧,程序員只能夠管理堆上的內(nèi)存,所以我們可以想到:

  • 1. 全局變量和局部靜態(tài)變量不在堆上卦羡,他們不需要block進行內(nèi)存管理噪馏。

至于其他的變量類型,我們可以列出來進行思考:

  • 2. 原本在棧上的變量绿饵。比如:基本數(shù)值類型欠肾。
  • 3. 原本就在堆上的變量。比如說:OC實例對象拟赊,__block修飾的變量刺桃。

<#未完工#>

4.「Block」循環(huán)引用的發(fā)生和解決


4.1 Block的循環(huán)引用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吸祟,隨后出現(xiàn)的幾起案子瑟慈,更是在濱河造成了極大的恐慌,老刑警劉巖欢搜,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件封豪,死亡現(xiàn)場離奇詭異谴轮,居然都是意外死亡炒瘟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門第步,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疮装,“玉大人,你說我怎么就攤上這事粘都±疲” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵翩隧,是天一觀的道長樊展。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么专缠? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任雷酪,我火速辦了婚禮,結(jié)果婚禮上涝婉,老公的妹妹穿的比我還像新娘哥力。我一直安慰自己,他們只是感情好墩弯,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布吩跋。 她就那樣靜靜地躺著,像睡著了一般渔工。 火紅的嫁衣襯著肌膚如雪锌钮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天引矩,我揣著相機與錄音轧粟,去河邊找鬼。 笑死脓魏,一個胖子當(dāng)著我的面吹牛兰吟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茂翔,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼混蔼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了珊燎?” 一聲冷哼從身側(cè)響起惭嚣,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎悔政,沒想到半個月后晚吞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡谋国,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年槽地,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芦瘾。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡捌蚊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出近弟,到底是詐尸還是另有隱情缅糟,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布祷愉,位于F島的核電站窗宦,受9級特大地震影響赦颇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赴涵,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一沐扳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧句占,春花似錦沪摄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至擂啥,卻和暖如春哄陶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哺壶。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工屋吨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人山宾。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓至扰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親资锰。 傳聞我的和親對象是個殘疾皇子敢课,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

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