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

本文用來介紹 iOS 開發(fā)中 『Blocks』的底層原理帘瞭。我將通過 Blocks 由 OC 轉(zhuǎn)變的 C++ 源碼來一步步解析 Blocks 的底層原理。

通過本文您將了解到:

  1. Blocks 的實質(zhì)是什么芋绸?
  2. Block 截獲局部變量和特殊區(qū)域變量
  3. Block 的存儲區(qū)域
  4. Block 的循環(huán)引用

文中 Demo 我已放在了 Github 上廷蓉,Demo 鏈接:傳送門


1. Blocks 的實質(zhì)是什么桃犬?

在第一篇中我們講解了 Blocks 的基本使用,也知道了 Blocks 是 帶有局部變量的匿名函數(shù)形用。但是 Block 的實質(zhì)究竟是什么呢?類型解愤?變量奸笤?還是什么黑科技揭保?

要想了解 Block 的本質(zhì),就需要從 Block 對應(yīng)的 C++ 源碼來入手。

下面我們通過一步步的源碼剖析來了解 Block 的本質(zhì)椭坚。

1.1 Blocks 由 OC 轉(zhuǎn) C++ 源碼方法

  1. 在項目中添加 blocks.m 文件,并寫好 block 的相關(guān)代碼频轿。
  2. 打開『終端』耕赘,執(zhí)行 cd XXX/XXX 命令,其中 XXX/XXX 為 block.m 所在的目錄册招。
  3. 繼續(xù)執(zhí)行clang -rewrite-objc block.m
  4. 執(zhí)行完命令之后,block.m 所在目錄下就會生成一個 block.cpp 文件河质,這就是我們需要的 block 相關(guān)的 C++ 源碼。

1.2 Blocks 源碼概覽

下面我們刪除掉 block.m 其他無關(guān)的代碼乐尊,只保留 blocks 相關(guān)的代碼,可以得到如下結(jié)果痢缎。

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

    myBlock();

    return 0;
}
  • 轉(zhuǎn)換后 C++ 源碼:
/* 包含 Block 實際函數(shù)指針的結(jié)構(gòu)體 */
struct __block_impl {
    void *isa;
    int Flags;               
    int Reserved;        // 今后版本升級所需的區(qū)域大小
    void *FuncPtr;      // 函數(shù)指針
};

/* Block 結(jié)構(gòu)體 */
struct __main_block_impl_0 {
    // impl:Block 的實際函數(shù)指針,指向包含 Block 主體部分的 __main_block_func_0 結(jié)構(gòu)體
    struct __block_impl impl;
    // Desc:Desc 指針嵌洼,指向包含 Block 附加信息的 __main_block_desc_0() 結(jié)構(gòu)體
    struct __main_block_desc_0* Desc;
    // __main_block_impl_0:Block 構(gòu)造函數(shù)
    __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 主體部分結(jié)構(gòu)體 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("myBlock\n");
}

/* Block 附加信息結(jié)構(gòu)體:包含今后版本升級所需區(qū)域大小褐啡,Block 的大小*/
static struct __main_block_desc_0 {
    size_t reserved;        // 今后版本升級所需區(qū)域大小
    size_t Block_size;    // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

/* main 函數(shù) */
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;
}

下面我們一步步來拆解轉(zhuǎn)換后的源碼。

1.3 Block 結(jié)構(gòu)體

我們先來看看 __main_block_impl_0 結(jié)構(gòu)體( Block 結(jié)構(gòu)體)

/* Block 結(jié)構(gòu)體 */
struct __main_block_impl_0 {
    // impl:Block 的實際函數(shù)指針萍恕,指向包含 Block 主體部分的 __main_block_func_0 結(jié)構(gòu)體
    struct __block_impl impl;
    // Desc:Desc 指針,指向包含 Block 附加信息的 __main_block_desc_0() 結(jié)構(gòu)體
    struct __main_block_desc_0* Desc;
    // __main_block_impl_0:Block 構(gòu)造函數(shù)
    __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 結(jié)構(gòu)體(Block 結(jié)構(gòu)體)包含了三個部分:

  1. 成員變量 impl;
  2. 成員變量 Desc 指針;
  3. __main_block_impl_0 構(gòu)造函數(shù)允粤。

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

1.3.1 struct __block_impl impl 說明

第一部分 impl__block_impl 結(jié)構(gòu)體類型的成員變量翼岁。__block_impl 包含了 Block 實際函數(shù)指針 FuncPtr类垫,FuncPtr 指針指向 Block 的主體部分,也就是 Block 對應(yīng) OC 代碼中的 ^{ printf("myBlock\n"); }; 部分悉患。還包含了標(biāo)志位 Flags陪捷,今后版本升級所需的區(qū)域大小 Reserved烁涌,__block_impl 結(jié)構(gòu)體的實例指針 isa抒钱。

/* 包含 Block 實際函數(shù)指針的結(jié)構(gòu)體 */
struct __block_impl {
    void *isa;               // 用于保存 Block 結(jié)構(gòu)體的實例指針
    int Flags;               // 標(biāo)志位
    int Reserved;        // 今后版本升級所需的區(qū)域大小
    void *FuncPtr;      // 函數(shù)指針
};

1.3.2 struct __main_block_desc_0* Desc 說明

第二部分 Desc 是指向的是 __main_block_desc_0 類型的結(jié)構(gòu)體的指針型成員變量,__main_block_desc_0 結(jié)構(gòu)體用來描述該 Block 的相關(guān)附加信息:

  1. 今后版本升級所需區(qū)域大小: reserved 變量。
  2. Block 大小:Block_size 變量习柠。
/* Block 附加信息結(jié)構(gòu)體:包含今后版本升級所需區(qū)域大小溶锭,Block 的大小*/
static struct __main_block_desc_0 {
    size_t reserved;      // 今后版本升級所需區(qū)域大小
    size_t Block_size;  // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

1.3.3 __main_block_impl_0 構(gòu)造函數(shù)說明

第三部分是 __main_block_impl_0 結(jié)構(gòu)體(Block 結(jié)構(gòu)體) 的構(gòu)造函數(shù),負(fù)責(zé)初始化 __main_block_impl_0 結(jié)構(gòu)體(Block 結(jié)構(gòu)體) 的成員變量欺栗。

__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;
}

關(guān)于結(jié)構(gòu)體構(gòu)造函數(shù)中對各個成員變量的賦值臊泰,我們需要先來看看 main() 函數(shù)中厂抽,對該構(gòu)造函數(shù)的調(diào)用藐守。

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

我們可以把上面的代碼稍微轉(zhuǎn)換一下,去掉不同類型之間的轉(zhuǎn)換融柬,使之簡潔一點:

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 構(gòu)造函數(shù),生成的 __main_block_impl_0 結(jié)構(gòu)體(Block 結(jié)構(gòu)體)類型實例的指針官地,賦值給 __main_block_impl_0 結(jié)構(gòu)體(Block 結(jié)構(gòu)體)類型的指針變量 myBlock

可以看到, 調(diào)用 __main_block_impl_0 構(gòu)造函數(shù)的時候旷余,傳入了兩個參數(shù)。

  1. 第一個參數(shù):__main_block_func_0窘行。

    • 其實就是 Block 對應(yīng)的主體部分,可以看到下面關(guān)于 __main_block_func_0 結(jié)構(gòu)體的定義 ,和 OC 代碼中 ^{ printf("myBlock\n"); }; 部分具有相同的表達(dá)式冠息。
    • 這里參數(shù)中的 __cself 是指向 Block 的值的指針變量,相當(dāng)于 OC 中的 self
    /* Block 主體部分結(jié)構(gòu)體 */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("myBlock\n");
    }
    
  2. 第二個參數(shù):__main_block_desc_0_DATA__main_block_desc_0_DATA 包含該 Block 的相關(guān)信息铆遭。
    我們再來結(jié)合之前的 __main_block_impl_0 結(jié)構(gòu)體定義橄妆。

    • __main_block_impl_0 結(jié)構(gòu)體(Block 結(jié)構(gòu)體)可以表述為:
    struct __main_block_impl_0 {
        void *isa;               // 用于保存 Block 結(jié)構(gòu)體的實例指針
        int Flags;               // 標(biāo)志位
        int Reserved;        // 今后版本升級所需的區(qū)域大小
        void *FuncPtr;      // 函數(shù)指針
        struct __main_block_desc_0* Desc;      // Desc:Desc 指針
    };
    
    • __main_block_impl_0 構(gòu)造函數(shù)可以表述為:
    impl.isa = &_NSConcreteStackBlock;    // isa 保存 Block 結(jié)構(gòu)體實例
    impl.Flags = 0;        // 標(biāo)志位賦值
    impl.FuncPtr = __main_block_func_0;    // FuncPtr 保存 Block 結(jié)構(gòu)體的主體部分
    Desc = &__main_block_desc_0_DATA;    // Desc 保存 Block 結(jié)構(gòu)體的附加信息
    

