iOS 開發:『Blocks』詳盡總結 (二)底層原理

本文首發於個人我的博客:『不羈閣』html

文章連接:bujige.net/blog/iOS-Bl…c++

文中 Demo 地址:傳送門git

本文用來介紹 iOS 開發中 『Blocks』的底層原理。我將經過 Blocks 由 OC 轉變的 C++ 源碼來一步步解析 Blocks 的底層原理。github

經過本文您將瞭解到:macos

  1. Blocks 的實質是什麼?
  2. Block 截獲局部變量和特殊區域變量
  3. Block 的存儲區域
  4. Block 的循環引用

1. Blocks 的實質是什麼?

在第一篇中咱們講解了 Blocks 的基本使用,也知道了 Blocks 是 帶有局部變量的匿名函數。可是 Block  的實質到底是什麼呢?類型?變量?仍是什麼黑科技?編程

要想了解 Block 的本質,就須要從 Block 對應的 C++ 源碼來入手。安全

下面咱們經過一步步的源碼剖析來了解 Block 的本質。bash

1.1 Blocks 由 OC 轉 C++ 源碼方法

  1. 在項目中添加 blocks.m 文件,並寫好 block 的相關代碼。
  2. 打開『終端』,執行 cd XXX/XXX 命令,其中 XXX/XXX 爲 block.m 所在的目錄。
  3. 繼續執行clang -rewrite-objc block.m
  4. 執行完命令以後,block.m 所在目錄下就會生成一個 block.cpp 文件,這就是咱們須要的 block 相關的 C++ 源碼。

1.2 Blocks 源碼概覽

下面咱們刪除掉 block.m 其餘無關的代碼,只保留 blocks 相關的代碼,能夠獲得以下結果。markdown

  • 轉換前 OC 代碼:
int main () {
    void (^myBlock)(void) = ^{
        printf("myBlock\n");
    };

    myBlock();

    return 0;
}
複製代碼
  • 轉換後 C++ 源碼:
/* 包含 Block 實際函數指針的結構體 */
struct __block_impl {
    void *isa;
    int Flags;               
    int Reserved;        // 從此版本升級所需的區域大小
    void *FuncPtr;      // 函數指針
};

/* Block 結構體 */
struct __main_block_impl_0 {
    // impl:Block 的實際函數指針,指向包含 Block 主體部分的 __main_block_func_0 結構體
    struct __block_impl impl;
    // Desc:Desc 指針,指向包含 Block 附加信息的 __main_block_desc_0() 結構體
    struct __main_block_desc_0* Desc;
    // __main_block_impl_0:Block 構造函數
    __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;
    }
};

/* Block 主體部分結構體 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("myBlock\n");
}

/* Block 附加信息結構體:包含從此版本升級所需區域大小,Block 的大小*/
static struct __main_block_desc_0 {
    size_t reserved;        // 從此版本升級所需區域大小
    size_t Block_size;    // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

/* main 函數 */
int main () {
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}
複製代碼

下面咱們一步步來拆解轉換後的源碼。多線程

1.3 Block 結構體

咱們先來看看 __main_block_impl_0 結構體( Block 結構體)

/* Block 結構體 */
struct __main_block_impl_0 {
    // impl:Block 的實際函數指針,指向包含 Block 主體部分的 __main_block_func_0 結構體
    struct __block_impl impl;
    // Desc:Desc 指針,指向包含 Block 附加信息的 __main_block_desc_0() 結構體
    struct __main_block_desc_0* Desc;
    // __main_block_impl_0:Block 構造函數
    __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;
    }
};
複製代碼

從上邊咱們能夠看出,__main_block_impl_0 結構體(Block 結構體)包含了三個部分:

  1. 成員變量 impl;
  2. 成員變量 Desc 指針;
  3. __main_block_impl_0 構造函數。

咱們先來把這幾個部分剖析一下。

1.3.1 struct __block_impl impl 說明

第一部分 impl 是 __block_impl 結構體類型的成員變量。__block_impl 包含了 Block 實際函數指針 FuncPtrFuncPtr 指針指向 Block 的主體部分,也就是 Block 對應 OC 代碼中的 ^{ printf("myBlock\n"); }; 部分。還包含了標誌位 Flags,從此版本升級所需的區域大小  Reserved__block_impl 結構體的實例指針 isa

