漫談Block

一、Objective-C發(fā)展史

Objective-C從1983年誕生,已經(jīng)走過了30多年的歷程。隨著時(shí)間的推移,Objective-C支持很多特性,下面是幾個(gè)重要的發(fā)展節(jié)點(diǎn):

  • Object Oriented C —1983
  • Retain and Release
  • Properties —2006(Objective-C2.0發(fā)布)
  • Blocks —Mac OS X 10.6(June 8, 2009) & iOS4.0(June 21, 2010)
  • ARC —Mac OS X 10.6&iOS5(2011)

以上發(fā)展節(jié)點(diǎn)來自這里,時(shí)間來自維基百科和其他網(wǎng)站。至于為什么會(huì)先說這些,主要我發(fā)現(xiàn)很多同學(xué)在了解一項(xiàng)技術(shù)時(shí),沒有一個(gè)立體的概念,經(jīng)常看后會(huì)很快忘記。當(dāng)我們對一個(gè)知識的來龍去脈了解時(shí),這個(gè)知識點(diǎn)就不再是個(gè)點(diǎn),而會(huì)變成一個(gè)面,起到幫助我們記憶的作用。另外我建議大家深入學(xué)習(xí)某項(xiàng)技術(shù)時(shí),不要僅看一些人的分享,現(xiàn)在知識分享已經(jīng)沒有門檻,經(jīng)常會(huì)看到人云亦云的博客(當(dāng)然也可能包括我:)),最好的方式就是先從博客上了解知識,然后從官方途徑上去學(xué)習(xí)驗(yàn)證。推薦大家?guī)讉€(gè)網(wǎng)站,第一個(gè)當(dāng)然是蘋果的開發(fā)文檔;第二個(gè)就是WWDC視頻;如果你還是不盡興,想從源碼方面上了解,可以去看下蘋果開放的源碼-runtimelibDispatchlibclosure,和clang文檔

二、Block的由來

閉包

在講Block之前,我們先了解下閉包的概念:

在計(jì)算機(jī)科學(xué)中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是引用了自由變量的函數(shù)。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。—維基百科

從上面維基百科的解釋中,我們可以看到,一個(gè)閉包即有捕獲自由變量的特性,又具有函數(shù)的特性。閉包在我們?nèi)粘9ぷ髦校?jīng)常用于回調(diào),尤其在延遲調(diào)用(也稱為惰性求值)的情況下非常有用。比如我們經(jīng)常需要在網(wǎng)絡(luò)請求完成時(shí)更新UI,網(wǎng)絡(luò)請求需要一定時(shí)間,在請求結(jié)束時(shí)再調(diào)用更新UI的方法。

Block

先從蘋果官方文檔上,看下關(guān)于Block的描述

Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use to impact behavior when executed.

簡單來講Block是Apple實(shí)現(xiàn)的閉包,它適用于C++,Objective-C,Objective-C++。另外Block需要runtime和clang的支持,下面會(huì)講到

三、Block使用

先看一個(gè)使用Block的場景,代碼如下:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

Block的使用和函數(shù)指針使用非常相似,最大的不同,可能就是函數(shù)指針的*變成了^。下面我們看下Block有哪些東西

blocks.jpg

三、Block存儲域

在iOS開發(fā)過程中,有三種不同類型的Block,分別是:

  • _NSConcreteStackBlock, 棧上的Block,在出了作用域之后會(huì)被釋放
  • _NSConcreteMallocBlock,堆上的Block,在MRR環(huán)境需要手動(dòng)釋放
  • _NSConcreteGlobalBlock,全局的Block,存在data區(qū)域,生命周期和應(yīng)用一樣

在我們初始化Block時(shí)只有_NSConcreteStackBlock/_NSConcreteGlobalBlock兩種類型。注意我們是無法生成_NSConcreteMallocBlock的,只有在_NSConcreteStackBlock調(diào)用__Block_copy時(shí)才會(huì)被copy到堆上,生成_NSConcreteMallocBlock。這一點(diǎn)可以從clang文檔中了解到,可能現(xiàn)在看懂這些還比較困難,沒關(guān)系,我們下面在Block內(nèi)部實(shí)現(xiàn)會(huì)講到。

