Block的本質(zhì)及變量截取

本文的內(nèi)容主要是基于Clang編譯器的官方文檔所寫。

在開始探索Block的本質(zhì)之前,大家先試著分析一下,下面的代碼會(huì)輸出什么:

void main() {
    __block int a = 13;
    int b = 13;
    NSMutableString *str = [[NSMutableString alloc] initWithString:@"Hello"];
    void(^blockTest)(void) = ^{
        NSLog(@"a = %d, b = %d, str = %@", a, b, str);
    };
    a++;
    b++;
    [str appendString:@"World"];
    blockTest();
}

如果你對(duì)輸出結(jié)果不是那么有把握的話,那么相信通過(guò)今天的這篇文章,你會(huì)有一個(gè)明確的答案(答案在文章最后)。

Clang

先說(shuō)些題外話,什么是Clang?Clang是C++編寫的編譯器。我們知道,我們平常代碼所寫的任何程序,最終都需要通過(guò)編譯器轉(zhuǎn)換成與語(yǔ)言無(wú)關(guān)的機(jī)器二進(jìn)制代碼。而Clang,則是支持/C++/Objective-C/Objective-C++的編譯器。那我們?cè)谧鯫C開發(fā)時(shí),可能也會(huì)聽說(shuō)LLVM編譯器,那么Clang和LLVM之間是什么關(guān)系呢?

它們的關(guān)系如下圖所示:


LLVM架構(gòu)

Clang是編譯器的前端,它會(huì)分析具體的編程語(yǔ)言,然后用于生成與機(jī)器無(wú)關(guān)的中間代碼。而LLVM是編譯器的后端,與具體編程語(yǔ)言無(wú)關(guān),而是會(huì)去分析統(tǒng)一的中間代碼,生成符合對(duì)應(yīng)機(jī)器的目標(biāo)程序。

這樣拆分前端后端的好處在于,前后端可以獨(dú)立的替換,便于編譯器的優(yōu)化。

關(guān)于Clang,我們了解這些就足夠了。

Block的本質(zhì)

回到Block上來(lái)。我們?cè)谑褂肂lock語(yǔ)法時(shí),總會(huì)感覺到有些奇怪:

^{
      NSLog(@"Hello");
 };

這么一個(gè)^{}是什么鬼?似乎在別的語(yǔ)言中也沒(méi)有見過(guò)這么個(gè)關(guān)鍵字定義。其實(shí),^{}對(duì)于Clang編譯器來(lái)說(shuō),僅僅是一個(gè)語(yǔ)言標(biāo)記,它會(huì)告訴Clang,這里我需要定義一個(gè)Block類型的結(jié)構(gòu)體。 而Clang發(fā)現(xiàn)這個(gè)語(yǔ)言標(biāo)記時(shí),會(huì)將^{}這么一個(gè)奇怪的定義,轉(zhuǎn)換為C語(yǔ)言中的結(jié)構(gòu)體。經(jīng)過(guò)Clang轉(zhuǎn)換后的Block,其形式是這樣的:

struct Block_literal_1 {
    // 第一部分. Block基本信息以及 invoke函數(shù)指針
    void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__ 
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    
    // 第二部分. Block descriptor指針
    struct Block_descriptor_1 {
    unsigned long int reserved;         // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        // optional helper functions
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
        // required ABI.2010.3.16
        const char *signature;                         // IFF (1<<30)
    } *descriptor;
    
    // 第三部分. Block所截取的外部變量(如果有的話)
    // imported variables
};

筆者將Block結(jié)構(gòu)體定義分成了三個(gè)部分:

  • Block基本信息以及 invoke函數(shù)指針
  • Block descriptor指針
  • Block所截取的外部變量

在這里我們得出結(jié)論:Block的本質(zhì)是一個(gè)C語(yǔ)言的struct。

Block對(duì)應(yīng)的結(jié)構(gòu)體

上面探討了Block的本質(zhì)是一個(gè)struct,接下來(lái)我們就來(lái)詳細(xì)看一下這個(gè) Block struct的定義。

Block基本信息以及Block descriptor

struct Block_literal_1 {
    // 第一部分. Block基本信息以及 invoke函數(shù)指針
    void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__ 
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    
     // 第二部分. Block descriptor指針
    struct Block_descriptor_1 {
    unsigned long int reserved;         // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        // optional helper functions
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
        // required ABI.2010.3.16
        const char *signature;                         // IFF (1<<30)
    } *descriptor;
    ...
};

