Block背后的数据结构及变量截取

本文的内容主要是基于Clang编译器的官方文档所写。

在开始探索Block的本质之前,大家先试着分析一下,下面的代码会输出什么:

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

如果你对输出结果不是那么有把握的话,那么相信通过今天的这篇文章,你会有一个明确的答案(答案在文章最后)。

Clang

先说些题外话,什么是Clang?Clang是C++编写的编译器。我们知道,我们平常代码所写的任何程序,最终都需要通过编译器转换成与语言无关的机器二进制代码。而Clang,则是支持/C++/Objective-C/Objective-C++的编译器。那我们在做OC开发时,可能也会听说LLVM编译器,那么Clang和LLVM之间是什么关系呢?

它们的关系如下图所示:
Block背后的数据结构及变量截取_第1张图片

Clang是编译器的前端,它会分析具体的编程语言,然后用于生成与机器无关的中间代码。而LLVM是编译器的后端,与具体编程语言无关,而是会去分析统一的中间代码,生成符合对应机器的目标程序。

这样拆分前端后端的好处在于,前后端可以独立的替换,便于编译器的优化。

关于Clang,我们了解这些就足够了。

Block的本质

回到Block上来。我们在使用Block语法时,总会感觉到有些奇怪:

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

这么一个^{}是什么鬼?似乎在别的语言中也没有见过这么个关键字定义。其实,^{}对于Clang编译器来说,仅仅是一个语言标记,它会告诉Clang,这里我需要定义一个Block类型的结构体。 而Clang发现这个语言标记时,会将^{}这么一个奇怪的定义,转换为C语言中的结构体。经过Clang转换后的Block,其形式是这样的:

struct Block_literal_1 {
	// 第一部分. Block基本信息以及 invoke函数指针
    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结构体定义分成了三个部分:

  • Block基本信息以及 invoke函数指针
  • Block descriptor指针
  • Block所截取的外部变量

在这里我们得出结论:Block的本质是一个C语言的struct

Block对应的结构体

上面探讨了Block的本质是一个struct,接下来我们就来详细看一下这个 Block struct的定义。

Block基本信息以及Block descriptor

struct Block_literal_1 {
	// 第一部分. Block基本信息以及 invoke函数指针
    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 struct的第一部分和第二部分。至于Block的第三部分,外部变量的截取,我们会在下面单独的章节进行讨论。
当我们声明一个Block时,对应的Block struct会被如下初始化:

  1. 系统会声明并初始化一个Block descriptor结构体。初始化Block descriptor步骤如下
    a. Block descriptor 的size部分会被设置为Block结构体的大小
    b. copy_helper 和 dispose_helper函数指针会被设置为对应的函数指针(如果需要这两个helper 函数的话)

  2. 系统初始化Block 结构体。 初始化Block 结构体的步骤如下:
    a. isa 部分会被设置为__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__ 所对应的地址。注意这里是地址,而不是__NSMallocBlock__这些变量。
    b. flags 会被置为对应的flag数值。比如,如果Block struct需要copy,dispose helper函数时,响应的flag会被置位。同时,flags还有标志Block ABI 版本的功能。
    c. 设置invoke函数指针指向对应的函数。该函数的第一个参数是Block struct本身的指针,而其余的参数则是Block执行时外部要传入的参数(如果有的话)

举个例子,对于下面的Block:

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

Clang会创建如下内容:

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将会如下被初始化:

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

这是Clang文档给出的官方例子,但是我们这里不要去纠结flags究竟是设置的什么,因为根据本人的测试,其flags的值并不是1<<29。

这里有个问题,就是什么时候isa会被设为&__NSGlobalBlock__/&__NSMallocBlock__/&__NSStackBlock__ 呢?

  • 当Block中没有引用外部变量,或引用了全局变量,const 标量或static变量时,Block的isa会被设置为&__NSGlobalBlock__。 这时的Block生命周期是伴随程序始终的。
  • &__NSStackBlock__ 表示这个block, 是在栈上面分配的,出了栈就会消亡。使用了外部栈变量,就会是__NSStackBlock__ 类型。
  • &__NSMallocBlock__ 表示Block复制到堆上面了,可以存储下来,以后使用。当Block引用了外部的OC对象,Block对象或用__block修饰的变量时,Block会被设置为&__NSMallocBlock__ 类型。这里有一点要注意,在ARC的情况下。只要将block赋值给变量,就自动帮你复制了。也就是说,如果将一个栈上的block赋值给另一个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]));

输出为:
在这里插入图片描述

而对于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]));

输出为:
在这里插入图片描述

这是因为对于Global,不必需要再在堆上开辟一块内存。

Block的外部变量截取

理解Block的关键,在于理解Block是如何处理外部变量的。

我们先来想一想,Block中会截取那些类型的外部变量:

  • 全局/静态变量
  • 自动(auto)存储类型
  • Block类型
  • NSObject类型
  • __block修饰的变量

截取全局/静态类型变量

