Objective-C--Block

參考文章:深入研究Block捕獲外部變量和__block實現(xiàn)原理

Block是什么?

Blocks是C語言的擴充功能,在OS X v10.6 和iOS 4中被引入,并在系統(tǒng)API中被廣泛使用。它很像標準的C函數(shù),但是它除了包含可執(zhí)行代碼外,還包含了執(zhí)行時需要訪問的變量(棧上或堆上)。

簡而言之,Block是能夠捕獲當前作用域變量的匿名函數(shù)。也可以理解為一個可以延遲執(zhí)行的代碼片段。

如何學習Block

Working with Blocks
Blocks Programming Topics
llvm-project開源 - BlocksRuntime

Block的特征

  • Block允許你創(chuàng)建一段代碼并能像變量一樣傳參、返回、存儲,然后在適當?shù)臅r機被執(zhí)行。這為異步編程打下了基礎
  • Block還能從當前作用域中捕獲變量,類似于其他語言的“閉包(closure)”、“ lambdas表達式”等概念。某些情況下Block還能修改被捕獲的原始變量(比如使用__block)

Block語法

  • 聲明Block引用
void (^blockReturningVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int, char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);

可以使用typedef簡化聲明

typedef float (^MyBlockType)(float, float);
MyBlockType myFirstBlock = // ... ;
  • 創(chuàng)建Block
^(float aFloat) {
    float result = aFloat - 1.0;
    return result;
};

注意創(chuàng)建時^左側不需要指明返回類型,且如果沒有參數(shù)的話,^右側可以把括號省略。

  • Block參數(shù)類型
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
  • Block返回類型

  • Block屬性

@property (copy) void (^blockProperty)(void);

Block的應用

  • 將代碼邏輯傳入API,自定義算法實現(xiàn):
char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };

qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
  char *left = *(char **)l;
  char *right = *(char **)r;
  return strncmp(left, right, 1);
});
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }
  • 作為回調參數(shù)傳入API,用來實現(xiàn)異步調用
[self beginTaskWithName:@"MyTask" completion:^{

  NSLog(@"The task is complete");

}];
  • 局部封裝代碼,實現(xiàn)代碼復用和延遲執(zhí)行
  void (^printTip)() = ^{
        NSLog(@"hello word");
    };

    if (needPrintDirectly) {
        printTip();
    }else{
        [self beginTaskWithName:@"MyTask" completion:^{
            printTip();
        }];
    }
  • 構造可分發(fā)的Task,實現(xiàn)并發(fā)編程
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    ...
}];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
  • 能夠捕獲和修改變量,從而延長變量的作用域和生命周期

Block是對象嗎?

Block在OC中的實現(xiàn)如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

從結構圖中很容易看到isa,所以OC處理Block是按照對象來處理的。在iOS中,isa常見的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock這3種。

即Block是一種OC對象,它也可以存放在NSArray或NSDictionary這樣的集合中。

Block三種類型的區(qū)別

三種類型的Block存儲在哪呢?

顧名思義,_NSConcreteStackBlock存儲在棧上,_NSConcreteMallocBlock存儲在堆上,_NSConcreteGlobalBlock存儲在全局區(qū)

那這三種類型怎樣產(chǎn)生的呢?

  • Block中沒有用到外部變量,或只用到全局變量、靜態(tài)變量(包括局部靜態(tài)變量)的都是_NSConcreteGlobalBlock。

  • 除了_NSConcreteGlobalBlock外,剛創(chuàng)建的Block都是_NSConcreteStackBlock。

  • 對_NSConcreteStackBlock進行copy后,就變成了_NSConcreteMallocBlock

以下是Block_copy的一個實現(xiàn),實現(xiàn)了從_NSConcreteStackBlock復制到_NSConcreteMallocBlock的過程。對應有9個步驟。

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;

    // 1
    if (!arg) return NULL;

    // 2
    aBlock = (struct Block_layout *)arg;

    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }

    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;

    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;

    // 8
    result->isa = _NSConcreteMallocBlock;

    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }

    return result;
}

對Block進行Copy的意義?

由于_NSConcreteStackBlock所屬的變量域一旦結束,那么該Block就會被銷毀,但很多情況下我們想延長Block的生命周期:

在MRC下,我們可以通過將block從棧copy到堆上,來延長block的生命周期,所以一般block類型的屬性都會使用copy描述

在ARC下,編譯器會自動的判斷,把block自動從棧copy到堆上,如以下四種情況:

  1. 手動調用copy
  2. Block是函數(shù)的返回值
  3. Block被強引用,Block被賦值給__strong或者id類型
  4. 調用系統(tǒng)API入?yún)⒅泻衭singBlcok的方法

