首页 关于 微信公众号
欢迎关注我的微信公众号

Block

本文介绍了 Block 的基本语法知识以及相关的使用场景,并进一步探索了 Block 的底层实现机制和内存管理机制,还总结了使用 Block 时应该注意的各种问题。这篇文章写的很长,是为了把关于 Block 的种种问题探索的更清晰,如果在阅读时觉得太累可以根据文章的目录结构寻找自己想要了解的部分。

Block 是什么

Block 是 Apple 在 C 语言的基础上添加的扩展功能,由于 Ojective-C、C++ 都是源自于 C,所以这三种语言都能够使用它。对于 Block 的功能,可以用一句话概括:能持有作用域变量的匿名函数。

匿名函数就是没有名称的函数,C 语言的标准不允许存在这样的函数,而通过 Block,源代码中就可以使用匿名函数了。

能持有作用域变量就是指 Block 能够获得其所在作用域的变量。其中其所在作用域的变量就包括:局部变量(自动变量)、函数的参数、静态局部变量、静态全局变量、全局变量。

Block 能够让我们创建明显的代码片段,并且可以像参数那样传递给方法或函数。在 Objective-C 中,Block 就像对象一样,能够被添加到集合中(比如:NSArray、NSDictionary)。Block 能够获得其所在作用域的变量,就如同其他语言里闭包(Closure)或者lambda 计算的概念。

Block 语法形式

Block 的语法遵循如下形式:

其中返回值类型是可以省略的,但需要保证表达式中的所有 return 语句的返回值类型相同。参数列表在为空的情况下也是可以省略的。

一些示例:

// 一个最简单的 block:
^{
	NSLog(@"This is a block");
}

// 不省略的 block:
^void (void) {
	NSLog(@"This is a block");
}

// 普通的 block:
^int (int a, int b) {
	NSLog(@"a is %d, b is %d", a, b);
	return a + b;
}

可见,Block 语法从形式上来看除了没有名字以及带有 ^ 外,其他都和 C 语言的函数定义相同。

Block 类型变量的使用

在 C 语言中,可以把一个函数的地址赋值给函数指针类型的变量,如下:

int func(int a) {
	return a + 1;
}
int (*func_pointer)(int) = &func;

同样,在 Block 语法中,也可以把一个 Block 赋值给 Block 类型的变量。

最简单的 Block

声明和定义一个不返回任何值,不接受任何参数的 Block:

// 完整的写法:
//// 声明
void (^simpleBlock)(void); 
//// 定义
simpleBlock = ^{
    NSLog(@"This is a block");
};
//// 声明、定义一起
void (^simpleBlock)(void) = ^{
    NSLog(@"This is a block");
};
// 调用 block
simpleBlock();

带参数和返回值的 Block

下面的 Block,接受两个 double 参数,返回一个 double 值:

// 声明
double (^multiplyTwoValues)(double, double);
// 定义
double (^multiplyTwoValues)(double, double) =
                          ^(double firstValue, double secondValue) {
                              return firstValue * secondValue;
                          };
// 调用
double result = multiplyTwoValues(2,4);
NSLog(@"The result is %f", result);

Block 作为参数时的缩写

Block 作为参数时的缩写如 Block 语法规则所约定那样。

// Block 没有参数,则 `()` 可以省略:
[UIView animateViewDuration:5.0 animation:^() { // 这个 block 没有参数,这里的 () 就可以省略。
	view.opacity = 0.5;
}];

// 如果 Block 中返回值的类型根据 return 后的内容能明显推出,那么可以省略:
NSSet* mySet = ...;
NSSet* matches = [mySet objectsPassingTest:^BOOL(id obj, ...) { // 根据return语句,这个block的返回值明显是BOOL,所以这里可以省略BOOL。
	return [obj isKindOfClass:[UIView class]]; // 返回值是 BOOL,很明显。
}];

把 Block 传递给方法或函数

// 调用带 block 参数的方法:
- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];
 
    XYZWebTask *task = ...
 
    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    }];
}

// 声明带 block 参数的方法:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;

// 方法的具体实现:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
	...
	callbackBlock();
}

苹果的建议是在一个方法中最好只使用一个 block 变量,并且如果这个方法如果还带有其他非 block 变量,那么 block 变量应该放在最后一个。