1.4 Block 實質(zhì)總結(jié)

至此,Block 的實質(zhì)就要真相大白了笋籽。

__main_block_impl_0 結(jié)構(gòu)體(Block 結(jié)構(gòu)體)相當(dāng)于 Objective-C 類對象的結(jié)構(gòu)體,isa 指針保存的是所屬類的結(jié)構(gòu)體的實例的指針咧栗。_NSConcreteStackBlock 相當(dāng)于 Block 的結(jié)構(gòu)體實例。對象 impl.isa = &_NSConcreteStackBlock; 語句中根欧,將 Block 結(jié)構(gòu)體的指針賦值給其成員變量 isa,相當(dāng)于 Block 結(jié)構(gòu)體的成員變量 保存了 Block 結(jié)構(gòu)體的指針,這里和 Objective-C 中的對象處理方式是一致的燥筷。

也就是說明: Block 的實質(zhì)就是對象拨扶。
Block 跟其他所有的 NSObject 一樣,都是對象鼻百。果不其然讯检,萬物皆對象,古人誠不欺我冯遂。


2. Block 截獲局部變量和特殊區(qū)域變量

2.1 Blcok 截獲局部變量的實質(zhì)

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

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

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

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

    a = 20;
    b = 30;

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

從中可以看到,我們在第一次調(diào)用 myLocalBlock(); 之后已經(jīng)重新給變量 a向胡、變量 b 賦值了荷辕,但是第二次調(diào)用 myLocalBlock(); 的時候惫谤,使用的還是之前對應(yīng)變量的值。

這是因為 Block 語法的表達(dá)式使用的是它之前聲明的局部變量 a动壤、變量 b筛严。Blocks 中,Block 表達(dá)式截獲所使用的局部變量的值笑旺,保存了該變量的瞬時值熊经。所以在第二次執(zhí)行 Block 表達(dá)式時喜每,即使已經(jīng)改變了局部變量 ab 的值,也不會影響 Block 表達(dá)式在執(zhí)行時所保存的局部變量的瞬時值咨演。
這就是 Blocks 變量截獲局部變量值的特性。

可是锌畸,為什么 Blocks 變量使用的是局部變量的瞬時值粘咖,而不是局部變量的當(dāng)前值呢?

我們來看一下對應(yīng)的 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 結(jié)構(gòu)體(Block 結(jié)構(gòu)體)中多了兩個成員變量 ab,這兩個變量就是 Block 截獲的局部變量焰雕。 ab 的值來自與 __main_block_impl_0 構(gòu)造函數(shù)中傳入的值。

    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 主體部分的結(jié)構(gòu)體)中氛悬,變量 ab 的值使用的 __cself 獲取的值航夺。
    __cself->a尸闸、__cself->b 是通過值傳遞的方式傳入進(jìn)來的彻亲,而不是通過指針傳遞。這也就說明了 ab 只是 Block 內(nèi)部的變量,改變 Block 外部的局部變量值帝牡,并不能改變 Block 內(nèi)部的變量值陈辱。

    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);
    }
    

那么來總結(jié)一下:

在定義 Block 表達(dá)式的時候,局部變量使用『值傳遞』的方式傳入 Block 結(jié)構(gòu)體中膜蠢,并保存為 Block 的成員變量。

而當(dāng)外部局部變量發(fā)生變化的時候,Block 結(jié)構(gòu)體內(nèi)部對應(yīng)的的成員變量的值并沒有發(fā)生改變抡砂,所以無論調(diào)用幾次,Block 表達(dá)式結(jié)果都沒有發(fā)生改變恬涧。