我們先來(lái)看Block struct的第一部分和第二部分。至于Block的第三部分,外部變量的截取,我們會(huì)在下面單獨(dú)的章節(jié)進(jìn)行討論。
當(dāng)我們聲明一個(gè)Block時(shí),對(duì)應(yīng)的Block struct會(huì)被如下初始化:

  1. 系統(tǒng)會(huì)聲明并初始化一個(gè)Block descriptor結(jié)構(gòu)體。初始化Block descriptor步驟如下
    a. Block descriptor 的size部分會(huì)被設(shè)置為Block結(jié)構(gòu)體的大小
    b. copy_helper 和 dispose_helper函數(shù)指針會(huì)被設(shè)置為對(duì)應(yīng)的函數(shù)指針(如果需要這兩個(gè)helper 函數(shù)的話)

  2. 系統(tǒng)初始化Block 結(jié)構(gòu)體。 初始化Block 結(jié)構(gòu)體的步驟如下:
    a. isa 部分會(huì)被設(shè)置為__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__ 所對(duì)應(yīng)的地址。注意這里是地址,而不是NSMallocBlock這些變量。
    b. flags 會(huì)被置為對(duì)應(yīng)的flag數(shù)值。比如,如果Block struct需要copy,dispose helper函數(shù)時(shí),響應(yīng)的flag會(huì)被置位。同時(shí),flags還有標(biāo)志Block ABI 版本的功能。
    c. 設(shè)置invoke函數(shù)指針指向?qū)?yīng)的函數(shù)。該函數(shù)的第一個(gè)參數(shù)是Block struct本身的指針,而其余的參數(shù)則是Block執(zhí)行時(shí)外部要傳入的參數(shù)(如果有的話)

舉個(gè)例子,對(duì)于下面的Block:

^ { printf("hello world\n"); }

Clang會(huì)創(chuàng)建如下內(nèi)容:

struct __block_literal_1 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_1 *);
    struct __block_descriptor_1 *descriptor;
};

void __block_invoke_1(struct __block_literal_1 *_block) {
    printf("hello world\n");
}

static struct __block_descriptor_1 {
    unsigned long int reserved;
    unsigned long int Block_size;
} __block_descriptor_1 = { 0, sizeof(struct __block_literal_1) };

那么Block struct將會(huì)如下被初始化:

struct __block_literal_1 _block_literal = {
     &__NSGlobalBlock__,
     (1<<29), <uninitialized>,
     __block_invoke_1,
     &__block_descriptor_1
};

這是Clang文檔給出的官方例子,但是我們這里不要去糾結(jié)flags究竟是設(shè)置的什么,因?yàn)楦鶕?jù)本人的測(cè)試,其flags的值并不是1<<29。

這里有個(gè)問(wèn)題,就是什么時(shí)候isa會(huì)被設(shè)為&__NSGlobalBlock__/&__NSMallocBlock__/&__NSStackBlock__ 呢?

  • 當(dāng)Block中沒(méi)有引用外部變量,或引用了全局變量,const 標(biāo)量或static變量時(shí),Block的isa會(huì)被設(shè)置為&__NSGlobalBlock__。 這時(shí)的Block生命周期是伴隨程序始終的。
  • &__NSStackBlock__ 表示這個(gè)block, 是在棧上面分配的,出了棧就會(huì)消亡。使用了外部棧變量,就會(huì)是__NSStackBlock__ 類型。
  • &__NSMallocBlock__ 表示Block復(fù)制到堆上面了,可以存儲(chǔ)下來(lái),以后使用。當(dāng)Block引用了外部的OC對(duì)象,Block對(duì)象或用__block修飾的變量時(shí),Block會(huì)被設(shè)置為&__NSMallocBlock__ 類型。這里有一點(diǎn)要注意,在ARC的情況下。只要將block賦值給變量,就自動(dòng)幫你復(fù)制了。也就是說(shuō),如果將一個(gè)棧上的block賦值給另一個(gè)block變量,則被賦值的block變量類型是 &__NSMallocBlock__ 類型。

如下面代碼:

    int a = 13;
    NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
    blockType1 blk2 = ^{
        NSLog(@"%d", a);
    };
    NSLog(@"block type is %@", NSStringFromClass([blk2 class]));

輸出為:


image