使用 typedef 来简化 Block 定义

上面的 Block 类型的变量在使用时,记述方式会一眼看上去有点不够简洁,这时候我们也可以用 typedef 来解决这个问题。

// typedef 一个 block
typedef void (^XYZSimpleBlock)(void);
// 使用 1
XYZSimpleBlock anotherBlock = ^{
    ...
};
// 使用 2
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
	...
	callbackBlock();
}

来看一个用 typedef 简化复杂 Block 定义的例子,下面的定义的名为 complexBlock 的变量是一个 block,这个 block 接受一个 block 作为参数,并且返回一个 block:

// 简化前:
void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
	...
	return ^{
    	...
	};
};
// 使用上面 typedef 的 XYZSimpleBlock 简化后:
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
    ...
    return ^{
        ...
    };
};

定义属性来持有 Block

使用 copy,因为 Block 要持有它原本所在作用域的其他外面的变量:

@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end

// setter 方法和调用
self.blockProperty = ^{
    ...
};
self.blockProperty();

// 使用 typedef 简化
typedef void (^XYZSimpleBlock)(void);
 
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end

Block 的指针类型变量

我们还可以使用指向 Block 类型变量的指针,即 Block 的指针类型变量。

typedef int (^MyBlock)(int);
MyBlock aBlock = ^int(int a) {
	return a + 1;
};

MyBlock* aBlockPointer = &aBlock;
int result = (*aBlockPointer)(10); // result = 11.

可见 Block 类型变量可像 C 语言中其他类型变量一样使用。

Block 的使用场景

Block 简化枚举

// NSArray 的一个方法,接受一个 block 作为参数,这个 block 会在该方法里每枚举一个对象时被调用一次:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
// 这个 block 接受三个参数,前两个是当前的对象和其 index,后面的 BOOL 值可以用来控制什么时候停止该枚举。
NSArray *array = ...
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
    NSLog(@"Object at index %lu is %@", idx, obj);
}];
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
    if (...) {
        *stop = YES;
    }
}];

Block 简化并发调用

Block 和 Operation Queue 一起用

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    ...
}];

Block 和 GCD 一起用

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    NSLog(@"Block for asynchronous execution");
});

Block 的使用场景小结

Block 的实现

在前面的内容中,我们知道了 Block 是「能持有作用域变量的匿名函数」,还介绍了使用 Block 的相关内容,那么 Block 究竟是如何实现的呢?我们可以用 clang(LLVM 编译器)把带 Block 语法的源代码代码转换为我们能够理解的源代码来初探一下。这里我们可以使用 clang -rewrite-objc <source-code-file> 把含有 Block 语法的源代码转换成 C++ 的源代码(这里其实就是使用了 struct 结构的 C 代码)。

最简单的Block

包含 Block 语法的源代码 test_block.m:

#include <stdio.h>

int main() {
	void (^blk)(void) = ^{
		int tag = 8;
		printf("Block, %d\n", tag);
	};

	blk();

	return 0;
}

使用 clang -rewrite-objc test_block.m 编译后得到了 test_block.cpp 代码,从里面截取相关的代码如下:

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

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	__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 的匿名函数。参数 __cself 为指向 block 值的指针,类似 Objective-C 中实例方法中指向对象自身的变量 self。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	int tag = 0;
	printf("Block, %d\n", tag);
 }

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() {
	void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

	((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

	return 0;
}

如转换后的代码所示,Block 使用的匿名代码被转换为 C 语言函数,其函数名 __main_block_func_0 则根据 Block 语法所属的函数名(这里是 main)和该 Block 语法在该函数出现的顺序值(这里是 0)来命名。((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); 最终简化后其实就是用函数指针实现对函数的调用。根据 impl.isa = &_NSConcreteStackBlock; 这里用 _NSConcreteStackBlock 来初始化 __block_impl 结构体的 isa 成员。

分析这些代码,我们最后能得出的结论是:

Block 截获自动变量

我们对 test_block.m 代码稍作修改。其中在 Block 中用到了自动变量。

#include <stdio.h>

int main() {
	int dummy = 256;
	int tag = 10;
	const char *say = "tag = %d\n";

    void (^blk)(void) = ^{
        printf(say, tag);
    };
    
    blk();
    
    return 0;
}

使用 clang -rewrite-objc test_block.m 转换代码后,截取相关片段如下:

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

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 这里可以看到,这里的两个 Block 结构体的成员变量与外面的两个自动变量完全相同,这是等会要来拷贝用的。
    const char *say;
    int tag;
    // 下面是结构体的构造函数。
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_say, int _tag, int flags=0) : say(_say), tag(_tag) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
// 这里可以看到,只是把自动变量的值拷贝进来了,并且只拷贝了 Block 里用到的自动变量,如:say 和 tag,并没有拷贝 dummy。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    const char *say = __cself->say; // bound by copy
    int tag = __cself->tag; // bound by copy
    
    printf(say, tag);
}

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 dummy = 256;
    int tag = 10;
    const char *say = "tag = %d\n";
    
    void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, say, tag);
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    return 0;
}