有兩個(gè)問題需要我們注意下:

  • 在初始化Block時(shí),我們怎么判斷是_NSConcreteStackBlock還是_NSConcreteGlobalBlock

    這里有個(gè)快速判斷的方法。如果Block的body里使用到了外部的非全局變量和非static靜態(tài)變量,那么這個(gè)Block就會(huì)在棧上創(chuàng)建即_NSConcreteStackBlock。反之如果沒有引用變量或者僅引用了全局變量或者static靜態(tài)變量則是全局Block_NSConcreteGlobalBlock,下面有幾個(gè)例子:

    // _NSConcreteGlobalBlock
    int multiplier = 7; // 全局變量
    {
        int (^myBlock)(int) = ^(int num) {
          return num * multiplier;
      };
    }
    // _NSConcreteGlobalBlock
    static int multiplier = 7; // 靜態(tài)變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
    // _NSConcreteStackBlock MRR環(huán)境
    int multiplier = 7; // 局部變量或者實(shí)例變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
    // _NSConcreteMallocBlock ARC環(huán)境
    // 注意這里雖然在打印myBlock時(shí)顯示為NSMallocBlock,但是這并不是說我們創(chuàng)建出了_NSConcreteMallocBlock,而是被隱式地調(diào)用了copy
    int multiplier = 7; // 局部變量或者實(shí)例變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
  • Block在MRR環(huán)境和ARC環(huán)境下使用時(shí),需要注意哪些情況?

    1、在MRR環(huán)境下系統(tǒng)提供了兩種方式把Block從棧上copy到堆上,第一種是顯式調(diào)用copy,第二種是顯式調(diào)用Block_copy宏。當(dāng)然如果使用屬性的話,則需要用copy標(biāo)記,如

    @property (copy) int(^Blk)(int)

    而使用retain標(biāo)記屬性是不行的,大家可以試一下

    2、在ARC環(huán)境下,編譯器幫助我們自動(dòng)插入了copy操作,省去了我們很多的工作量,以下幾種情況編譯器會(huì)隱式執(zhí)行copy操作:

    • 將Block作為函數(shù)返回值時(shí)
    • 將Block賦值給__strong修改的局部變量,或者標(biāo)記為strongcopy的屬性時(shí)
    • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時(shí)

    雖然上面的幾種情況幾乎涵蓋了所有Block使用場景,不過仍然一種情況需要我們考慮到,就是把Block當(dāng)方法參數(shù)傳遞給我們自定義方法時(shí)進(jìn)行傳遞時(shí),系統(tǒng)是不幫我們copy的,在使用時(shí)需要我們手動(dòng)進(jìn)行copy,如:

    {
        int multiplier = 7; 
        print(^(int num) {
          return num * multiplier;
      };)
    }
    void print(Blk blk) {
        // NSStackBlock
      blk(1);
    }
    

四、Block的內(nèi)部實(shí)現(xiàn)

在看源碼之前,我們先大概捋下如果讓我們自己實(shí)現(xiàn)一個(gè)閉包該怎么實(shí)現(xiàn),或者需要考慮什么?比如如何捕獲外部變量;如何管理外部變量的內(nèi)存;調(diào)用方式是怎樣的?如果解決了這些問題,我們也能寫一個(gè)閉包的實(shí)現(xiàn),無非是沒有編譯器的加持,調(diào)用起來不是那么優(yōu)雅。好了,說了這么多,我們帶著這些疑問去看Block的實(shí)現(xiàn)源碼,思路會(huì)更加清晰。

我們先準(zhǔn)備好clang里的說明文檔(這個(gè)文檔解釋Block內(nèi)部實(shí)現(xiàn))和Block實(shí)現(xiàn)源碼