而對(duì)于const類型的引用,

    const int a = 13;  // 這里是const引用
    NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
    blockType1 blk2 = ^{
        NSLog(@"%d", a);
    };
    NSLog(@"block type is %@", NSStringFromClass([blk2 class]));

輸出為:


image

這是因?yàn)閷?duì)于Global,不必需要再在堆上開辟一塊內(nèi)存。

Block的外部變量截取

理解Block的關(guān)鍵,在于理解Block是如何處理外部變量的。

我們先來(lái)想一想,Block中會(huì)截取那些類型的外部變量:

  • 全局/靜態(tài)變量
  • 自動(dòng)(auto)存儲(chǔ)類型
  • Block類型
  • NSObject類型
  • __block修飾的變量

截取全局/靜態(tài)類型變量

對(duì)于全局/靜態(tài)變量,Block會(huì)直接引用這類變量,不會(huì)copy。 例如,

static int a = 13;
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"Outside Block, static int a address is %p", &a);
   ^{
        NSLog(@"Inside Block, static int a address is %p", &a);
    }();
   
}

輸出為:


image

在Block 外和Block內(nèi),static int a的地址是一樣的,Block并沒(méi)有做特殊的處理。

截取自動(dòng)存儲(chǔ)類型變量

所謂自動(dòng)存儲(chǔ)類型,指的是auto類型。我們可以理解為棧上的變量(Block類型、__block、NSObject類型除外),其內(nèi)存會(huì)有系統(tǒng)自動(dòng)釋放。

對(duì)于auto類型的變量截取,Clang文檔有如下描述:

Variables of auto storage class are imported as const copies.

也就是說(shuō),auto類型會(huì)在Block中用const copy一份。也就是說(shuō)Block內(nèi)、外是完全不同的兩個(gè)變量。

我們做個(gè)測(cè)試:

    int b = 12;
    NSLog(@"Outside Block, address of int b is %p", &b);
   ^{
        NSLog(@"Inside Block, address of int b is %p", &b);
    }();

輸出為:


image

可以看到,在Block外和Block內(nèi)部,表面上同樣的b變量,其地址是不一樣的。究其原因,就是因?yàn)樵贐lock內(nèi)部,系統(tǒng)會(huì)默默的const copy一份b。

這時(shí)候,Block的數(shù)據(jù)結(jié)構(gòu)是這樣的:

int x = 10;
void (^vv)(void) = ^{ printf("x is %d\n", x); }
x = 11;
vv();
struct __block_literal_2 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_2 *);
    struct __block_descriptor_2 *descriptor;
    const int x;   // 這里會(huì)有一份const copy
};
struct __block_literal_2 __block_literal_2 = {
      &_NSConcreteStackBlock,
      (1<<29), <uninitialized>,
      __block_invoke_2,
      &__block_descriptor_2,
      x
 };

一般的,對(duì)于標(biāo)量類型(int, float, bool等基本類型),struct,unions和函數(shù)指針類型,都會(huì)采用const copy的方式,將Block外部的變量拷貝到Block內(nèi)部。

這里需要注意一點(diǎn),在iOS系統(tǒng)中,當(dāng)我們把一個(gè)stack 上的Block賦值給一個(gè)Block變量時(shí):

void (^vv)(void) = ^{ printf("x is %d\n", x); }

會(huì)默認(rèn)調(diào)用Block的copy方法,即,上面實(shí)際上是如下代碼:

void (^vv)(void) = [^{ printf("x is %d\n", x); } copy];

這樣得到的vv,是一個(gè)在堆上的Block變量。這時(shí)候再輸出vv中x的地址,會(huì)得到一個(gè)堆上的地址。

因此,我們?cè)谧鰧?shí)驗(yàn)的時(shí)候,不要輸出對(duì)拷貝后的Block中變量地址,而應(yīng)該直接輸出Block中的地址:

 ^{
        NSLog(@"Inside Block, static int a address is %p", &a);
    }();

上面代碼中并沒(méi)有賦值,因此會(huì)輸出棧上的a的const copy地址。

截取Block類型變量