因此,ARC下,block類型的屬性直接用strong描述即可

Block如何捕獲變量

我們來測一下Block中引用四種類型的變量:

  1. 全局變量
  2. 靜態(tài)全局變量
  3. 靜態(tài)局部變量
  4. 自動變量
int global_i = 1;

static int static_global_j = 2;

int main(int argc, const char * argv[]) {

    static int static_k = 3;
    int val = 4;

    void (^myBlock)(void) = ^{
        global_i ++;
        static_global_j ++;
        static_k ++;
        NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
    };

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);

    myBlock();

    return 0;
}

轉換成C++源碼如下

int global_i = 1;

static int static_global_j = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_k;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_k = __cself->static_k; // bound by copy
  int val = __cself->val; // bound by copy

        global_i ++;
        static_global_j ++;
        (*static_k) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
    }

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

    static int static_k = 3;
    int val = 4;

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));

    global_i ++;
    static_global_j ++;
    static_k ++;
    val ++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);

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

    return 0;
}

在__main_block_impl_0中,可以看到局部靜態(tài)變量static_k和自動變量val,被Block從外面捕獲進來,成為__main_block_impl_0這個結構體的成員變量了。

總結一下

  • 能在Block中被修改的變量:全局變量、靜態(tài)全局變量、靜態(tài)局部變量
  • 能被Block捕獲的變量:靜態(tài)局部變量、自動變量
  • Block捕獲外部變量僅僅只捕獲Block閉包里面會用到的值,其他用不到的值,它并不會去捕獲。

相關解釋:

  • 首先全局變量和靜態(tài)全局變量可以被修改,是因為他們作用域很廣,所以在Block中和Block外被操作,它們的值依舊可以得以保存下來。
  • 靜態(tài)局部變量可以被修改,是因為被捕獲的是變量地址,在Block中通過變量地址可以修改原始變量。
  • 自動變量不可以被修改,是因為Block是通過拷貝值的方式捕獲的自動變量,因此不能修改原始變量。OC在編譯的層面就防止開發(fā)者可能犯的錯誤,因為自動變量沒法在Block中改變外部變量的值,所以編譯過程中就報編譯錯誤。

如何在Block中修改捕獲的變量

通過上述例子,我們知道除了全局變量、靜態(tài)全局變量可以被修改外,靜態(tài)局部變量也可以被修改,因為捕獲的是變量地址。

而根據(jù)官方文檔我們可以了解到,在自動變量前加入 __block關鍵字,就可以在Block里面改變外部自動變量的值了。

總結一下在Block中改變變量值有2種方式:

  1. 傳遞內存地址指針到Block中
  2. 改變存儲區(qū)方式(__block)。

__block實現(xiàn)原理

我們繼續(xù)研究一下__block實現(xiàn)原理。

先來看看普通變量的情況。

int main(int argc, const char * argv[]) {

    __block int i = 0;

    void (^myBlock)(void) = ^{
        i ++;
        NSLog(@"%d",i);
    };

    myBlock();

    return 0;
}

把上述代碼用clang轉換成源碼。

struct __Block_byref_i_0 {
  void *__isa;
  __Block_byref_i_0 *__forwarding;
  int __flags;
  int __size;
  int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
  (i->__forwarding->i) ++;
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
  _Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
  _Block_object_dispose((void*)src->i, 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*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};


int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));

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

    return 0;
}

從源碼我們能發(fā)現(xiàn),帶有 __block的變量也被轉化成了一個結構體__Block_byref_i_0,這個結構體有5個成員變量。第一個是isa指針,第二個是指向自身類型的__forwarding指針,第三個是一個標記flag,第四個是它的大小,第五個是變量值,名字和變量名同名。

__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,
(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};

源碼中是這樣初始化的。__forwarding指針初始化傳遞的是自己的地址。然而這里__forwarding指針真的永遠指向自己么?

我們做了個實驗,將Block進行copy后再打印,發(fā)現(xiàn)__forwarding指向了堆上的地址,我們猜想__block也被拷貝到堆上了。

我們把Block通過copy到了堆上,堆上也會重新復制一份__block,并且該Block也會繼續(xù)持有該__block。當Block釋放的時候,__block沒有被任何對象引用,也會被釋放銷毀。

__forwarding指針這里的作用就是針對堆的Block,把原來__forwarding指針指向自己,換成指向_NSConcreteMallocBlock上復制之后的__block自己。然后堆上的變量的__forwarding再指向自己。這樣不管__block怎么復制到堆上,還是在棧上,都可以通過(i->__forwarding->i)來訪問到變量值。


所以在__main_block_func_0函數(shù)里面就是寫的(i->__forwarding->i)。

Block造成的環(huán)引用