Block的實(shí)現(xiàn)主要包括四個(gè)文件:Block.hBlock_private.hdata.cruntime.c

  • Block.h

    // Create a heap based copy of a Block or simply add a reference to an existing one.
    // This must be paired with Block_release to recover memory, even when running
    // under Objective-C Garbage Collection.
    BLOCK_EXPORT void *_Block_copy(const void *aBlock);
    
    // Lose the reference, and if heap based and last reference, recover the memory
    BLOCK_EXPORT void _Block_release(const void *aBlock);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_dispose(const void *, const int);
    
    // Used by the compiler. Do not use these variables yourself.
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
    BLOCK_EXPORT void * _NSConcreteStackBlock[32];
    
    #if __cplusplus
    }
    #endif
    
    // Type correct macros
    
    #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
    #define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
    

    這個(gè)文件包括Block向外提供的方法,我們可以看到Block_copy宏實(shí)現(xiàn),其實(shí)就是調(diào)用了_Block_copy方法,Block_release宏調(diào)用了_Block_release方法,所以我們也可以將宏調(diào)用改為方法調(diào)用。這個(gè)文件是暴露出去的,我們可以在工程中看到:路徑在/usr/include/Block.h,這四個(gè)文件只有這個(gè)文件是暴露的,用來給我們調(diào)用

  • data.c

    BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; 
    BLOCK_EXPORT void * _NSConcreteAutoBlock[32] = { 0 }; // only used in GC
    BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32] = { 0 }; // only used in GC
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32] = { 0 }; // only used in GC
    

    這個(gè)文件定義了6種Block類型。其中三種類型只有GC環(huán)境才會(huì)出現(xiàn),因?yàn)镚C在ARC推出后,已被蘋果廢棄了。所以我們只關(guān)心其中三種類型即可,這也印證了我們上面說的三種Block類型

    BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; 
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
    
  • Block_private.h

    這個(gè)文件就是Block的具體結(jié)構(gòu)

  • runtime.c

    這個(gè)文件描述了Block的copy/release和Block持有變量的copy/release操作。

好了,這就是Block的所有文件,Block_private.hruntime.c兩個(gè)文件是我們需要重點(diǎn)關(guān)注。哦,其實(shí)還有一個(gè)關(guān)聯(lián)的文件沒說,先放著不管,在講到runtime.c會(huì)引出該文件,這個(gè)文件很重要,如果沒有這個(gè)文件我們在閱讀到某塊代碼時(shí)會(huì)有點(diǎn)莫名其妙。

Block的內(nèi)部結(jié)構(gòu)

首先我們從Block內(nèi)部結(jié)構(gòu)說起,上面我們已經(jīng)講到,它在Block_private.h文件中,先貼下Block的結(jié)構(gòu)代碼

#define BLOCK_DESCRIPTOR_1 1
/* Block描述1
 * 如果是_NSConcreteStackBlock時(shí),在copy到堆上時(shí),需要確定Block的大小,用來在堆上分配空間
*/
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size; // Block's size
};

#define BLOCK_DESCRIPTOR_2 1
/* Block描述2
 * 如果是_NSConcreteStackBlock并且捕獲了__block修飾的變量或者(id, NSObject, __attribute__((NSObject)), block, ...)類型的變量,在copy到堆上時(shí)需要生成copy方法,這個(gè)方法用來解決捕獲的變量在被copy時(shí)要執(zhí)行的動(dòng)作。
 * 如果是_NSConcreteGlobalBlock則不會(huì)生成該結(jié)構(gòu)
 */
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

// Block的內(nèi)部布局
struct Block_layout {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 
    volatile int32_t flags; // contains ref count 這個(gè)值主要用來告知系統(tǒng)Block在copy時(shí)應(yīng)該執(zhí)行什么操作
    int32_t reserved; 
    void (*invoke)(void *, ...); // function ptr
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

從上面我們可以了解到,Block由三個(gè)struct構(gòu)成,每個(gè)struct我都做了詳細(xì)的注釋。Block_layout中的flags在文件中也有描述:

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime  only use in GC
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code  only use in GC
    BLOCK_IS_GC =             (1 << 27), // runtime  only use in GC
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE only use in GC
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler  only use in GC
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler  only use in GC
};

去掉GC環(huán)境需要的值

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
};