/* 包含 Block 實際函數指針的結構體 */
struct __block_impl {
    void *isa;               // 用於保存 Block 結構體的實例指針
    int Flags;               // 標誌位
    int Reserved;        // 從此版本升級所需的區域大小
    void *FuncPtr;      // 函數指針
};
複製代碼

1.3.2 struct __main_block_desc_0* Desc 說明

第二部分 Desc 是指向的是 __main_block_desc_0 類型的結構體的指針型成員變量,__main_block_desc_0 結構體用來描述該 Block 的相關附加信息:

  1. 從此版本升級所需區域大小: reserved 變量。
  2. Block 大小:Block_size 變量。
/* Block 附加信息結構體:包含從此版本升級所需區域大小,Block 的大小*/
static struct __main_block_desc_0 {
    size_t reserved;      // 從此版本升級所需區域大小
    size_t Block_size;  // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
複製代碼

1.3.3 __main_block_impl_0 構造函數說明

第三部分是 __main_block_impl_0 結構體(Block 結構體) 的構造函數,負責初始化 __main_block_impl_0 結構體(Block 結構體) 的成員變量。

__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;
}
複製代碼

關於結構體構造函數中對各個成員變量的賦值,咱們須要先來看看 main() 函數中,對該構造函數的調用。

  void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
複製代碼

咱們能夠把上面的代碼稍微轉換一下,去掉不一樣類型之間的轉換,使之簡潔一點:

struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *myBlock = &temp;
複製代碼

這樣,就容易看懂了。該代碼將經過 __main_block_impl_0 構造函數,生成的 __main_block_impl_0 結構體(Block 結構體)類型實例的指針,賦值給 __main_block_impl_0 結構體(Block 結構體)類型的指針變量 myBlock

能夠看到, 調用 __main_block_impl_0 構造函數的時候,傳入了兩個參數。

  1. 第一個參數:__main_block_func_0。     - 其實就是 Block 對應的主體部分,能夠看到下面關於 __main_block_func_0 結構體的定義 ,和 OC 代碼中 ^{ printf("myBlock\n"); }; 部分具備相同的表達式。     - 這裏參數中的 __cself 是指向 Block 的值的指針變量,至關於 OC 中的 self
/* Block 主體部分結構體 */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("myBlock\n");
    }
複製代碼
  1. 第二個參數:__main_block_desc_0_DATA__main_block_desc_0_DATA 包含該 Block 的相關信息。 咱們再來結合以前的 __main_block_impl_0 結構體定義。

    • __main_block_impl_0 結構體(Block 結構體)能夠表述爲:
    struct __main_block_impl_0 {
            void *isa;               // 用於保存 Block 結構體的實例指針
           int Flags;               // 標誌位
           int Reserved;        // 從此版本升級所需的區域大小
           void *FuncPtr;      // 函數指針
           struct __main_block_desc_0* Desc;      // Desc:Desc 指針
       };
    複製代碼
    • __main_block_impl_0 構造函數能夠表述爲:
    impl.isa = &_NSConcreteStackBlock;    // isa 保存 Block 結構體實例
        impl.Flags = 0;        // 標誌位賦值
        impl.FuncPtr = __main_block_func_0;    // FuncPtr 保存 Block 結構體的主體部分
        Desc = &__main_block_desc_0_DATA;    // Desc 保存 Block 結構體的附加信息
    複製代碼

1.4 Block 實質總結

至此,Block 的實質就要真相大白了。

__main_block_impl_0 結構體(Block 結構體)至關於 Objective-C 類對象的結構體,isa 指針保存的是所屬類的結構體的實例的指針。_NSConcreteStackBlock 至關於 Block 的結構體實例。對象 impl.isa = &_NSConcreteStackBlock; 語句中,將 Block 結構體的指針賦值給其成員變量 isa,至關於 Block 結構體的成員變量 保存了 Block 結構體的指針,這裏和 Objective-C 中的對象處理方式是一致的。

也就是說明: Block 的實質就是對象。  Block 跟其餘全部的 NSObject 同樣,都是對象。果不其然,萬物皆對象,古人誠不欺我。


2. Block 截獲局部變量和特殊區域變量

2.1 Blcok 截獲局部變量的實質

回顧一下上篇文章講解的例子:

// 使用 Blocks 截獲局部變量值
- (void)useBlockInterceptLocalVariables {
    int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
        printf("a = %d, b = %d\n",a, b);
    };

    myLocalBlock();    // 輸出結果:a = 10, b = 20

    a = 20;
    b = 30;

    myLocalBlock();    // 輸出結果:a = 10, b = 20
}
複製代碼