分析上面的代码,我们可以得到的结论是:

说到这里,我们就能理解为什么使用 C 语言的数组时会有问题了。

// 这样是有问题的:
const char text[] = "hello";
void (^blk)(void) = {
	printf("%c\n", text[2]);
}
// 这样是没问题的:
const char* text = "hello";
void (^blk)(void) = {
	printf("%c\n", text[2]);
}

上面有问题的那种使用方式,可能被转换后得到的代码形式是类似这样的:

void func(const char text[]) {
	const char text2[] = text; // 这种赋值方式是不符合 C 语言规范的。
	printf("%c\n", text[2]);
}
int main() {
	cosnt char text[] = "hello";
	func(text);
}

而这种赋值方式是不符合 C 语言规范的。而使用指针就没问题了。

Block 使用静态、全局变量

下面的代码中,用到了静态变量、全局变量、全局静态变量,并在 Block 中改变了他们的值。

#include <stdio.h>

int global_val = 1;
static int static_global_val = 2;

int main() {
	static int static_val = 3;


    void (^blk)(void) = ^{
        global_val = 100;
        static_global_val = 200;
        static_val = 300;
    };
    
    blk();
    
    return 0;
}

使用 clang -rewrite-objc test_block.m 转换代码后,截取相关片段如下:

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

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 可以看到,对于静态变量,在 Block 中是用其指针,所以改变其值也是没问题的。
    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 = 100;
    static_global_val = 200;
    (*static_val) = 300;
}

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 = 3;
    
    
    void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    return 0;
}

分析上面上面的代码,可以得出的结论是:

__block 说明符

上面展示了 Block 截获自动变量以及使用静态变量、全局变量和全局静态变量的实现,接下来,再来看看当需要在 Block 里面改变自动变量时的情况,也就是使用 __block 说明符时 Block 的实现。这里需要关注的问题是:当使用了 __block 说明符后,对变量做了什么处理,才使得不在变量作用域的 Block 可以去访问修改变量呢?

__block 说明符更准确的表述是:__block 存储域类说明符。在 C 语言中还有这些存储域类说明符:typedef、extern、static、auto、register。这些说明符用于指定将变量值设置到哪个存储域中。例如,auto 表示作为自动变量存储在栈上,static 表示作为静态变量存储在数据区等。下面来看看 __block。

#include <stdio.h>

int main() {
	int __block val = 3;

    void (^blk)(void) = ^{
		val = 300;
	};
    
    blk();
    
    return 0;
}

使用 clang -rewrite-objc test_block.m 转换代码后,截取相关片段如下:

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

// 这个结构体是用来定义原来的自动变量 val 的。
struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding; // 这个成员变量是一个指向自己实例的指针。
    int __flags;
    int __size;
    int val; // 这个成员变量将用来存原来自动变量的值。
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 原来的自动变量被重新定义成了一个结构体,val 成了指向这个结构体的一个实例的指针。
    __Block_byref_val_0 *val; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
    
    // 下面的赋值可以看出,是把原自动变量变成了 __main_block_impl_0 结构体的实例的指针。再通过指针去访问这个实例的成员变量。其中成员变量 __forwarding 指向实例自己,成员变量 __val 存储值。
    (val->__forwarding->val) = 300;
}

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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() {
	// 可以看到,变量 val 竟然成为了结构体 __Block_byref_val_0 的实例。
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 3};
    
    void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    return 0;
}