flags只有這四種值需要我們關(guān)注,其中是BLOCK_HAS_COPY_DISPOSEBLOCK_IS_GLOBAL是我們在使用block時(shí)由編譯器根據(jù)上下文生成的。另外兩個(gè)是在Block被copy時(shí),runtime用到和修改的,這里的runtime特指runtime.c文件,在分析runtime.c文件時(shí),再具體說明。

Block在捕獲非全局和非靜態(tài)變量時(shí),都是copy的不可變變量,在Block的body里是不能修改這個(gè)值的,編譯器會(huì)給我們提示如:

(注解:Block的body里使用全局變量或者靜態(tài)變量,這些變量并不會(huì)進(jìn)行copy,它只做使用,也不會(huì)去管理這些變量的內(nèi)存,如果Block僅僅使用了全局變量或者靜態(tài)變量,Block為_NSConcreteGlobalBlock類型)

block_const_copy.png

如果需要修改變量值,我們可以通過__block修飾變量:

__block int multiplier = 7; // 局部變量或者實(shí)例變量
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};
myBlock(1);

__block修飾的變量會(huì)被Block_byref這樣的結(jié)構(gòu)包起來,具體如下

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding; // 初始化時(shí)會(huì)指向自己,當(dāng)Block被copy時(shí),Block_byref也會(huì)被copy到堆上,forwarding會(huì)指向堆上的Block_byref
    volatile int32_t flags; // contains ref count
    uint32_t size; // Block_byref大小,用來copy時(shí)分配內(nèi)存
};

// Block_byref被copy到堆上和釋放時(shí)需要的操作
struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
    void (*byref_destroy)(struct Block_byref *);
};

如果變量屬于id, NSObject, __attribute__((NSObject)), block, …這種類型,則會(huì)生成byref_keepbyref_destroy方法,來管理變量的內(nèi)存,如果是修飾的是自動(dòng)變量如int,CGPoint,enum類型,則不會(huì)生成結(jié)構(gòu)體Block_byref_2

Block如何管理內(nèi)存

這里引出另外一個(gè)文件runtime.c,該文件包含了Block的copy/release、id, NSObject, __attribute__((NSObject)), block, …類型變量的copy/release和__block修飾的變量的copy/release的具體實(shí)現(xiàn)。里面有幾個(gè)方法是我們需要關(guān)注的:

  • Block的copy方法:_Block_copy

    _Block_copy該方法用來確定Block怎樣進(jìn)行copy

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

這個(gè)方法通過aBlock->flags確定不同類型Block應(yīng)該怎么copy,flags的值都在Block_private.h文件中,上 面分析這個(gè)文件時(shí)有講到,如果忘記了,往前翻一下。

當(dāng)flagsBLOCK_IS_GLOBAL即Block的isa_NSConcreteGlobalBlock時(shí)直接返回不做任何操作

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    ...
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    ...
}

當(dāng)為BLOCK_HAS_COPY_DISPOSE即Block為_NSConcreteStackBlock時(shí),則在堆上生成同樣大小的Block,把堆上的Block的flags改為BLOCK_HAS_COPY_DISPOSE|BLOCK_NEEDS_FREE|2,把isa指向_NSConcreteMallocBlock

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    ...
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

另外會(huì)調(diào)用_Block_call_copy_helper方法,這個(gè)方法實(shí)際上會(huì)調(diào)用Block_descriptor_2Block_private.h里有講到)的copy方法

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

如果Block已經(jīng)被copy到堆上,再調(diào)用copy方法,則只需要增加Block的引用計(jì)數(shù)即可,從實(shí)現(xiàn)上看,Block在連續(xù)調(diào)用Block_copy時(shí)僅增加了Block的引用計(jì)數(shù),并沒有增加對Block持有的變量的引用計(jì)數(shù)

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    ...
}
  • 上面已經(jīng)說了,如果捕獲的變量為id, NSObject, __attribute__((NSObject)), block, …類型變量和__block修飾的變量,則需要調(diào)用Block_descriptor_2->copy()方法,該方法最終會(huì)調(diào)用到_Block_object_assign方法(為什么能調(diào)用到它,我只能說是編譯器干的,現(xiàn)在不需要關(guān)心,等到我們使用clang命令重寫B(tài)lock的使用時(shí),就會(huì)明白了)。

    _Block_object_assign方法用來確定被捕獲的變量怎樣進(jìn)行copy

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;
      
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
      default:
        break;
    }
}