對(duì)于截取Block類型的變量,在Block內(nèi)部,會(huì)保留const copy其Block指針。
如下代碼:

    int a4 = 13;
    void (^existingBlock)(void) = ^{NSLog(@"Hello %d", a4);};
    NSLog(@"Outside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);
    ^{
    NSLog(@"Inside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);}();

    blockType1 blk = existingBlock;
    blk();

輸出為:


image

這里可以看到,對(duì)于Block變量,existingBlock(注意,這個(gè)existingBlock變量是一個(gè)Block指針,而不是Block本身)被const copy了一份到Block中。而對(duì)于Block指針?biāo)赶虻腂lock實(shí)體,并沒(méi)有發(fā)生改變。
也就說(shuō),在Block內(nèi)部和外部,會(huì)有兩個(gè)Block指針,指向了同一個(gè)Block結(jié)構(gòu)體。

這里再次強(qiáng)調(diào)一下,我們所聲明的Block變量existingBlock,是一個(gè)指向Block類型的指針,而不是Block實(shí)體。正如同NSObject *obj = [NSObject new]一樣,obj是一個(gè)指向NSObject的指針,而不是NSObject實(shí)體。

下面是Clang文檔的例子:

void (^existingBlock)(void) = ...;
void (^vv)(void) = ^{ existingBlock(); }
vv();

struct __block_literal_3 {
   ...; // existing block
};

struct __block_literal_4 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_4 *);
    struct __block_literal_3 *const existingBlock;  // 這里可以看到,在Block內(nèi)部,是保持了外部的Block指針
};

void __block_invoke_4(struct __block_literal_2 *_block) {
   __block->existingBlock->invoke(__block->existingBlock);
}

void __block_copy_4(struct __block_literal_4 *dst, struct __block_literal_4 *src) {
     //_Block_copy_assign(&dst->existingBlock, src->existingBlock, 0);
     _Block_object_assign(&dst->existingBlock, src->existingBlock, BLOCK_FIELD_IS_BLOCK);
}

void __block_dispose_4(struct __block_literal_4 *src) {
     // was _Block_destroy
     _Block_object_dispose(src->existingBlock, BLOCK_FIELD_IS_BLOCK);
}

static struct __block_descriptor_4 {
    unsigned long int reserved;
    unsigned long int Block_size;
    void (*copy_helper)(struct __block_literal_4 *dst, struct __block_literal_4 *src);
    void (*dispose_helper)(struct __block_literal_4 *);
} __block_descriptor_4 = {
    0,
    sizeof(struct __block_literal_4),
    __block_copy_4,
    __block_dispose_4,
};

這時(shí)候Block的數(shù)據(jù)結(jié)構(gòu)是:

struct __block_literal_4 _block_literal = {
      &_NSConcreteStackBlock,
      (1<<25)|(1<<29), <uninitialized>
      __block_invoke_4,
      & __block_descriptor_4  // 這里可以看到,在Block內(nèi)部,是保持了外部的Block指針
      existingBlock,
};

截取NSObject類型變量

在Clang中,NSObject類型變量被當(dāng)做__attribute__((NSObject))類型。Block截取NSObject對(duì)象時(shí),同樣會(huì)做一份const copy NSObject *
比如:

@interface MyObject : NSObject
- (void)sayMyObjectAddress
@end

@implementation MyObject
- (void)sayMyObjectAddress {
    NSLog(@"Instance pointer address is %p, Instance address is %p", &self, self);
}
@end

    MyObject *obj = [MyObject new];
    [obj sayMyObjectAddress];
    ^{
        [obj sayMyObjectAddress];
    }();

輸出為:


image

可以看到,當(dāng)Block對(duì)NSObject做const copy時(shí),僅是做了淺拷貝,并沒(méi)有復(fù)制指針?biāo)赶虻膬?nèi)容,僅僅是const copy了指針。因此,這里的self指針地址是改變了,而self指針?biāo)赶虻牡刂范际峭粋€(gè)。

就像上面Block類型變量的例子,是同一個(gè)道理。

而對(duì)于NSObject類型,同樣需要兩個(gè)copy helper函數(shù):

void __block_copy_foo(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
     _Block_object_assign(&dst->objectPointer, src-> objectPointer, BLOCK_FIELD_IS_OBJECT);
}

void __block_dispose_foo(struct __block_literal_5 *src) {
     _Block_object_dispose(src->objectPointer, BLOCK_FIELD_IS_OBJECT);
}

截取__block修飾的變量

鑒于我們上面所說(shuō)的都是const copy,因此對(duì)于在Block中對(duì)于其截取變量的任何改變,都是不被允許的。如果我們要修改Block內(nèi)部的值,編譯器就會(huì)提示如下錯(cuò)誤:


image