上面的代码有两个函数需要说明一下:_Block_object_assign 函数实现把 Block 从栈上拷贝到堆上;_Block_object_dispose 函数实现把 Block 从堆上释放。

结合上面的代码,我们可以知道这里解答了本节开头的问题:怎么处理使得可以不在变量作用域(Block 被实现为自动变量作用域之外的一个函数了)的 Block 中访问修改 __block 类型的变量?答案简单来说就是用指针,不过做了一些包装:__block 描述的自动变量被重新定义为了结构体 __Block_byref_val_0,原自动变量 val 变成了这个结构体的成员变量 val,并且在 Block 中使用的是这个结构体实例的指针 val。但是,用指针,这其实跟前面讲到 Block 修改静态局部变量的方式一样,这并没有展示出 __block 类型变量的真正核心意义。

那 __block 说明符的核心意义究竟是什么呢?这要回到当初设计 Block 的初衷上去看,Block 是「能截获自动变量的匿名函数」,既然是函数,那就有在某处被调用的需求了,比如说自动变量的作用域之外(我们转换代码已经看到,Block 转换后确实是实现成作用域之外的函数)。我们上面也看到了用指针来截获和修改自动变量的手段,但是超过了作用域调用 Block 时,它去修改被它截获的、过期的自动变量是会崩溃的,所以 __block 的核心要解决的是超过自动变量作用域来调用 Block 的场景,并且这个 Block 要能截获和修改这个自动变量。解释这个问题需要先了解与 __block 对应的内存管理方式,这个在下节说明。

__block 类型变量被转换后的结构体如下图所示。这里同时引出了一个问题:用指针 val 指向变量转变后的结构体实例就好了,为什么结构体里还有一个指向自己实例的 __forwarding 指针?这个问题将放在后面讲解。

image

此外,Block 的实现中使用的是 __Block_byref_val_0 结构体的实例的指针,这样可以做到在多个 Block 中使用同一个 __block 变量。当然在一个 Block 中使用多个 __block 变量也是可以的,只要增加 Block 结构体的成员变量与构造函数的参数即可。

在这节,我们遗留了两个问题:

这两个问题是关于 __block 自动变量的,但是我们还要先看看 Block 内存管理相关的知识后再来解答。

Block 存储域

Block 有 3 种类型:

image

1)NSConcreteGlobalBlock

在下面两种情况下,我们得到的 Block 是 _NSConcreteGlobalBlock 类型的:

2)NSConcreteStackBlock

除上面两种情况创建的 Block 以外,其他 Block 语法生成的 Block 均为 _NSConcreteStackBlock 类型,存储在栈上。

下面看几个例子:

// 此 Block 是 _NSConcreteGlobalBlock 类型的。此 Block 结构体实例的内容不会依赖执行时的状态,所以整个程序中只需要一个实例。因此将其放在与全局变量相同的数据区域即可。
void (^blk)(void) = ^{printf("Global Block\n");};
int main() {}

// 此 Block 需要截获自动变量,截获的自动变量的值会根据执行时的状态变化,下面每次 for 循环 Block 截获的自动变量的值都不一样。所以需要把对应的 Block 结构体的实例放在调用栈中,让其对应不同调用上下文的状态。
typedef int (^MyBlock)(int);
for (int i = 0; i < 10; i++) {
	MyBlock blk = ^(int c) {
		return i * c;
	};
}

// 此 Block 虽然不是放在全局代码位置,但是它在实现中没有使用任何自动变量,Block 结构体的实例在每次循环中都是一个。虽然通过 clang 转换代码看到的是 _NSConcreteStackBlock 类型,但实现上却有不同。它还是会被放在全局数据区域。
typedef int (^MyBlock)(int);
for (int i = 0; i < 10; i++) {
	MyBlock blk = ^(int c) {
		return c;
	};
}

3)NSConcreteMallocBlock

那么问题来了,什么时候会用到存储在堆上的 _NSConcreteMallocBlock 类型的 Block 呢?

Block 语法机制提供了将 Block 和 __block 变量从栈上复制到堆上的方法来解决这个问题。将存储在栈上的 Block 复制到堆上,即使 Block 语法记述的变量作用域生命周期结束,堆上的 Block 还是可以继续存在。复制到堆上的 Block 将 _NSConcreteMallocBlock 类对象写入 Block 结构体实例的 isa 成员变量,而 __forwarding 成员变量就可以实现无论 __block 变量存储在栈上还是堆上都能准确的被访问。