該方法根據(jù)不同的flags值,執(zhí)行不同的操作。如果為id, NSObject, __attribute__((NSObject))類型,則flags為BLOCK_FIELD_IS_OBJECT,從上面可以看到調(diào)用了_Block_retain_object

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;
      ...
    }
}

默認(rèn)_Block_retain_object方法什么也沒做,如果在MRR環(huán)境下,libDispatch會(huì)調(diào)用_Block_use_RR2方法,_Block_retain_object方法會(huì)被賦值為retain操作,具體調(diào)用看object.m這個(gè)文件的void_os_object_init(void)方法,但是在ARC環(huán)境下,該文件是不會(huì)編譯的,文件中也做了說明

object.m
#if _OS_OBJECT_OBJC_ARC
#error "Cannot build with ARC"
#endif

_Block_retain_object實(shí)現(xiàn)如下

static void _Block_retain_object_default(const void *ptr __unused) { }
// 默認(rèn)_Block_retain_object被賦值為_Block_retain_object_default,即什么都不做
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

// Called from CF to indicate MRR. Newer version uses a versioned structure, so we can add more functions
// without defining a new entry point.
// 上面的注釋其實(shí)不是libclosure-65版本的,我從libclosure-63版本摘過來的,不知道蘋果為啥把這個(gè)注釋去掉了,讓人讀起源碼來,會(huì)感覺莫名其妙
void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}

通過上面的分析,可以知道如果在MRR環(huán)境下,Block會(huì)通過_Block_retain_object方法持有id, NSObject, __attribute__((NSObject))類型變量。而在ARC環(huán)境下,_Block_retain_object方法是個(gè)空操作,并不會(huì)持有該類型變量,那ARC環(huán)境下Block怎么樣達(dá)到持有對象類型的變量呢?ARC環(huán)境有了更完善的內(nèi)存管理,如果外部變量由__strongcopystrong修飾時(shí),Block會(huì)把捕獲的變量用__strong來修飾進(jìn)而達(dá)到持有的目的。

在使用Block時(shí),最大的困擾就是RetainCycle(循環(huán)引用),原因很簡單:對象A持有了Block,Block又持有了對象A。因?yàn)锽lock持有了對象A,所以對象A想釋放則必須要先釋放Block,而Block又由于被對象A持有也釋放不了,這就造成了循環(huán)引用。解決循環(huán)引用有兩種辦法,一種是手動(dòng)把Block置為nil來釋放Block對對象A的引用;另外一種就是禁止Block強(qiáng)持有對象A,在MRR和ARC環(huán)境下禁止Block持有對象A的做法是不一樣,ARC環(huán)境下只需要把變量加上__weak修飾就可以避免Block持有變量;而在MRR環(huán)境下只能通過避免調(diào)用_Block_retain_object方法,怎么避免呢,可以往下繼續(xù)看

(這里吐槽下,蘋果的文檔真是亂,什么地方都有,找起來很麻煩不說,libDispatch里面竟然也有Block實(shí)現(xiàn),看文檔的日期,竟然比libclosure-65還新,一度讓我不知道該看哪個(gè),最后我發(fā)現(xiàn),libDispatch里的版本其實(shí)不是最新的Block實(shí)現(xiàn),比如libclosure-65版本廢棄了_Block_use_RR方法,徹底使用_Block_use_RR2按照文檔上的解釋,說這種調(diào)用方式可以隨意增加方法;()

當(dāng)變量由__block修飾時(shí),該變量會(huì)被打包成Block_byref類型,flags會(huì)被標(biāo)記為BLOCK_FIELD_IS_BYREF

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
      ...
    }
}

我們可以看到實(shí)際上是調(diào)用了_Block_byref_copy方法