那如何在Block中修改截取變量的值呢?我們自然會(huì)想到對(duì)外部變量加上__block修飾符。我們將上面代碼改成下面的形式,則會(huì)順利編譯通過(guò):

    __block int b = 13;
    NSLog(@"Outside Block, address of __block int b is %p, b = %d", &b, b);
    blockType1 blk = ^{
        b++;
        NSLog(@"Inside Block, address of __block int b is %p, b = %d", &b, b);
    };
    blk();
    NSLog(@"After Block, address of __block int b is %p, b = %d", &b, b);

輸出為:


image

這里會(huì)發(fā)現(xiàn)一個(gè)有意思的現(xiàn)象,雖然在進(jìn)入Block前后,b的地址并不一樣!** 也就是在進(jìn)入Block前后,其實(shí)會(huì)有兩個(gè)不同的b **。

之所以會(huì)這樣,與Clang對(duì)于__block類型變量的處理有關(guān)。

當(dāng)變量被標(biāo)記為__block類型時(shí),Clang會(huì)對(duì)變量b進(jìn)行改寫成一個(gè)如下格式的struct:

struct _block_byref_foo {
    void *isa;   // 設(shè)置為NULL
    struct Block_byref *forwarding;   // Block外部變量的地址
    int flags;   //refcount;
    int size;  // size of _block_byref_foo
    typeof(marked_variable) marked_variable;  // copy of Block 外部變量
};

比如:

int __block i = 10;
i = 11;

會(huì)被Clang改寫做:

struct _block_byref_i {
  void *isa;
  struct _block_byref_i *forwarding;
  int flags;   //refcount;
  int size;
  int captured_i;
} i = { NULL, &i, 0, sizeof(struct _block_byref_i), 10 };

i.forwarding->captured_i = 11;

可以看到,int __block i 被改寫為了struct _block_byref_i 結(jié)構(gòu)體。這里需要明確一點(diǎn):
添加了__block關(guān)鍵字后的int b,實(shí)質(zhì)類型并不是int類型,而是一個(gè)struct _block的結(jié)構(gòu)體類型了。

這里有個(gè)關(guān)鍵的屬性變量,forwarding,forwarding指向一個(gè)__block結(jié)構(gòu)體。
當(dāng)__block在棧上時(shí),forwarding會(huì)指向__block自身。而當(dāng)__block在堆上生成一份copy時(shí),這時(shí)候棧上的forwarding會(huì)指向堆上的那一份拷貝。而在堆上的那個(gè)__block的__forwarding 指針,則指向自己的首地址。

也就是說(shuō),只要通過(guò)forwarding來(lái)操作__block結(jié)構(gòu)體捕獲的外部變量,實(shí)質(zhì)上是操作的同一個(gè)變量。
我們用圖片可以更清楚的弄懂其中的原理:

image

這也就是為什么,即使Block外和Block內(nèi)部b分別是兩個(gè)變量,而b的值卻可以被改變的原因。因?yàn)樵跅I系?code>__block結(jié)構(gòu)體中,通過(guò)forwarding指針指向了堆上的Block的地址。那么當(dāng)在Block內(nèi)部修改b的值,也就是改變堆上的int b的值的時(shí)候,在Block外部再訪問(wèn)b的值的時(shí)候,其實(shí)在棧上的__block int b通過(guò)__forwarding 指針,訪問(wèn)到了堆上的__block int b,這讓我們感覺在棧上的變量也被修改了。

這也就是為什么,在測(cè)試代碼中,在執(zhí)行完Block后,再輸出b的地址,發(fā)現(xiàn)是和Block內(nèi)部的地址一致,而不是進(jìn)入Block之前的地址的原因。(以為進(jìn)入Block后,再次訪問(wèn)b,實(shí)際上會(huì)指向堆上的那個(gè)b,而不是之前棧上的那個(gè)b)

當(dāng)我們將__block的變量導(dǎo)入Block中時(shí),Clang會(huì)作如下改寫:
例如,

int __block i = 2;
functioncall(^{ i = 10; });

會(huì)被Clang做如下改寫:

struct _block_byref_i {
    void *isa;  // set to NULL
    struct _block_byref_voidBlock *forwarding;
    int flags;   //refcount;
    int size;
    void (*byref_keep)(struct _block_byref_i *dst, struct _block_byref_i *src);
    void (*byref_dispose)(struct _block_byref_i *);
    int captured_i;
};