如果在 Block 主體部分對外部局部變量進(jìn)行修改呢注益?類似下面這樣,是不是就可以將截獲的外部局部變量修改了溯捆?

int a = 10, b = 20;

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

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

myLocalBlock();   

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

這種方式也走不通提揍。

由此我們暫時可以得出一個結(jié)論:

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

可是,憑啥不能改變碳锈?如果我們非要改變呢顽冶,該咋整?

有一個辦法售碳,可以通過 __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);    // 輸出結(jié)果:a = 20, b = 30
    };

    myLocalBlock();
}

從中我們可以發(fā)現(xiàn):通過 __block 修飾的局部變量,可以在 Block 的主體部分中改變值间景。

我們來轉(zhuǎn)換下源碼佃声,分析一下:

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 結(jié)構(gòu)體中可以看到: 原 OC 代碼中,被 __block 修飾的局部變量 __block int a封拧、__block int b 分別變成了 __Block_byref_a_0志鹃、__Block_byref_b_1 類型的結(jié)構(gòu)體指針 a、結(jié)構(gòu)體指針 b泽西。這里使用結(jié)構(gòu)體指針 a 曹铃、結(jié)構(gòu)體指針 b 說明 _Block_byref_a_0__Block_byref_b_1 類型的結(jié)構(gòu)體并不在 __main_block_impl_0 結(jié)構(gòu)體中捧杉,而只是通過指針的形式引用陕见,這是為了可以在多個不同的 Block 中使用 __block 修飾的變量。

__Block_byref_a_0味抖、__Block_byref_b_1 類型的結(jié)構(gòu)體聲明如下:


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 結(jié)構(gòu)體定義來說明评甜,__Block_byref_a_0 有 5 個部分:

  1. __isa:標(biāo)識對象類的 isa 實例變量
  2. __forwarding:傳入變量的地址
  3. __flags:標(biāo)志位
  4. __size:結(jié)構(gòu)體大小
  5. a:存放實變量 a 實際的值,相當(dāng)于原局部變量的成員變量(和之前不加__block修飾符的時候一致)仔涩。

再來看一下 main() 函數(shù)中忍坷,__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 為結(jié)構(gòu)體的大小零聚,a 賦值為 10。下圖用來說明 __forwarding 指針的指向情況些侍。

這下隶症,我們知道 __forwarding 其實就是局部變量 a 本身的地址,那么我們就可以通過 __forwarding 指針來訪問局部變量岗宣,同時也能對其進(jìn)行修改了蚂会。

來看一下 Block 主體部分對應(yīng)的 __main_block_func_0 結(jié)構(gòu)體來驗證一下。

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 更改特殊區(qū)域變量值

除了通過 __block 說明符修飾的這種方式修改局部變量的值之外,還有一些特殊區(qū)域的變量彪见,我們也可以在 Block 的內(nèi)部將其修改儡司。

這些特殊區(qū)域的變量包括:靜態(tài)局部變量靜態(tài)全局變量余指、全局變量捕犬。

我們還是通過 OC 代碼和 C++ 源碼來說明一下:

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

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

    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 結(jié)構(gòu)體中,將靜態(tài)局部變量 static_val 以指針的形式添加為成員變量酵镜,而靜態(tài)全局變量 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 主體部分對應(yīng)的 __main_block_func_0 結(jié)構(gòu)體部分淮韭。靜態(tài)全局變量 static_global_val垢粮、全局變量 global_val 是直接訪問的,而靜態(tài)局部變量 static_val 則是通過『指針傳遞』的方式進(jìn)行訪問和賦值缸濒。

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 的存儲區(qū)域

通過之前對 Block 本質(zhì)的探索足丢,我們知道了 Block 的本質(zhì)是 Objective-C 對象。通過上述代碼中 impl.isa = &_NSConcreteStackBlock;庇配,可以知道該 Block 的類名為 NSConcreteStackBlock斩跌,根據(jù)名稱可以看出,該 Block 是存于棧區(qū)中的捞慌。而與之相關(guān)的耀鸦,還有 _NSConcreteGlobalBlock_NSConcreteMallocBlock啸澡。

3.1 _NSConcreteGlobalBlock