從中能夠看到,咱們在第一次調用 myLocalBlock(); 以後已經從新給變量 a、變量 b 賦值了,可是第二次調用 myLocalBlock(); 的時候,使用的仍是以前對應變量的值。

這是由於 Block 語法的表達式使用的是它以前聲明的局部變量 a、變量 b。Blocks 中,Block 表達式截獲所使用的局部變量的值,保存了該變量的瞬時值。因此在第二次執行 Block 表達式時,即便已經改變了局部變量 ab 的值,也不會影響 Block 表達式在執行時所保存的局部變量的瞬時值。  這就是 Blocks 變量截獲局部變量值的特性。

但是,爲何 Blocks 變量使用的是局部變量的瞬時值,而不是局部變量的當前值呢?

咱們來看一下對應的 C++ 代碼:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;
    int b;
    __main_block_impl_0(void *fp, struct __main_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 __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    int b = __cself->b; // bound by copy

    printf("a = %d, b = %d\n",a, b);
}

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 a = 10, b = 20;

    void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, b));
    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);

    a = 20;
    b = 30;

    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);
}
複製代碼
  1. 能夠看到 __main_block_impl_0 結構體(Block 結構體)中多了兩個成員變量 ab,這兩個變量就是 Block 截獲的局部變量。 ab 的值來自與 __main_block_impl_0 構造函數中傳入的值。

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        int a;    // 增長的成員變量 a
        int b;    // 增長的成員變量 b
        __main_block_impl_0(void *fp, struct __main_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;
        }
    };
    複製代碼
  2. 還能夠看出 __main_block_func_0(保存 Block 主體部分的結構體)中,變量 ab 的值使用的 __cself 獲取的值。 而 __cself->a__cself->b 是經過值傳遞的方式傳入進來的,而不是經過指針傳遞。這也就說明了 ab 只是 Block 內部的變量,改變 Block 外部的局部變量值,並不能改變 Block 內部的變量值。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int a = __cself->a; // bound by copy
        int b = __cself->b; // bound by copy
        printf("a = %d, b = %d\n",a, b);
    }
    複製代碼

那麼來總結一下:

在定義 Block 表達式的時候,局部變量使用**『值傳遞』**的方式傳入 Block 結構體中,並保存爲 Block 的成員變量。

而當外部局部變量發生變化的時候,Block 結構體內部對應的的成員變量的值並無發生改變,因此不管調用幾回,Block 表達式結果都沒有發生改變。

若是在 Block 主體部分對外部局部變量進行修改呢?相似下面這樣,是否是就能夠將截獲的外部局部變量修改了?

int a = 10, b = 20;

void (^myLocalBlock)(void) = ^{
    a = 20;
    b = 30;

    printf("a = %d, b = %d\n",a, b);
};

myLocalBlock();   
複製代碼

很遺憾,編譯直接報錯了。

這種方式也走不通。

由此咱們暫時能夠得出一個結論:

被截獲的自動變量的值是沒法直接修改的。

但是,憑啥不能改變?若是咱們非要改變呢,該咋整?

有一個辦法,能夠經過 __block 說明符修飾局部變量。

2.2 使用 __block 說明符更改局部變量值

// 使用 __block 說明符修飾,更改局部變量值
- (void)useBlockQualifierChangeLocalVariables {
    __block int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
        a = 20;
        b = 30;

        printf("a = %d, b = %d\n",a, b);    // 輸出結果:a = 20, b = 30
    };

    myLocalBlock();
}
複製代碼

從中咱們能夠發現:經過 __block 修飾的局部變量,能夠在 Block 的主體部分中改變值。

咱們來轉換下源碼,分析一下:

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

struct __Block_byref_b_1 {
    void *__isa;
    __Block_byref_b_1 *__forwarding;
    int __flags;
    int __size;
    int b;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    __Block_byref_b_1 *b; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__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_a_0 *a = __cself->a; // bound by ref
    __Block_byref_b_1 *b = __cself->b; // bound by ref

    (a->__forwarding->a) = 20;
    (b->__forwarding->b) = 30;

    printf("a = %d, b = %d\n",(a->__forwarding->a), (b->__forwarding->b));
}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->b, 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() {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    __Block_byref_b_1 b = {(void*)0,(__Block_byref_b_1 *)&b, 0, sizeof(__Block_byref_b_1), 20};

    void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, (__Block_byref_b_1 *)&b, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);

    return 0;
}
複製代碼