該方法先在堆上生成同樣大小的Block_byref賦值給堆上的Block,并把flags設(shè)置為src->flags | BLOCK_BYREF_NEEDS_FREE | 4

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            ...

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    ...
    return src->forwarding;
}

從上面可以看到最終會(huì)調(diào)用Block_byref的copy方法,該方法又會(huì)調(diào)用_Block_object_assign方法,如果__block修飾的變量是id, NSObject, __attribute__((NSObject))類型,則flags會(huì)被設(shè)置成BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT,我們看到僅僅做了賦值操作。所以通過__block修飾可以避免調(diào)用到_Block_retain_object方法,也就是在MRR環(huán)境下我們可以通過__block來避免Block強(qiáng)持有變量,進(jìn)而避免循環(huán)引用

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
    }
}

當(dāng)然Block也支持嵌套Block使用,flags會(huì)被標(biāo)記為BLOCK_FIELD_IS_BLOCK,被捕獲的Block被copy就是調(diào)用上面的_Block_copy方法

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break; 
      ...
    }
}

回頭看下runtime.c這個(gè)文件的幾個(gè)方法

_Block_copy方法用來確定_NSConcreteStackBlock_NSConcreteGlobalBlock_NSConcreteMallocBlock三種類型在Block調(diào)用copy時(shí)執(zhí)行的動(dòng)

_Block_object_assign方法用來確定Block捕獲了id, NSObject, __attribute__((NSObject)), block, …類型變量和__block修飾的變量在_NSConcreteStackBlock類型Block copy到堆上時(shí)執(zhí)行的動(dòng)作

_Block_byref_copy方法用來確定被__block修飾的變量在_NSConcreteStackBlock類型Block copy到堆上時(shí)執(zhí)行的動(dòng)作

通過上面的分析,Block在使用不同類型的外部變量時(shí),內(nèi)存管理有一下幾種情況:

  • 當(dāng)外部變量是全局變量或者static靜態(tài)變量時(shí),只使用且不需要管理內(nèi)存;
  • 當(dāng)外部變量是值類型如int、CGPoint時(shí)進(jìn)行值const copy,不需要管理內(nèi)存;
  • 當(dāng)外部變量是對象型變量id, NSObject, __attribute__((NSObject))時(shí),進(jìn)行指針const copy,在Block被copy到堆上時(shí)增加引用計(jì)數(shù)用來持有該變量,在MRR環(huán)境通過_Block_retain_object方法持有變量,在ARC環(huán)境下通過__strong持有變量。這里是解決循環(huán)引用的關(guān)鍵
  • 當(dāng)外部變量被__block修飾時(shí),會(huì)使用Block_byrefstruct包裝該變量,在Block被copy到堆上時(shí)copyBlock_byref,如果__block修飾的是id, NSObject, __attribute__((NSObject))對象型類型,該變量只做指針copy不會(huì)增加變量的引用計(jì)數(shù)。當(dāng)__block修飾的是值類型時(shí),做值const copy
  • 當(dāng)外部變量是block類型時(shí),在Block被copy到堆上時(shí),調(diào)用_Block_copy 進(jìn)行持有,如果外部block類型變量也持有變量,則遞歸進(jìn)行copy

五、Block使用-續(xù)

第三部分講到Block怎么樣使用,第四節(jié)講到了Block的內(nèi)部結(jié)構(gòu)和內(nèi)存管理。這里大家可能有個(gè)疑問,Block在使用時(shí),并沒有聲明那一堆struct啊,而且像什么isa、flags值都是誰設(shè)置的呢。

先看下Block使用例子:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

再看下Block_private.h文件中Block的結(jié)構(gòu):

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size; // Block's size
};

struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