在以下兩種情況下使用 Block 的時候袖订,Block 為 NSConcreteGlobalBlock 類對象。

  1. 記述全局變量的地方嗅虏,使用 Block 語法時洛姑;
  2. Block 語法的表達(dá)式中沒有截獲的自動變量時。

NSConcreteGlobalBlock 類的 Block 存儲在『程序的數(shù)據(jù)區(qū)域』皮服。因為存放在程序的數(shù)據(jù)區(qū)域楞艾,所以即使在變量的作用域外,也可以通過指針安全的使用龄广。

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

int main() {
    myGlobalBlock();

    return 0;
}

通過對應(yīng) C++ 源碼,我們可以發(fā)現(xiàn):Block 結(jié)構(gòu)體的成員變量 isa 賦值為:impl.isa = &_NSConcreteGlobalBlock;择同,說明該 Block 為 NSConcreteGlobalBlock 類對象两入。

3.2 _NSConcreteStackBlock

除了 3.1 _NSConcreteGlobalBlock 中提到的兩種情形,其他情形下創(chuàng)建的 Block 都是 NSConcreteStackBlock 對象敲才,平常接觸的 Block 大多屬于 NSConcreteStackBlock 對象裹纳。

NSConcreteStackBlock 類的 Block 存儲在『棧區(qū)』的择葡。如果其所屬的變量作用域結(jié)束,則該 Block 就會被廢棄痊夭。如果 Block 使用了 __block 變量刁岸,則當(dāng) __block 變量的作用域結(jié)束,則 __block 變量同樣被廢棄她我。

3.3 _NSConcreteMallocBlock

為了解決棧區(qū)上的 Block 在變量作用域結(jié)束被廢棄這一問題虹曙,Block 提供了 『復(fù)制』 功能》撸可以將 Block 對象和 __block 變量從棧區(qū)復(fù)制到堆區(qū)上酝碳。當(dāng) Block 從棧區(qū)復(fù)制到堆區(qū)后,即使棧區(qū)上的變量作用域結(jié)束時恨狈,堆區(qū)上的 Block 和 __block 變量仍然可以繼續(xù)存在疏哗,也可以繼續(xù)使用。

此時禾怠,『堆區(qū)』上的 Block 為 NSConcreteMallocBlock 對象返奉,Block 結(jié)構(gòu)體的成員變量 isa 賦值為:impl.isa = &_NSConcreteMallocBlock;

那么,什么時候才會將 Block 從棧區(qū)復(fù)制到堆區(qū)呢吗氏?

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

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

3.4.1 Block 的自動拷貝

在使用 ARC 時,大多數(shù)情形下編譯器會自動進(jìn)行判斷弦讽,自動生成將 Block 從棧上復(fù)制到堆上的代碼:

  1. 將 Block 作為函數(shù)返回值返回時污尉,會自動拷貝;
  2. 向方法或函數(shù)的參數(shù)中傳遞 Block 時往产,使用以下兩種方法的情況下被碗,會進(jìn)行自動拷貝,否則就需要手動拷貝:
    1. Cocoa 框架的方法且方法名中含有 usingBlock 等時仿村;
    2. Grand Central Dispatch(GCD) 的 API锐朴。

3.4.2 Block 的手動拷貝

我們可以通過『copy 實例方法(即 alloc / new / copy / mutableCopy)』來對 Block 進(jìn)行手動拷貝。當(dāng)我們不確定 Block 是否會被遺棄蔼囊,需不需要拷貝的時候包颁,直接使用 copy 實例方法即可,不會引起任何的問題压真。

關(guān)于 Block 不同類的拷貝效果總結(jié)如下:

Block 類 存儲區(qū)域 拷貝效果
_NSConcreteStackBlock 棧區(qū) 從棧拷貝到堆
_NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)域 不做改變
_NSConcreteMallocBlock 堆區(qū) 引用計數(shù)增加

3.5 __block 變量的拷貝

在使用 __block 變量的 Block 從棧復(fù)制到堆上時蘑险,__block 變量也會受到如下影響:

__block 變量的配置存儲區(qū)域 Block 從棧復(fù)制到堆時的影響
堆區(qū) 從棧復(fù)制到堆滴肿,并被 Block 所持有
棧區(qū) 被 Block 所持有