能夠看到,只是加上了一個 __block,代碼量就增長了不少。

咱們從 __main_block_impl_0 開始提及:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    __Block_byref_b_1 *b; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
複製代碼

咱們在 __main_block_impl_0 結構體中能夠看到: 原 OC 代碼中,被 __block 修飾的局部變量 __block int a__block int b 分別變成了 __Block_byref_a_0__Block_byref_b_1 類型的結構體指針 a、結構體指針 b。這裏使用結構體指針 a 、結構體指針 b 說明 _Block_byref_a_0__Block_byref_b_1 類型的結構體並不在 __main_block_impl_0 結構體中,而只是經過指針的形式引用,這是爲了能夠在多個不一樣的 Block 中使用 __block 修飾的變量。

__Block_byref_a_0__Block_byref_b_1 類型的結構體聲明以下:

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

struct __Block_byref_b_1 {
    void *__isa;
    __Block_byref_b_1 *__forwarding;
    int __flags;
    int __size;
    int b;
};
複製代碼

拿第一個 __Block_byref_a_0 結構體定義來講明,__Block_byref_a_0 有 5 個部分:

  1. __isa:標識對象類的 isa 實例變量
  2. __forwarding:傳入變量的地址
  3. __flags:標誌位
  4. __size:結構體大小
  5. a:存放實變量 a 實際的值,至關於原局部變量的成員變量(和以前不加__block修飾符的時候一致)。

再來看一下 main() 函數中,__block int a__block int b 的賦值狀況。

順便把代碼整理一下,使之簡易一點:

__Block_byref_a_0 a = {
    (void*)0,
    (__Block_byref_a_0 *)&a, 
    0, 
    sizeof(__Block_byref_a_0), 
    10
};

__Block_byref_b_1 b = {
    0,
    &b, 
    0, 
    sizeof(__Block_byref_b_1), 
    20
};
複製代碼

仍是拿第一個__Block_byref_a_0 a 的賦值來講明。

能夠看到 __isa 指針值傳空,__forwarding 指向了局部變量 a 自己的地址,__flags 分配了 0,__size 爲結構體的大小,a 賦值爲 10。下圖用來講明 __forwarding 指針的指向狀況。

這下,咱們知道 __forwarding 其實就是局部變量 a 自己的地址,那麼咱們就能夠經過 __forwarding 指針來訪問局部變量,同時也能對其進行修改了。

來看一下 Block 主體部分對應的 __main_block_func_0 結構體來驗證一下。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    __Block_byref_b_1 *b = __cself->b; // bound by ref

    (a->__forwarding->a) = 20;
    (b->__forwarding->b) = 30;

    printf("a = %d, b = %d\n",(a->__forwarding->a), (b->__forwarding->b));
}
複製代碼

能夠看到 (a->__forwarding->a) = 20;(b->__forwarding->b) = 30; 是經過指針取值的方式來改變了局部變量的值。這也就解釋了經過 __block 來修飾的變量,在 Block 的主體部分中改變值的原理實際上是:經過**『指針傳遞』**的方式。

2.3 更改特殊區域變量值

除了經過 __block 說明符修飾的這種方式修改局部變量的值以外,還有一些特殊區域的變量,咱們也能夠在 Block 的內部將其修改。

這些特殊區域的變量包括:靜態局部變量靜態全局變量全局變量

咱們仍是經過 OC 代碼和 C++ 源碼來講明一下:

  • OC 代碼:
int global_val = 10; // 全局變量
static int static_global_val = 20; // 靜態全局變量

int main() {
    static int static_val = 30; // 靜態局部變量

    void (^myLocalBlock)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;

        printf("static_val = %d, static_global_val = %d, global_val = %d\n",static_val, static_global_val, static_val);
    };

    myLocalBlock();

    return 0;
}

複製代碼
  • C++ 代碼:
int global_val = 10;
static int static_global_val = 20;

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int *static_val;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_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_val = __cself->static_val; // bound by copy
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *= 3;

    printf("static_val = %d, static_global_val = %d, global_val = %d\n",(*static_val), static_global_val, (*static_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() {
    static int static_val = 30;

    void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);

    return 0;

}

複製代碼

從中能夠看到:

在 __main_block_impl_0 結構體中,將靜態局部變量 static_val 以指針的形式添加爲成員變量,而靜態全局變量 static_global_val、全局變量 global_val 並無添加爲成員變量。

int global_val = 10;
static int static_global_val = 20;

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int *static_val;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
複製代碼

再來看一下 Block 主體部分對應的 __main_block_func_0 結構體部分。靜態全局變量 static_global_val、全局變量 global_val 是直接訪問的,而靜態局部變量 static_val 則是經過『指針傳遞』的方式進行訪問和賦值。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static_val = __cself->static_val; // bound by copy
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *= 3;

    printf("static_val = %d, static_global_val = %d, global_val = %d\n",(*static_val), static_global_val, (*static_val));
}
複製代碼