struct __block_literal_5 {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(struct __block_literal_5 *);
    struct __block_descriptor_5 *descriptor;
    struct _block_byref_i *i_holder;
};

void __block_invoke_5(struct __block_literal_5 *_block) {
    _block_byref_i * i_holder = _block->i_holder;
   i_holder->forwarding->captured_i = 10;
}

void __block_copy_5(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
     _Block_object_assign(&dst->i_holder, src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}

void __block_dispose_5(struct __block_literal_5 *src) {
     _Block_object_dispose(src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}

static struct __block_descriptor_5 {
    unsigned long int reserved;
    unsigned long int Block_size;
    void (*copy_helper)(struct __block_literal_5 *dst, struct __block_literal_5 *src);
    void (*dispose_helper)(struct __block_literal_5 *);
} __block_descriptor_5 = { 0, sizeof(struct __block_literal_5) __block_copy_5, __block_dispose_5 };

上面的數(shù)據(jù)結(jié)構(gòu)會(huì)做如下初始化

struct _block_byref_i i_holder = {( .isa=NULL, .forwarding=&i, .flags=0, .size=sizeof(struct _block_byref_i), .captured_i=2 )};
struct __block_literal_5 _block_literal = {
      &_NSConcreteStackBlock,
      (1<<25)|(1<<29), <uninitialized>,
      __block_invoke_5,
      &__block_descriptor_5,
      & i_holder,
};

是否只有__block類型才能夠在Block中被修改?

這里插入一個(gè)小測(cè)試,對(duì)于靜態(tài)變量a,是否可以在Block中作出改變呢?

static int a = 13;
- (void)viewDidLoad {
       [super viewDidLoad];
         NSLog(@"Outside Block, static int a address is %p", &a);
   ^{
        NSLog(@"Inside Block, static int a address is %p", &a);
       a++;
    }();
    NSLog(@"Now a is %d", a);
}

答案是可以的,在Block之后,a的值變?yōu)?4。這是因?yàn)閷?duì)于全局/靜態(tài)變量而言,Block會(huì)直接引用變量,而不會(huì)做const copy。

所以,我們這一節(jié)討論的,是除去全局、靜態(tài)變量外,被Block const copy的其他的類型變量。

小測(cè)試

題目一. 下面代碼會(huì)輸出什么?

typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 13;
        blockType blk = ^{
            NSLog(@"In block i = %d", i);
        };
        i += 2;
        
        blk();
        NSLog(@"Now i = %d", i);
        
    }
    return 0;
}

這里考察對(duì)于auto類型變量,Block的截取方式。因?yàn)閍uto變量會(huì)在Block中做一份const copy,因此在Block內(nèi)外,實(shí)質(zhì)上應(yīng)該存在兩個(gè)i。
這里的輸出為:

image

題目二. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash

typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       NSString *str = @"Hello";
        blockType blk = ^{
            str = @"World";
        };
        blk();
        NSLog(@"Now str is %@", str);
    }
    return 0;
}

因?yàn)閷?duì)于NSObject類型,在Block中會(huì)當(dāng)做NSObject *const obj處理,此時(shí)是一個(gè)指針常量。對(duì)于指針常量,是不能夠更改其指針?biāo)赶虻奈恢玫?,因此,這里會(huì)出現(xiàn)編譯錯(cuò)誤。

題目三. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash

typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSString *str = @"Hello";
        blockType blk = ^{
            str = @"World";
        };
        blk();
        NSLog(@"Now str is %@", str);
    }
    return 0;
}

因?yàn)閟tr變量用了__block修飾,因此__block NSString *str 實(shí)質(zhì)上一個(gè)__block struct 類型變量:

struct _block_byref_str {
        void *isa;
        struct _block_byref_str  *forwarding;
        int flags;
        int size;
        NSString *captureStr;
}

當(dāng)創(chuàng)建__block 類型變量時(shí),在Block結(jié)構(gòu)體中,會(huì)存儲(chǔ)__block結(jié)構(gòu)體指針:

struct __block_literal {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(struct __block_literal * _cself);
        struct __block_descriptor *descriptor;
         struct _block_byref_str *str_holder;    // __block結(jié)構(gòu)體指針
}

當(dāng)調(diào)用invoke方法時(shí),會(huì)是這樣的:

void invoke(struct __block_literal * _cself) {
        _block_byref_str *str_holder = _cself->str_holder;
        str_holder->forwarding->captureStr = @"World";
}