image

所以总结起来,NSConcreteGlobalBlock 和 NSConcreteStackBlock 两种类型从语法上就能判断,而 NSConcreteMallocBlock 则更像是一种运行时的类型,在是在程序执行时,当 Block 被 copy 到堆上的时候将 Block 的类型设置为了 NSConcreteMallocBlock 类型。

看到这里,上节遗留的两个问题的答案已经初见端倪,不过,这里还是集中精神先说 Block 的内存管理,还没深入到解答那两个问题,只是突然扯出了答案的尾巴,先不管,还是先关注 Block 吧。

Block 内存管理需要注意的问题

在 ARC 的情况下,编译器在大多时候会做出恰当的判断,自动生成把 Block 从栈上拷贝到堆上的代码。比如:

typedef int (^blk_t)(int);
blk_t func(int rate) {
    return ^(int count){return rate * count;};
}

上面的代码通过对应 ARC 的编译器可转换如下:

blk_t func(int rate) {
    blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    tmp = objc_retainBlock(tmp);
    return objc_autoreleaseReturnValue(tmp); 
}

其大致意思就是将 Block 作为函数返回值返回时,编译器会自动生成复制到堆上的代码。

但有时候编译器是无法判断的,这时候需要我们手动过调用 Block 的 copy 方法来将 Block 从栈上复制到堆上。比如:

- (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects: 
                            ^{NSLog(@"blk0:%d", val);}, 
                            ^{NSLog(@"blk1:%d", val);}, nil];
}

int main() {
	id obj = getBlockArray();
	typedef void (^blk_t)(void);
	blk_t blk = (blk_t)[obj objectAtIndex:0];
	
	blk(); // 执行时发生异常。
}

blk() 在执行时发生异常,这时由于 getBlockArray 函数执行结束时,栈上的 Block 被废弃的缘故。可惜此时编译器不能判断是否需要自动复制。将代码像下面这样改一下就可以正常运行了:

// 将 getBlockArray 改为下面代码即可(手动 copy 数组中的 Block):
- (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects: 
                            [^{NSLog(@"blk0:%d", val);} copy], 
                            [^{NSLog(@"blk1:%d", val);} copy], nil];
}

也可以不让编译器判断,在任何时候都使用复制,但是将 Block 从栈上复制到堆上是很消耗 CPU 的,所以我们需要了解哪些时候编译器能判断出来并自动处理,哪些时候不能判断处理需要我们手动 copy。

在下面这些情况下,编译器判断不出究竟是什么情况:

但是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。以下方法或函数就不需要手动复制:

关于 Block 的拷贝操作可以用一张表总结下:

Block 的类 副本源的存储域 copy 效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 引用计数增加

所以,不管 Block 存储在何处,用 copy 方法复制都不会引起任何问题。在不确定时调用 copy 方法即可。在 ARC 中不能显式的 release,但是即使多次调用了 copy 方法进行复制也不会有问题。

小结

- (void)testBlock {
    NSArray *blkArray = [self getBlockArray];
    typedef void (^blk_t)(void);
    for (blk_t blk in blkArray) {
        blk();
    }
    blk_t singleBlk = [self getBlock];
    singleBlk();
}
// 在方法中返回 Block 不需要 copy 了。
- (id)getBlock {
    int val = 88;
    return ^{NSLog(@"blk:%d", val);};
}
// 在方法中传递 Block 作为参数时,还是需要 copy 的。
- (NSArray *)getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"blk0:%d", val);} copy],
            [^{NSLog(@"blk1:%d", val);} copy], nil];
}

__block 变量存储域

讲完对 Block 的处理后,我们知道了通过把 Block 拷贝到堆上,使得 Block 离开作用域也可以被使用,但是,Block 自己还有使用作用域里的自动变量的需求。所以接下来要解答这个问题,开讲 Block 机制对 __block 变量的处理。

使用 __block 变量的 Block 从栈上复制到堆上时,__block 也会受到影响:

__block 变量原来的存储区域 Block 从栈复制到堆对其截获的 __block 变量的影响
从栈上复制到堆上并被 Block 持有
被 Block 持有