當(dāng)然,如果不再有 Block 引用該 __block 變量佃迄,那么 __block 變量也會被廢除泼差。


4. Block 的循環(huán)引用

從上文 2. Block 截獲局部變量和特殊區(qū)域變量 中我們知道 Block 會對引用的局部變量進(jìn)行持有贵少。同樣,如果 Block 也會對引用的對象進(jìn)行持有(引用計數(shù) + 1)堆缘,從而會導(dǎo)致相互持有滔灶,引起循環(huán)引用。

/* —————— 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 轉(zhuǎn)換為 C++ 代碼來看一下:

節(jié)選部分 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 結(jié)構(gòu)體中增加了成員變量 person吼肥,同時 __main_block_func_0 結(jié)構(gòu)體中也使用了 __cself->person录平。

這樣就導(dǎo)致了:person 持有成員變量 myBlock blk,而 blk 也同時持有成員變量 person缀皱,就造成了循環(huán)引用問題斗这。

那么,如何來解決這個問題呢啤斗?

4.1 ARC 下表箭,通過 __weak 修飾符來消除循環(huán)引用

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

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

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

    return 0;
}

這樣就可以解決循環(huán)引用的問題。我們再來轉(zhuǎn)換為 C++ 代碼來看看崔拥。

這里需要改下轉(zhuǎn)換 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 命令來轉(zhuǎn)換。

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

使用 __weak 修飾后的 Block 示例代碼中握童,節(jié)選的部分 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 進(jìn)行弱引用。

這樣澡绩,person 持有成員變量 myBlock blk稽揭,而 blkperson 進(jìn)行弱引用,從而就消除了循環(huán)引用肥卡。

4.2 MRC 下溪掀,通過 __block 修飾符來消除循環(huán)引用

MRC 下,是不支持 __weak 修飾符的步鉴。我們可以通過 __block 來消除循環(huán)引用揪胃。

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 命令來轉(zhuǎn)換為 C++ 代碼。

使用 __block 修飾后的 Block 示例代碼中氛琢,節(jié)選的部分 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 結(jié)構(gòu)體指針阳似。這里通過指針的方式來訪問 person骚勘,而沒有對 person 進(jìn)行強引用,所以不會造成循環(huán)引用。


參考資料


至此俏讹,Blocks 相關(guān)內(nèi)容已經(jīng)全部總結(jié)完畢当宴,前前后后大概花費了差不多三周的時間。原本只是想簡單寫一下 Blocks 的基本應(yīng)用泽疆,寫著寫著就去翻了下 『Objective-C 高級編程 iOS 與OS X 多線程和內(nèi)存管理 』中關(guān)于 Block 的篇章户矢。也借鑒了大佬關(guān)于這本書中對于 Blocks 的理解。然后就有了這篇關(guān)于 Blocks 的底層原理部分殉疼。
希望大家能夠喜歡梯浪。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驱证,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恋腕,更是在濱河造成了極大的恐慌抹锄,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荠藤,死亡現(xiàn)場離奇詭異伙单,居然都是意外死亡,警方通過查閱死者的電腦和手機哈肖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門吻育,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淤井,你說我怎么就攤上這事布疼。” “怎么了币狠?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵游两,是天一觀的道長。 經(jīng)常有香客問我漩绵,道長贱案,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任止吐,我火速辦了婚禮宝踪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碍扔。我一直安慰自己瘩燥,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布不同。 她就那樣靜靜地躺著厉膀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上站蝠,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音卓鹿,去河邊找鬼菱魔。 笑死,一個胖子當(dāng)著我的面吹牛吟孙,可吹牛的內(nèi)容都是我干的澜倦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼杰妓,長吁一口氣:“原來是場噩夢啊……” “哼藻治!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巷挥,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桩卵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后倍宾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雏节,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年高职,在試婚紗的時候發(fā)現(xiàn)自己被綠了钩乍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡怔锌,死狀恐怖寥粹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情埃元,我是刑警寧澤涝涤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站亚情,受9級特大地震影響妄痪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楞件,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一衫生、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧土浸,春花似錦罪针、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春墓阀,著一層夾襖步出監(jiān)牢的瞬間毡惜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工斯撮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留经伙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓勿锅,卻偏偏與公主長得像帕膜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子溢十,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355