3. Block 的存儲區域

經過以前對 Block 本質的探索,咱們知道了 Block 的本質是 Objective-C 對象。經過上述代碼中 impl.isa = &_NSConcreteStackBlock;,能夠知道該 Block 的類名爲 NSConcreteStackBlock,根據名稱能夠看出,該 Block 是存於棧區中的。而與之相關的,還有 _NSConcreteGlobalBlock_NSConcreteMallocBlock

3.1 _NSConcreteGlobalBlock

在如下兩種狀況下使用 Block 的時候,Block 爲 NSConcreteGlobalBlock 類對象。

  1. 記述全局變量的地方,使用 Block 語法時;
  2. Block 語法的表達式中沒有截獲的自動變量時。

NSConcreteGlobalBlock 類的 Block 存儲在**『程序的數據區域』**。由於存放在程序的數據區域,因此即便在變量的做用域外,也能夠經過指針安全的使用。

  • 記述全局變量的地方,使用 Block 語法示例代碼:
void (^myGlobalBlock)(void) = ^{
    printf("GlobalBlock\n");
};

int main() {
    myGlobalBlock();

    return 0;
}
複製代碼

經過對應 C++ 源碼,咱們能夠發現:Block 結構體的成員變量 isa 賦值爲:impl.isa = &_NSConcreteGlobalBlock;,說明該 Block 爲 NSConcreteGlobalBlock 類對象。

3.2 _NSConcreteStackBlock

除了 3.1 _NSConcreteGlobalBlock 中提到的兩種情形,其餘情形下建立的 Block 都是 NSConcreteStackBlock 對象,日常接觸的 Block 大多屬於 NSConcreteStackBlock 對象。

NSConcreteStackBlock 類的 Block 存儲在『棧區』的。若是其所屬的變量做用域結束,則該 Block 就會被廢棄。若是 Block 使用了 __block 變量,則當 __block 變量的做用域結束,則 __block 變量一樣被廢棄。

3.3 _NSConcreteMallocBlock

爲了解決棧區上的 Block 在變量做用域結束被廢棄這一問題,Block 提供了 『複製』 功能。能夠將 Block 對象和 __block 變量從棧區複製到堆區上。當 Block 從棧區複製到堆區後,即便棧區上的變量做用域結束時,堆區上的 Block 和 __block 變量仍然能夠繼續存在,也能夠繼續使用。

此時,『堆區』上的 Block 爲 NSConcreteMallocBlock 對象,Block 結構體的成員變量 isa 賦值爲:impl.isa = &_NSConcreteMallocBlock;

那麼,何時纔會將 Block 從棧區複製到堆區呢?

這就涉及到了 Block 的自動拷貝和手動拷貝。

3.4 Block 的自動拷貝和手動拷貝

3.4.1 Block 的自動拷貝

在使用 ARC 時,大多數情形下編譯器會自動進行判斷,自動生成將 Block 從棧上覆制到堆上的代碼:

  1. 將 Block 做爲函數返回值返回時,會自動拷貝;
  2. 向方法或函數的參數中傳遞 Block 時,使用如下兩種方法的狀況下,會進行自動拷貝,不然就須要手動拷貝:     1. Cocoa 框架的方法且方法名中含有 usingBlock 等時;
        2. Grand Central Dispatch(GCD) 的 API。

3.4.2 Block 的手動拷貝

咱們能夠經過『copy 實例方法(即 alloc / new / copy / mutableCopy)』來對 Block 進行手動拷貝。當咱們不肯定 Block 是否會被遺棄,需不須要拷貝的時候,直接使用 copy 實例方法便可,不會引發任何的問題。

關於 Block 不一樣類的拷貝效果總結以下:

Block 類 存儲區域 拷貝效果
_NSConcreteStackBlock 棧區 從棧拷貝到堆
_NSConcreteGlobalBlock 程序的數據區域 不作改變
_NSConcreteMallocBlock 堆區 引用計數增長