// Block的內(nèi)部布局
struct Block_layout {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 
    volatile int32_t flags; // contains ref count 這個(gè)值主要用來告知系統(tǒng)Block在copy時(shí)應(yīng)該執(zhí)行什么操作
    int32_t reserved; 
    void (*invoke)(void *, ...); // function ptr
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

正常來講,我們應(yīng)該先聲明Block_layout對象,然后實(shí)現(xiàn)invoke方法,告知系統(tǒng)Block是_NSConcreteStackBlock還是_NSConcreteGlobalBlock,再實(shí)現(xiàn)BLOCK_DESCRIPTOR_2的copy方法,管理捕獲的對象的內(nèi)存。可以看到,這種調(diào)用方式太麻煩了,而且需要我們深刻理解Block_layout里每個(gè)變量的含義,稍不留神就會(huì)出錯(cuò)。為了讓我們使用更簡單,這一堆變量的設(shè)置都交給了編譯器去實(shí)現(xiàn)。編譯器可以聰明地把上面的結(jié)構(gòu)轉(zhuǎn)成下面的結(jié)構(gòu)。這里我們借助編譯器前端clang,使用clang -rewrite-objc xxx.m命令重寫成c++代碼,看下編譯器是怎么轉(zhuǎn)換的

好了,我們先建個(gè)工程,假設(shè)叫BlockImpl;然后創(chuàng)建文件,比如也叫BlockImpl,把上面那段代碼copy進(jìn)來

#import "BlockImpl.h"

@implementation BlockImpl
- (void)test {
    int multiplier = 7; // 局部變量或者實(shí)例變量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    myBlock(1);
}
@end

然后,我們在終端中輸入命令clang -rewrite-objc BlockImpl.m(當(dāng)然你首先要cd 該文件所在文件夾:)),回車,你會(huì)發(fā)現(xiàn)在BlockImpl.m同級文件夾中生成了一個(gè)名字叫BlockImpl.cpp文件。這個(gè)文件就是重寫后的c++文件。打開后如下:

#ifndef __OBJC2__
#define __OBJC2__
#endif
struct objc_selector; struct objc_class;
struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};
#ifndef _REWRITER_typedef_Protocol
typedef struct objc_object Protocol;
#define _REWRITER_typedef_Protocol
#endif
#define __OBJC_RW_DLLIMPORT extern
...

幾行代碼被重寫成將近10萬行代碼,沒事兒,不要怕,我們只需要關(guān)注跟我們相關(guān)的代碼,在這我把相關(guān)代碼摘出來,如下:

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

// @implementation BlockImpl