对于全局/静态变量,Block会直接引用这类变量,不会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);
    }();
   
}

输出为:
在这里插入图片描述

在Block 外和Block内,static int a的地址是一样的,Block并没有做特殊的处理。

截取自动存储类型变量

所谓自动存储类型,指的是auto类型。我们可以理解为栈上的变量(Block类型、__block、NSObject类型除外),其内存会有系统自动释放。

对于auto类型的变量截取,Clang文档有如下描述:

Variables of auto storage class are imported as const copies.

也就是说,auto类型会在Block中用const copy一份。也就是说Block内、外是完全不同的两个变量。

我们做个测试:

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

输出为:
在这里插入图片描述

可以看到,在Block外和Block内部,表面上同样的b变量,其地址是不一样的。究其原因,就是因为在Block内部,系统会默默的const copy一份b。

这时候,Block的数据结构是这样的:

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;   // 这里会有一份const copy
};
struct __block_literal_2 __block_literal_2 = {
      &_NSConcreteStackBlock,
      (1<<29), ,
      __block_invoke_2,
      &__block_descriptor_2,
      x
 };

一般的,对于标量类型(int, float, bool等基本类型),struct,unions和函数指针类型,都会采用const copy的方式,将Block外部的变量拷贝到Block内部

这里需要注意一点,在iOS系统中,当我们把一个stack 上的Block赋值给一个Block变量时:

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

会默认调用Block的copy方法,即,上面实际上是如下代码:

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

这样得到的vv,是一个在堆上的Block变量。这时候再输出vv中x的地址,会得到一个堆上的地址。

因此,我们在做实验的时候,不要输出对拷贝后的Block中变量地址,而应该直接输出Block中的地址:

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

上面代码中并没有赋值,因此会输出栈上的a的const copy地址。

截取Block类型变量

对于截取Block类型的变量,在Block内部,会保留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();

输出为:
在这里插入图片描述

这里可以看到,对于Block变量,existingBlock(注意,这个existingBlock变量是一个Block指针,而不是Block本身)被const copy了一份到Block中。而对于Block指针所指向的Block实体,并没有发生改变。
也就说,在Block内部和外部,会有两个Block指针,指向了同一个Block结构体。

这里再次强调一下,我们所声明的Block变量existingBlock,是一个指向Block类型的指针,而不是Block实体。正如同NSObject *obj = [NSObject new]一样,obj是一个指向NSObject的指针,而不是NSObject实体

下面是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内部,是保持了外部的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,
};

这时候Block的数据结构是:

struct __block_literal_4 _block_literal = {
      &_NSConcreteStackBlock,
      (1<<25)|(1<<29), 
      __block_invoke_4,
      & __block_descriptor_4  // 这里可以看到,在Block内部,是保持了外部的Block指针
      existingBlock,
};

截取NSObject类型变量

在Clang中,NSObject类型变量被当做__attribute__((NSObject))类型。Block截取NSObject对象时,同样会做一份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];
    }();

输出为:
在这里插入图片描述

可以看到,当Block对NSObject做const copy时,仅是做了浅拷贝,并没有复制指针所指向的内容,仅仅是const copy了指针。因此,这里的self指针地址是改变了,而self指针所指向的地址都是同一个。

就像上面Block类型变量的例子,是同一个道理。

而对于NSObject类型,同样需要两个copy helper函数:

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修饰的变量

鉴于我们上面所说的都是const copy,因此对于在Block中对于其截取变量的任何改变,都是不被允许的。如果我们要修改Block内部的值,编译器就会提示如下错误:
在这里插入图片描述

那如何在Block中修改截取变量的值呢?我们自然会想到对外部变量加上__block修饰符。我们将上面代码改成下面的形式,则会顺利编译通过:

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

输出为:
在这里插入图片描述

这里会发现一个有意思的现象,虽然在进入Block前后,b的地址并不一样!也就是在进入Block前后,其实会有两个不同的b

之所以会这样,与Clang对于__block类型变量的处理有关。

当变量被标记为__block类型时,Clang会对变量b进行改写成一个如下格式的struct:

struct _block_byref_foo {
    void *isa;   // 设置为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;

会被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 结构体。这里需要明确一点:
添加了__block关键字后的int b,实质类型并不是int类型,而是一个struct _block的结构体类型了

这里有个关键的属性变量,forwardingforwarding指向一个__block结构体。
当__block在栈上时,forwarding会指向__block自身。而当__block在堆上生成一份copy时,这时候栈上的forwarding会指向堆上的那一份拷贝。而在堆上的那个__block的__forwarding 指针,则指向自己的首地址。

也就是说,只要通过forwarding来操作__block结构体捕获的外部变量,实质上是操作的同一个变量。
我们用图片可以更清楚的弄懂其中的原理:
Block背后的数据结构及变量截取_第2张图片

这也就是为什么,即使Block外和Block内部b分别是两个变量,而b的值却可以被改变的原因。因为在栈上的__block结构体中,通过forwarding指针指向了堆上的Block的地址。那么当在Block内部修改b的值,也就是改变堆上的int b的值的时候,在Block外部再访问b的值的时候,其实在栈上的__block int b通过__forwarding 指针,访问到了堆上的__block int b,这让我们感觉在栈上的变量也被修改了。

这也就是为什么,在测试代码中,在执行完Block后,再输出b的地址,发现是和Block内部的地址一致,而不是进入Block之前的地址的原因。(以为进入Block后,再次访问b,实际上会指向堆上的那个b,而不是之前栈上的那个b)

当我们将__block的变量导入Block中时,Clang会作如下改写:
例如,

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

会被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 };