由于通過(guò)forwarding指針,確保了Block外部和內(nèi)部的str都是一個(gè)指針,因此,當(dāng)Block內(nèi)部的str指向新的地址時(shí)(str = @"World"),在Block外部的str也指向了新的地址。(因?yàn)樗鼈兪峭粋€(gè)東西)。
這個(gè)過(guò)程用圖表示為:

  1. __block str = @"World";


    image
  1. 當(dāng)在Block中操作str=@"World"時(shí),相應(yīng)的__block結(jié)構(gòu)體會(huì)拷貝到heap上,同時(shí),stack上的__block結(jié)構(gòu)體的forwarding指針也會(huì)指向heap上的那份copy:


    image
  2. 因此,在Block外面再次輸出str的內(nèi)容時(shí),由于這時(shí)候stack上__block結(jié)構(gòu)體的forwarding指針已經(jīng)指向了heap上的__block結(jié)構(gòu)體,因此也會(huì)輸出heap上的captured_str指針?biāo)赶虻膬?nèi)容:@“World”。

為了驗(yàn)證我們的猜測(cè),我們可以用如下代碼:


image

在進(jìn)入Block前,Block中,進(jìn)入Block后分別設(shè)置斷點(diǎn),并打印aR指針的地址&aR,會(huì)得到如下結(jié)果:

image

可以看到,在Block中和進(jìn)入Block后,aR的地址是一樣的,而在進(jìn)入Block之前,則是另一個(gè)地址。這是因?yàn)樵趕tack上的__block結(jié)構(gòu)變量,將其forwarding指針指向了heap地址所導(dǎo)致的。

題目四. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash

typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *aStr = @"Hello";
        __block NSString *str = aStr;
        blockType blk = ^{
            str = @"World";
        };
        blk();
        NSLog(@"Now a aStr is %@", aStr);
        NSLog(@"Now str is %@", str);
    }
    return 0;
}

這個(gè)題目和題目三類似,只不過(guò)對(duì)于str的賦值由__block NSString *str = @"Hello"變成了__block NSString *str = aStr。

上面這段代碼會(huì)正常輸出,其結(jié)果為:


image

至于str為什么會(huì)由@"Hello"變成@“World”,其原因見題目三。

這里aStr是沒(méi)有任何變化的,這是因?yàn)樵趯tr在Block中賦值為@"World"時(shí),僅僅是將str指向了新的地址,而沒(méi)有更改原地址的內(nèi)容。而aStr一直指向舊的地址,也就是值為@"World"的地址。

題目五. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash

        NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
        blockType blk = ^{
            [str appendString:@" World"];
        };
        blk();
        NSLog(@"Now str is %@", str);

答案是會(huì)正常輸出。因?yàn)閷?duì)于NSObject類型來(lái)說(shuō),Block會(huì)copy一份指針常量來(lái)保存NSObject的地址。所謂指針常量,是指指針指向的地址是不可用更改的。而這里在Block中,并沒(méi)有更改指針指向的地址,而僅僅是改變了指針指向地址中的值,這個(gè)操作是允許的。
其輸出結(jié)果為:


image

同樣的,類似還有下面代碼,也是可以正常運(yùn)行,并輸出名字Tim:

        MyRetaion *aR = [MyRetaion new];
        aR.name = @"Jack";
        blockType blk = ^{
            aR.name = @"Tim";
        };
        blk();
        NSLog(@"Now name is %@", aR.name);

總結(jié)

在本篇文章中,我們根據(jù)Clang的官方文檔,分析總結(jié)了Clang為了支持Block,其背后所使用的數(shù)據(jù)結(jié)構(gòu)。同時(shí),我們重點(diǎn)分析了Block對(duì)于不同類型的外部變量的截取方式。按照Block不同的處理方式,Block截取的變量類型可以分為:

  • 全局/靜態(tài)類型
  • auto類型
  • Block類型
  • NSObject類型
  • __block類型

不同的類型,Block都有不同的截取處理方式。

通過(guò)深入了解Block的機(jī)制,相信對(duì)大家編程中正確高效的使用Block,是很有幫助的。

現(xiàn)在來(lái)回答我們文章最開始的部分,代碼的輸出結(jié)果為:

a = 14, b = 13, str = HelloWorld

至于原因,相信大家都會(huì)知道了:)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容