接下来看几个具体的场景:

到这里我们可以看到,上面的内存管理方式跟 Objective-C 引用计数式的内存管理完全相同。

回顾一下前面的问题:

所以这两个问题的答案简单来讲其实就是:把 Block 和 __block 变量拷贝到堆上,来实现在超过自动变量作用域的地方调用 Block 时,不用担心自动变量过期的问题。而 __forwarding 正是用来解决 __block 变量拷贝到堆上还能被正确访问这件事的。这样我们就更能理解,转换代码后显示的 Block 和 __block 变量被封装成了结构体的实例,这也是为了拷贝上堆服务的。

下面这个例子进一步说明一下 __block 变量转换后要用 __forwarding 指针的原因:

void func() {
    int __block val = 0;
    
    // Block 从栈上复制到堆上,它使用的 __block 变量 val 也被复制到堆上。
    void (^blk)(void) = [^{++val;} copy]; 
    NSLog(@"%d", val); // 打印:0
    ++val;
    NSLog(@"%d", val); // 打印:1
    blk();
    NSLog(@"%d", val); // 打印:2
}

可以看到,++val; 这句代码使用的是与 Block 无关的栈上的变量 val。blk(); 这句代码调用后,其实现 ^{++val;} 使用的是拷贝到堆上的 __block 变量 val。最后打印出来的结果是我们想要的。其实这两处的 ++val; 都可以翻译为 ++(val._forwarding->val); 结合前面转换代码里看到的 __block 变量的结构,可以知道这里的 __block 变量的内存状态大致如图:

image

这里,__forwarding 指针的意义显而易见了吧。

总结

这节的内容讲的东西还是比较多的,看着看着可能就失去焦点,陷入细节之中了,在这里,我梳理了一下这节内容的逻辑链条以 Why-How-When 的顺序展开以方便理解。

Why

「Block 的实现」这节是在我们明确了「Block 是能持有作用域变量的匿名函数」这个定义后,来探索它究竟是如何实现这个定义的。那么首先我们需要搞清楚的是 Block 这个定义背后的设计目标(Why):

How

接下来我们分为下面几个步骤来探讨怎么实现上述的设计目标(How):

When

通过上面的内容,我们可以理解 Block 的能力的实现机制,但是在具体使用时在怎样的场景下会触发这些机制是我们接下来需要搞清楚的(When):

使用 Block 需要注意的问题

使用 Block 时需要手动 copy 的情况

总结一下在 ARC 下使用 Block 时需要手动 copy 和不需要手动 copy 的情况:

Block 截获作用域变量的行为

基本类型变量

// 不能改变局部变量:
int anInteger = 42;
void (^testBlock)(void) = ^{
    NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock(); // 输出:Integer is: 42
// 加了 __block 后可以改变局部变量:
int __block anInteger = 42;
void (^testBlock)(void) = ^{
    NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock(); // 输出:Integer is: 84
// 还可以这样:
int __block anInteger = 42;
void (^testBlock)(void) = ^{
    NSLog(@"Integer is: %i", anInteger);
    anInteger = 100;
};
testBlock(); // 输出:Integer is: 42
NSLog(@"Value of original variable is now: %i", anInteger); // 输出:Value of original variable is now: 100

如果获得对象(在堆上),调用变更该对象的方法是没问题的(存储对象的空间在堆上),而直接向截获的变量赋值则会产生编译错误(这个对象的指针是在栈上的)。

// 这样是没问题的:
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
	id obj = [[NSObject alloc] init];
	[array addObject:obj];
};
// 这样是不对的:
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
	array = [[NSMutableArray alloc] init];
};

在 Block 中使用 C 语言的数组时必须小心使用其指针。在现在的 Block 中,截获自动变量的方法并没有实现对 C 语言数组的截获,使用指针可以解决该问题。

// 这样是有问题的:
const char text[] = "hello";
void (^blk)(void) = {
	printf("%c\n", text[2]);
}
// 这样是没问题的:
const char* text = "hello";
void (^blk)(void) = {
	printf("%c\n", text[2]);
}

对象

在 ARC 环境下,我们来讨论下 Block 对 __weak__strong__block 修饰的对象的截获行为。