上面的数据结构会做如下初始化

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), ,
      __block_invoke_5,
      &__block_descriptor_5,
      & i_holder,
};

是否只有__block类型才能够在Block中被修改?

这里插入一个小测试,对于静态变量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的值变为14。这是因为对于全局/静态变量而言,Block会直接引用变量,而不会做const copy。

所以,我们这一节讨论的,是除去全局、静态变量外,被Block const copy的其他的类型变量。

小测试

题目一. 下面代码会输出什么?

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

这里考察对于auto类型变量,Block的截取方式。因为auto变量会在Block中做一份const copy,因此在Block内外,实质上应该存在两个i
这里的输出为:
在这里插入图片描述

题目二. 下面的代码会 正常输出/编译错误/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;
}

因为对于NSObject类型,在Block中会当做NSObject *const obj处理,此时是一个指针常量。对于指针常量,是不能够更改其指针所指向的位置的,因此,这里会出现编译错误。

题目三. 下面的代码会 正常输出/编译错误/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;
}

因为str变量用了__block修饰,因此__block NSString *str 实质上一个__block struct 类型变量:

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

当创建__block 类型变量时,在Block结构体中,会存储__block结构体指针:

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结构体指针
}

当调用invoke方法时,会是这样的:

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

由于通过forwarding指针,确保了Block外部和内部的str都是一个指针,因此,当Block内部的str指向新的地址时(str = @"World"),在Block外部的str也指向了新的地址。(因为它们是同一个东西)。
这个过程用图表示为:

  1. __block str = @“World”;
    Block背后的数据结构及变量截取_第3张图片

  2. 当在Block中操作str=@"World"时,相应的__block结构体会拷贝到heap上,同时,stack上的__block结构体的forwarding指针也会指向heap上的那份copy:
    Block背后的数据结构及变量截取_第4张图片

  3. 因此,在Block外面再次输出str的内容时,由于这时候stack上__block结构体的forwarding指针已经指向了heap上的__block结构体,因此也会输出heap上的captured_str指针所指向的内容:@“World”

为了验证我们的猜测,我们可以用如下代码:
Block背后的数据结构及变量截取_第5张图片

在进入Block前,Block中,进入Block后分别设置断点,并打印aR指针的地址&aR,会得到如下结果:
Block背后的数据结构及变量截取_第6张图片

可以看到,在Block中和进入Block后,aR的地址是一样的,而在进入Block之前,则是另一个地址。这是因为在stack上的__block结构变量,将其forwarding指针指向了heap地址所导致的。

题目四. 下面的代码会 正常输出/编译错误/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;
}

这个题目和题目三类似,只不过对于str的赋值由__block NSString *str = @"Hello"变成了__block NSString *str = aStr

上面这段代码会正常输出,其结果为:
在这里插入图片描述

至于str为什么会由@"Hello"变成@“World”,其原因见题目三。

这里aStr是没有任何变化的,这是因为在将str在Block中赋值为@"World"时,仅仅是将str指向了新的地址,而没有更改原地址的内容。而aStr一直指向旧的地址,也就是值为@"World"的地址。

题目五. 下面的代码会 正常输出/编译错误/runtime crash

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

答案是会正常输出。因为对于NSObject类型来说,Block会copy一份指针常量来保存NSObject的地址。所谓指针常量,是指指针指向的地址是不可用更改的。而这里在Block中,并没有更改指针指向的地址,而仅仅是改变了指针指向地址中的值,这个操作是允许的。
其输出结果为:
在这里插入图片描述

同样的,类似还有下面代码,也是可以正常运行,并输出名字Tim:

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

总结

在本篇文章中,我们根据Clang的官方文档,分析总结了Clang为了支持Block,其背后所使用的数据结构。同时,我们重点分析了Block对于不同类型的外部变量的截取方式。按照Block不同的处理方式,Block截取的变量类型可以分为:

  • 全局/静态类型
  • auto类型
  • Block类型
  • NSObject类型
  • __block类型

不同的类型,Block都有不同的截取处理方式。

通过深入了解Block的机制,相信对大家编程中正确高效的使用Block,是很有帮助的。

现在来回答我们文章最开始的部分,代码的输出结果为:

a = 14, b = 13, str = HelloWorld

至于原因,相信大家都会知道了:)

你可能感兴趣的:(ios开发)