3.5 __block 變量的拷貝

在使用 __block 變量的 Block 從棧複製到堆上時,__block 變量也會受到以下影響:

__block 變量的配置存儲區域 Block 從棧複製到堆時的影響
堆區 從棧複製到堆,並被 Block 所持有
棧區 被 Block 所持有

固然,若是再也不有 Block 引用該 __block 變量,那麼 __block 變量也會被廢除。


4. Block 的循環引用

從上文 2. Block 截獲局部變量和特殊區域變量 中咱們知道 Block 會對引用的局部變量進行持有。一樣,若是 Block 也會對引用的對象進行持有(引用計數 + 1),從而會致使相互持有,引發循環引用。

/* —————— retainCycleBlcok.m —————— */   
#import <Foundation/Foundation.h>
#import "Person.h"

int main() {
    Person *person = [[Person alloc] init];
    person.blk = ^{
        NSLog(@"%@",person);
    };

    return 0;
}


/* —————— Person.h —————— */ 
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

@interface Person : NSObject
@property (nonatomic, copy) myBlock blk;
@end


/* —————— Person.m —————— */ 
#import "Person.h"

@implementation Person    

@end
複製代碼

咱們將 retainCycleBlcok.m 轉換爲 C++ 代碼來看一下:

節選部分 C++ 代碼:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    Person *person;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *person = __cself->person; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_8957e0_mi_0,person);
}
複製代碼

能夠看到 __main_block_impl_0 結構體中增長了成員變量 person,同時 __main_block_func_0 結構體中也使用了 __cself->person

這樣就致使了:person 持有成員變量 myBlock blk,而 blk 也同時持有成員變量 person,就形成了循環引用問題。

那麼,如何來解決這個問題呢?

4.1 ARC 下,經過 __weak 修飾符來消除循環引用

在 ARC 下,可聲明附有 __weak 修飾符的變量,並將對象賦值使用。

int main() {
    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person;

    person.blk = ^{
        NSLog(@"%@",weakPerson);
    };

    return 0;
}
複製代碼

這樣就能夠解決循環引用的問題。咱們再來轉換爲 C++ 代碼來看看。

這裏須要改下轉換 C++ 指令,由於使用原指令會報錯:error: cannot create __weak reference because the current deployment target does not support weak references

這裏須要使用 clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations retainCycleBlcok.m 命令來轉換。

參考連接:How to use __weak reference in clang?

使用 __weak 修飾後的 Block 示例代碼中,節選的部分 C++ 代碼:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    Person *__weak weakPerson;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *__weak weakPerson = __cself->weakPerson; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_447367_mi_0,weakPerson);
}
複製代碼

能夠看到,__main_block_impl_0 使用過了 __weak 對成員變量 person 進行弱引用。

這樣,person 持有成員變量 myBlock blk,而 blk 對 person 進行弱引用,從而就消除了循環引用。

4.2 MRC 下,經過 __block 修飾符來消除循環引用

MRC 下,是不支持 __weak 修飾符的。咱們能夠經過 __block 來消除循環引用。

int main() {
    Person *person = [[Person alloc] init];
    __block typeof(person) blockPerson = person;

    person.blk = ^{
        NSLog(@"%@", blockPerson);
    };

    return 0;
}
複製代碼

使用 clang -rewrite-objc -fno-objc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations retainCycleBlcok.m 命令來轉換爲 C++ 代碼。

使用 __block 修飾後的 Block 示例代碼中,節選的部分 C++ 代碼:

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_blockPerson_0 *blockPerson; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__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_blockPerson_0 *blockPerson = __cself->blockPerson; // bound by ref

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_536cd4_mi_0,(blockPerson->__forwarding->blockPerson));
}
複製代碼

能夠看到,經過 __block 引用的 blockPerson,生成了 __Block_byref_blockPerson_0 結構體指針。這裏經過指針的方式來訪問 person,而沒有對 person 進行強引用,因此不會形成循環引用。


參考資料


至此,Blocks 相關內容已經所有總結完畢,前先後後大概花費了差很少三週的時間。本來只是想簡單寫一下 Blocks 的基本應用,寫着寫着就去翻了下 『Objective-C 高級編程 iOS 與OS X 多線程和內存管理 』中關於 Block 的篇章。也借鑑了大佬關於這本書中對於 Blocks 的理解。而後就有了這篇關於 Blocks 的底層原理部分。

但願你們可以喜歡。

相關文章
相關標籤/搜索