struct __BlockImpl__test_block_impl_0 {
    struct __block_impl impl;
    struct __BlockImpl__test_block_desc_0* Desc;
    int multiplier;
    __BlockImpl__test_block_impl_0(void *fp, struct __BlockImpl__test_block_desc_0 *desc, int _multiplier, int flags=0) : multiplier(_multiplier) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static int __BlockImpl__test_block_func_0(struct __BlockImpl__test_block_impl_0 *__cself, int num) {
    int multiplier = __cself->multiplier; // bound by copy
    return num * multiplier;
}

static struct __BlockImpl__test_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __BlockImpl__test_block_desc_0_DATA = { 0, sizeof(struct __BlockImpl__test_block_impl_0)};

static void _I_BlockImpl_test(BlockImpl * self, SEL _cmd) {
    int multiplier = 7;
    int (*myBlock)(int) = ((int (*)(int))&__BlockImpl__test_block_impl_0((void *)__BlockImpl__test_block_func_0, &__BlockImpl__test_block_desc_0_DATA, multiplier));
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}
// @end

摘完后,代碼是不是清爽了許多,好了,下一步我們就嘗試對應(yīng)下,看看重寫后的代碼是不是和Block_private.h文件中定義的結(jié)構(gòu)一樣:

1、__BlockImpl__test_block_impl_0__block_impl加起來對應(yīng)上了Block_layout。這里有個(gè)知識點(diǎn)注意下,struct并沒有改變變量在內(nèi)存的位置,所以這兩個(gè)是可以劃等號的

2、__BlockImpl__test_block_desc_0Block_descriptor_1對應(yīng)

3、Block的body被轉(zhuǎn)換成了函數(shù)指針__BlockImpl__test_block_func_0

從上面的分析,可以看到它們倆是完全可以對應(yīng)上的。另外我們也可以看到系統(tǒng)為做了一些自動(dòng)化的工作,比如為isa賦值&_NSConcreteStackBlockmyBlock(1)調(diào)用轉(zhuǎn)換為函數(shù)調(diào)用((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);;局部變量multiplier被copy到了__BlockImpl__test_block_impl_0上,函數(shù)調(diào)用時(shí)也是使用的__BlockImpl__test_block_impl_0multiplier值,而不是那個(gè)局部變量,這也說明了Block不能隨意地修改外部局部變量(當(dāng)然添加__block修飾符才可以,第三、四部分有講到原因)。

clang -rewrite-objc BlockImpl.m以MRR方式重寫,如果要想要以ARC方式,加上-fobjc-arc-fobjc-runtime=macosx-10.7,即clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m,通過-rewrite-object命令重寫下你的應(yīng)用吧,結(jié)合第四部分,你會(huì)深刻了解Block的工作方式和循環(huán)引用產(chǎn)生的原因和解決方案(第四部分已經(jīng)加粗說明)

循環(huán)引用

很多人搞不明白,ARC的循環(huán)引用是怎么引起的,總以為和MRR是一樣的,其實(shí)第四部分已經(jīng)說明過一次了,在這里單拎出來強(qiáng)調(diào)下。MRR環(huán)境下是_Block_retain_object實(shí)現(xiàn)強(qiáng)引用外部變量的,這個(gè)可以自己寫下代碼用-rewrite-objc命令重寫下,很容易理解。在ARC環(huán)境_Block_retain_object其實(shí)是個(gè)空操作,在第四部分已經(jīng)說明。ARC是通過__strong實(shí)現(xiàn)變量的持有的,下面我們寫一個(gè)循環(huán)引用的例子

@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
    self.myBlock = ^(int num) {
        NSLog(@"%@", self);
    };
    self.myBlock(1);
}
@end

使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m,得到

***
// 只貼出來最重要的部分
struct __BlockImpl__testRetainCycle_block_impl_0 {
  struct __block_impl impl;
  struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
  BlockImpl *const __strong self; // 看這里,Block通過__strong持有了self 
  __BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
***

如果禁止Block的持有,則只能通過__weak修飾self,如下

@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
    __weak __typeof(self) weakSelf = self;
    self.myBlock = ^(int num) {
        NSLog(@"%@", weakSelf);
    };
    self.myBlock(1);
}
@end

轉(zhuǎn)化為

struct __BlockImpl__testRetainCycle_block_impl_0 {
  struct __block_impl impl;
  struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
  BlockImpl *const __weak weakSelf; // 看這里,__weak修飾,Block不再強(qiáng)持有self
  __BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

所以ARC環(huán)境下__weak可以解決循環(huán)引用

小結(jié)

從Objective-C的發(fā)展史引出了主題Block。在講Block前,先熟悉了閉包的概念,然后了解到Block其實(shí)就是閉包的一種實(shí)現(xiàn)。閉包的實(shí)質(zhì)就是捕獲了外部變量的函數(shù),Block要解決捕獲變量和變量內(nèi)存管理相關(guān)的問題。在使用時(shí)又用讓編譯器簡化了我們使用的成本。第四部分在講Block的內(nèi)存管理時(shí),又講到了MRR環(huán)境和ARC環(huán)境循環(huán)引用的原因和解決方案

網(wǎng)上已經(jīng)有很多篇關(guān)于Block的實(shí)現(xiàn),為什么我還要再寫一篇?有兩個(gè)原因:其實(shí)在2014年底的時(shí)候,我寫過一篇關(guān)于Block原理的文章,當(dāng)時(shí)還有自己的博客,后來博客到期了,文章也丟了;(;另外我找遍了所有網(wǎng)上的博客,發(fā)現(xiàn)千篇一律,而且并沒有說明白循環(huán)引用產(chǎn)生的原因和為什么加了__block(MRR)、__weak(ARC)就能避免循環(huán)引用。

最后感謝你花了這么長時(shí)間看我的這篇絮絮叨叨的文章:)

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

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