為何會造成環(huán)引用呢?

//MRC下運行
__block id block_obj = [[NSObject alloc]init];
id obj = [[NSObject alloc]init];
NSLog(@"***Block前****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
void (^myBlock)(void) = ^{
     NSLog(@"***Block中****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
};
myBlock();
    
void (^myBlockCopy)(void) = [myBlock copy];
NSLog(@"***BlockCopy前****block_obj = [%p , %lu] , obj = [%p , %lu]", &block_obj ,(unsigned long)[block_obj retainCount] , &obj,(unsigned long)[obj retainCount]);
myBlockCopy();

輸出

***Block前****block_obj = [0x7fff5d509bd8 , 1] , obj = [0x7fff5d509ba8 , 1]
***Block中****block_obj = [0x7fff5d509bd8 , 1] , obj = [0x7fff5d509b80 , 1]
***BlockCopy前****block_obj = [0x608000243238 , 1] , obj = [0x7fff5d509ba8 , 2]
***Block中****block_obj = [0x608000243238 , 1] , obj = [0x6080002433b0 , 2]

在MRC下,對于普通的對象,_NSConcreteStackBlock是不強持有的,而_NSConcreteMallocBlock是強持有的。也可以認為對一個_NSConcreteStackBlock進行copy時會強持有已捕獲的普通對象(引用計數(shù)增加)。

而ARC下經(jīng)常會自動將一個Block進行copy到堆上,因此很容易強引用了已捕獲的普通對象,從而可能造成環(huán)引用問題。

在MRC下,__block修飾的對象在整個block進行copy時也會被copy到堆上,但是它的引用計數(shù)沒有變化,即沒有被強持有,這一點可以用來避免環(huán)引用。

而在ARC下,__block修飾的對象在整個Block進行copy時,引用計數(shù)會增加,即仍然會被強持有。

如何打破環(huán)引用呢?

在MRC下:

  1. __block方式
myViewController * __block  myController = [[MyViewController alloc] init];
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
};

在ARC下:

  1. 主動打破環(huán)的方式
__block MyViewController * myController = [[MyViewController alloc] init];
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;  // 注意這里,保證了block結束myController強引用的解除
};
  1. 弱引用的方式(推薦)
MyViewController *myController = [[MyViewController alloc] init];
MyViewController * __weak weakMyViewController = myController;
myController.completionHandler =  ^(NSInteger result) {
    [weakMyViewController dismissViewControllerAnimated:YES completion:nil];
};

思考ARC下的弱引用方式是否很完善?會不會在Block執(zhí)行到一半時weak變量就被釋放掉了?在多線程環(huán)境下這種情況是可能發(fā)生的。

解決方法就是我們在block內新定義一個強引用strongMyController來指向weakMyController指向的對象,這樣多了一個強引用,就能保證block執(zhí)行時weakMyController指向的對象不會被釋放。

strongMyController 雖然是強引用,但是它屬于bolck新聲明的變量,存在于棧中。當函數(shù)執(zhí)行完成后,引用被銷毀,引用關系也被解除了。

最終代碼如下:

MyViewController *myController = [[MyViewController alloc] init…];
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;

  if (strongMyController) {
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
    } else {
        // Probably nothing...
    }
};

捕獲變量的總結

對于非對象的變量來說

自動變量的值,被copy進了Block,不帶__block的自動變量只能在里面被訪問,并不能改變值。


帶__block的自動變量 和 靜態(tài)變量 就是直接地址訪問。所以在Block里面可以直接改變變量的值。


而剩下的靜態(tài)全局變量,全局變量,函數(shù)參數(shù),也是可以在直接在Block中改變變量值的,但是他們并沒有變成Block結構體__main_block_impl_0的成員變量,因為他們的作用域大,所以可以直接更改他們的值。

值得注意的是,靜態(tài)全局變量,全局變量,函數(shù)參數(shù)他們并不會被Block持有,也就是說不會增加retainCount值。

對于對象來說

對于不用__block修飾的普通對象,一開始會像自動變量一樣被拷貝到_NSConcreteStackBlock中,引用計數(shù)無變化;但當Block被copy到堆上時,被捕獲的對象引用計數(shù)增加。

對于__block修飾的對象,一開始會像自動變量一樣被拷貝到_NSConcreteStackBlock中,引用計數(shù)無變化;但當Block被copy到堆上時,分兩種情況:

  • MRC下,被捕獲的對象引用計數(shù)不變。
  • ARC下,被捕獲的對象引用計數(shù)增加。

__block作用的總結:

MRC下

  • 說明變量可改
  • 說明指針指向的對象不做這個隱式的retain操作,打破環(huán)引用

ARC下

  • 說明變量可改
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容