为了测试,先自定义一个类 MyObject,含一个属性 text,重写 description 方法。如下:

// MyObject.h
#import <Foundation/Foundation.h>
@interface MyObject : NSObject
@property (nonatomic, strong) NSString *text;
@end

// MyObject.m
#import "MyObject.h"
@implementation MyObject
#pragma mark - Property
- (NSString *)description {
    return _text;
}
@end

1)先来看看 __weak 指针:

// ViewController.m
#define PrintObject(prefix, obj) { NSLog(@"%@: (指针内存地址: %p, 指针值: %p, 指向的对象值: %@)", prefix, &obj, obj, obj); }

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    typedef void (^MyBlock)();
    
    MyBlock blk;
    
    {
        MyObject *obj = [[MyObject alloc] init];
        obj.text = @"I-am-an-obj.";
        PrintObject(@"obj", obj);
        
        typeof(obj) __weak weakObj = obj;
        PrintObject(@"weakObj", weakObj);
        
        blk = ^() {
            PrintObject(@"weakObj in Block", weakObj);
        };
        
        blk();
    } // obj、weakObj 的作用域结束。
    
    blk();	    
}

程序的打印信息如下:

obj: (指针的内存地址: 0x7fff5e514120, 指针值: 0x7ff56c1003e0, 指向的对象值: I-am-an-obj.)
weakObj: (指针的内存地址: 0x7fff5e514118, 指针值: 0x7ff56c1003e0, 指向的对象值: I-weakObj in Block: (指针的内存地址: 0x7ff56bf1cce0, 指针值: 0x7ff56c1003e0, 指向的对象值: I-am-an-obj.)
weakObj in Block: (指针的内存地址: 0x7ff56bf1cce0, 指针值: 0x0, 指向的对象值: (null))

结论:

2)再来看看 __strong 指针:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    typedef void (^MyBlock)();
    
    MyBlock blk;
    
    {
        MyObject *obj = [[MyObject alloc] init];
        obj.text = @"I-am-an-obj.";
        PrintObject(@"obj", obj);
        
        typeof(obj) __strong strongObj = obj;
        PrintObject(@"strongObj", strongObj);
        
        blk = ^() {
            PrintObject(@"strongObj in Block", strongObj);	        };
        
        blk();
    } // obj、strongObj 的作用域结束。
    
    blk();
}

程序打印的信息如下:

obj: (指针的内存地址: 0x7fff54284120, 指针值: 0x7f9012c23e70, 指向的对象值: I-am-an-obj.)
strongObj: (指针的内存地址: 0x7fff54284118, 指针值: 0x7f9012c23e70, 指向的对象值: I-am-an-obj.)
strongObj in Block: (指针的内存地址: 0x7f9012d1cc20, 指针值: 0x7f9012c23e70, 指向的对象值: I-am-an-obj.)
strongObj in Block: (指针的内存地址: 0x7f9012d1cc20, 指针值: 0x7f9012c23e70, 指向的对象值: I-am-an-obj.)

结论:

3)接着看 __block

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    typedef void (^MyBlock)();

    MyBlock blk;
    
    {
        MyObject *obj = [[MyObject alloc] init];
        obj.text = @"I-am-an-obj.";
        PrintObject(@"obj", obj);
        
        typeof(obj) __block blockObj = obj;
        PrintObject(@"blockObj", blockObj);
        
        blk = ^() {
            PrintObject(@"blockObj in Block", blockObj);
        };
        
        blk();
    } // obj、blockObj 的作用域结束。
    
    blk();
    
    
}

程序打印的信息:

obj: (指针的内存地址: 0x7fff57581120, 指针值: 0x7f9eac800440, 指向的对象值: I-am-an-obj.)
blockObj: (指针的内存地址: 0x7fff57581118, 指针值: 0x7f9eac800440, 指向的对象值: I-am-an-obj.)
blockObj in Block: (指针的内存地址: 0x7f9eac800e28, 指针值: 0x7f9eac800440, 指向的对象值: I-am-an-obj.)
blockObj in Block: (指针的内存地址: 0x7f9eac800e28, 指针值: 0x7f9eac800440, 指向的对象值: I-am-an-obj.)

结论:

4)来看一种有意思的混搭,在 Block 内部使用 __strong 类型的引用持有外部的 __weak 对象:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    typedef void (^MyBlock)();
    
    MyBlock blk;
    
    {
        MyObject *obj = [[MyObject alloc] init];
        obj.text = @"I-am-an-obj.";
        PrintObject(@"obj", obj);
        
        typeof(obj) __weak weakObj = obj;
        PrintObject(@"weakObj", weakObj);
        
        blk = ^() {
            typeof(weakObj) __strong strongObj = weakObj;
            PrintObject(@"weakObj in Block", weakObj);
            PrintObject(@"strongObj in Block", strongObj);
        };
        
        blk();
    } // obj、weakObj 的作用域结束。
    
    blk();
}

程序打印的信息:

obj: (指针的内存地址: 0x7fff54625120, 指针值: 0x7fb832210110, 指向的对象值: I-am-an-obj.)
weakObj: (指针的内存地址: 0x7fff54625118, 指针值: 0x7fb832210110, 指向的对象值: I-am-an-obj.)
weakObj in Block: (指针的内存地址: 0x7fb832210010, 指针值: 0x7fb832210110, 指向的对象值: I-am-an-obj.)
strongObj in Block: (指针的内存地址: 0x7fff54625048, 指针值: 0x7fb832210110, 指向的对象值: I-am-an-obj.)
weakObj in Block: (指针的内存地址: 0x7fb832210010, 指针值: 0x0, 指向的对象值: (null))
strongObj in Block: (指针的内存地址: 0x7fff54625048, 指针值: 0x0, 指向的对象值: (null))

结论:

避免强引用循环

每次向 Block 里的对象发送消息(方法调用)的时候,将会创建一个 strong 指针指向这个对象,直到 Block 结束。所以像下面的代码,self strong 持有 Block,而在 Block 里又 strong 持有了 self,这样谁也不能被释放:

// .h
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
// .m
@implementation XYZBlockKeeper
- (void)configureBlock {
    self.block = ^{
    	// capturing a strong reference to self, creates a strong reference cycle
        [self doSomething];
    };
}
...
@end

可以这样处理:

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    self.block = ^{
    	// capture the weak reference to avoid the reference cycle
        [weakSelf doSomething];
    }
}

为了解决强引用循环的问题,上面我们用到了 __weak,但是为了考虑对于早期版本的兼容以及 ARC 和 MRC 环境下的使用,我们还需要了解:

// MRC.
#import <malloc/malloc.h>
- (void)configureBlock {
	XYZBlockKeeper * __block blockSelf = self;
	self.block = ^{
		if (!malloc_zone_from_ptr(blockSelf)) {
			return;
		}
		[blockSelf doSomething];
	}
}

避免Block使用的对象被提前释放

上面提到了使用 weak 引用的方式解决 retain cycle 的方案,但随着应用场景的改变,又会带来新的问题,比如:在 Block 中用异步的方式使用了外部对象,当对象被释放后,异步方法回调时访问该对象则会为空,这时就可能造成程序崩溃了。解决这个问题的方式则是 weak/strong 化,示例代码如下:

TestBlockViewController.m

#import "TestBlockViewController.h"
#import "TestService.h"

@interface TestBlockViewController ()
@property (nonatomic, strong) NSString *tag;
@property (nonatomic, copy) void (^myBlock)(void);
@end

@implementation TestBlockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Init properties.
    self.tag = @"tag is OK.";
    
    // Init TestService's block.
    typeof(self) __weak weakSelf = self;
    self.myBlock = ^{
        typeof(weakSelf) __strong strongSelf = weakSelf;
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"strongSelf is OK.");
            NSLog(@"%@", strongSelf.tag);
            //NSLog(@"%@", self.tag); // Retain cycle.
        });
    };
    
}

- (IBAction)callService:(id)sender {    
    self.myBlock();
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)dealloc {
    NSLog(@"TestBlockViewController dealloc.");
}

@end

这样即使当前的 TestBlockViewController 被 pop 后,block 中也由于强引用了 weakSelf 所以延长了其生命周期,最后打印的结果是:

strongSelf is OK.
tag is OK.
TestBlockViewController dealloc.

我们在前面「Block 截获作用域变量的行为」那节已经提到过这种用法,这里我们看到了具体的实例了。这里需要关注的是两点:

参考

Blog

Opinion

Project