Block
  • Block 对象是一个C语言层级的语法和 runtime 的特性。

  • Block 类似于标准C函数,但除了可执行代码外,它还可能包含了自动(栈stack)或托管(堆heap)的内存所绑定的变量。因此,Block 可以保存一组状态(数据),当执行它时可以使用这些状态(数据)来影响行为。

  • 可以使用Block来编写函数表达式,这些函数表达式可以传递给API,可以将其存储并由多个线程使用。

  • Block作为回调特别有用,因为Block既包含回调时要执行的代码,也包含执行过程中所需的数据。

  • block runtime是开源的,可以在LLVM’s compiler-rt子项目存储库中找到,或Open Sourece查看,或者下载源码

  • Block也提交给C标准工作组,由于Objective-CC++都从C派生而来,所以Block被设计成可以在这三种语言中运行(以及Objective-C++)。

Block的基本使用

创建一个block时,^运算符指示block字面量表达式的开头,它的后面可能跟着一个包含在()中的参数列表。Block的主体包含在{}中。block定义语法如下:

1
2
3
<returnType>(^<blockName>)(<parameterTypes>) = ^(<parameters>) {
<statements>
};

1、定义有参数和无参数的block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1、声明block变量,block变量保存对block的引用。
int (^sumBlock)(int, int);
sumBlock = ^(int a, int b){
return a + b;
};
// 通过block变量名,像调用函数一样block一样使用它
// block变量保存对block的引用, 如:sumBlock
int sum = sumBlock(10, 20);

// 2、定义无参block
// 声明无参数的block必须在参数列表中指定void
void (^block1)(void) = ^(){
NSLog(@"调用了block1");
};

void(^block2)(void) = ^{

};

2、定义block时返回值类型通常省略

如果没有显式声明Block表达式的返回值,则可以从块的内容自动推导出返回值。

1
2
3
4
5
6
7
void (^block4)(NSString *) = ^ void (NSString *str){
NSLog(@"%@", str);
};

int (^block5)() = ^{
return 100;
};

3、block的类型

1
2
3
4
// block4的类型:int(^)(NSString *)
int (^block4)(NSString *) = ^(NSString *name) {
return 2;
};

4、通常,block用作为参数传递给方法或函数。这样的情况下,通常创建一个内联块(block "inline"),而不需要声明

1
2
3
4
5
6
7
8
- (void)doSome:(NSInteger (^)(NSString *))doBlock {
NSInteger count = doBlock(@"张三");
NSLog(@"%ld", count);
}

[self doSome:^ NSInteger (NSString *str) {
return [str isEqualToString:@"张三"] ? 2 : 1;
}];

5、typedef关键字给定义的block类型取别名,定义语法如下:

1
typedef <returnType>(^<name>)(<arguments>);
1
2
3
4
5
6
typedef float(^CalculateBlock)(float, float);

CalculateBlock divBlock = ^(float a, float b) {
return a * b + a / b;
};
float res = divBlock(2.15, 3.56);

6、全局block
在文件层面,可以定义全局block字面量表达式

1
2
3
4
5
#import "ViewController.h"

NSInteger (^getCount)(NSString *) = ^ NSInteger (NSString *name) {
return [name isEqualToString:@"测试"] ? 1 : 0;
};

Block 的概念

Block对象提供了一种创建特殊函数体的方法,函数体可用CC衍生语言(如Objective-CC++)作为表达式。在其他语言和环境中,Block对象有时也称为“闭包”(closure)。

Block的功能

Block是一个匿名的内联代码块,它包含:

  • 和函数一样具有类型化的参数列表
  • 具有推断或声明的返回类型
  • 可以在它定义的词法范围内 捕获 状态
  • 可以有选择地修改词法范围的状态
  • 可以与在相同的词法范围内定义的其它 Block 共享修改状态
  • 在词法作用域(stack)被销毁后,可以继续共享和修改在词法作用域(stack)中定义的状态

可以copy一个Block,甚至将它传递给其他线程以延迟执行(或本线程的执行循环里)。编译器 和 runtime 会把Block用到的所有变量保存到Block的所有副本的生命周期后。

注意:尽管 Block 可以用于纯Cc++,但是Block也是一个Objective-C对象。

Block回调

Block 通常表示小段的,独立的代码块。因此,它们在封装并发执行的工作单元、集合中的项或在另一个操作完成时作为回调函数特别有用。

Block是传统回调函数的有用替代方法,主要有两个原因:

  • 允许你在方法调用的地方编写block代码,并稍后在方法实现的上下文中执行。
    因此,Block通常是框架方法的参数。

  • block允许访问局部变量。
    相比于回调函数需要一个在执行回调操作所需的所有上下文信息的数据结构,block可以直接访问本地变量。

block 和 变量

block使用的变量类型

Block 对象的代码体中可以引用三种标准类型的变量,就像函数传参一样:

  • 全局变量,包括静态局部变量;
  • 全局函数,(从技术上讲,它不是变量);
  • 作用域中的局部变量和参数;

Block 还支持另外两种类型的变量:

  • 在函数级别上是__block变量。 它们在 Block(和作用域)内是可变的,如果将引用的 Block 复制到堆(heap)中,则将保留它们。
  • const导入

以下规则适用于 Block 中使用的变量:

1、 可以访问全局变量,包括存在于作用域内的静态(static)变量。

2、传递给 Block 的参数是可访问的(就像函数的参数一样)。

3、词法作用域内的栈(stack,或者非静态)变量被捕获为常量(const)变量。
它们的值在程序内的Block表达式处获取。在嵌套Block中,从最近的封闭范围中捕获值。

4、__block存储修饰符声明的词法作用域内的局部变量,是传递引用的,因此是可变的。
任何更改都将反映在词法作用域内,包括在同一词法作用域内定义的任何其他block

5、block的词法作用域内声明的局部变量,其行为与函数中的局部变量完全相同。
block的每次调用都提供该变量的新副本。这些变量又可以在块中作为常量(const)使用,或在block内包含的block中的引用变量。

__block存储类型

  • 可以在 block 内部读取导入变量的值,直接修改变量的值会报错。可以通过 __block 存储类型修饰符来指定导入到block 内部的变量是可变的(即可读写)。

  • __block存储方式类似于局部变量的registerautostatic存储类型,但它们相互排斥。

  • __block变量存在于存储中,该存储在变量的词法范围与在该变量的词法范围内声明或创建的所有blockblock副本之间共享。给定词法作用域中的多个block可以同时使用一个共享变量。

注意:作为一种优化,block存储从栈(stack)开始,就像block本身一样。如果block是用Block_copy(或在Objective-C中发送copy)复制的,则会将block变量复制到堆(heap)。因此一个__block变量的地址会随时间改变。

__block变量还有两个严格的限制:它们不能是可变长度数组,也不能是包含C99可变长度数组的结构体。

1、block 内部使用 (staticnon-static__block修饰的)局部变量

  • (static__block修饰的)局部变量是引用传递;
  • non-static的局部变量是值传递;
1
2
3
4
5
6
7
8
9
10
int a = 3;
__block int b = 3;
static int c = 3;
void (^testBlock)(void) = ^{
b = b * 10;
c = c * 10;
NSLog(@"a = %d, b = %d, c = %d", a, b, c);
};
a = 5; b = 5; c = 5;
testBlock(); // a = 3, b = 50, c = 50

2、block中使用全局变量

  • block 中使用全局变量是引用传递
1
2
3
4
5
6
7
8
9
10
11
12
int a = 2;
static int b = 2;

{
void (^testBlock)(void) = ^{
a = a * 10;
b = b * 10;
NSLog(@"a = %d, b = %d", a, b);
};
a = 5; b = 5;
testBlock(); // a = 50, b = 50
}

对象和block变量

block提供对Objective-CC++对象以及其他block 作为变量的支持。

Objective-C对象

当复制block时,它会创建对在block中使用的对象变量的强引用。如果在方法的实现中使用block

  • 如果通过引用访问实例变量,则会对self进行强引用。
  • 如果按值访问实例变量,则会对该变量进行强引用。
1
2
3
4
5
6
7
8
void (^testBlock)(void) = ^{
NSLog(@"%d", self.count); // 会对 self 进行一次强引用
};

NSInteger countP = self.count;
void (^testBlock2)(void) = ^{
NSLog(@"%d", countP); // 直接使用 countP 的值,会对countP强引用
};

C++对象

一般来说,可以在block内使用C++对象。在成员函数中,对成员变量和函数的引用是通过隐式导入该指针实现的,因此看起来是可变的。如果block被复制,有两个注意事项:

  • 如果你有一个__block存储类来存储基于栈(stack)的C++对象,那么将使用常规copy构造函数。

  • 如果在block中使用任何其他基于c++栈(stack)的对象,则它必须具有constcopy构造函数。然后使用该构造函数复制c++对象。

block对象

当您复制一个block时,如果需要,将复制该block中对其他block的任何引用——可以复制整个树(从顶部开始)。如果您有block变量,并且从该block中引用了一个block,那么该block将被复制。

Block Tips

避免的模式

block字面量(即:^{ ... })是表示Block栈(stack)本地数据结构的地址。因此,栈(stack)本地数据结构的范围是封闭的复合语句,因此应避免以下示例中显示的模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void (^blockArray[3])(void);  // 定义包含3个block引用的数组

for (int i = 0; i < 3; ++i) {
blockArray[i] = ^{ printf("hello, %d\n", i); };
// 错误:block字面量的范围是 for 循环
}
blockArray[1]();

void (^block)(void);
int i = random();
if (i > 1000) {
block = ^{ printf("got i at: %d\n", i); };
// 错误:block字面量的范围是 then 子句
}

block存放区域

block runtime中,定义了6种类:

1
2
3
4
5
6
BLOCK_EXPORT void * _NSConcreteStackBlock[32];  栈上创建的block;由系统自动分配和释放,作用域执行完毕之后就会被立即释放
BLOCK_EXPORT void * _NSConcreteMallocBlock[32]; 堆上创建的block
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]; 全局变量的block,存储在.data区(数据区);直到程序结束才会被回收
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];

在MRC环境下

如果block没有引用外部的局部变量,block放在全局区
如果block引用了外部的局部变量,block就放在栈里面

MRC环境下,block只能使用copy,不能使用retain。使用retain,则block存放在栈里面。使用 copy 会把 block 从栈空间复制到堆空间。

在ARC环境下

1
2
3
4
5
6
void (^globalBlock)(void) = ^{ };
{
void (^testBlock)(void) = ^{ };

NSLog(@"%@, %@", testBlock, globalBlock); // __NSGlobalBlock__, __NSGlobalBlock__
}
1
2
3
4
5
6
7
8
9
int a = 2;
static int b = 2;
{
int c = 0;
__block int d = 0;
static int e = 0;

void (^testBlock)(void) = ^{ NSLog(@"%d", b); };
}
  • block中使用了外部的 非静态的 和 __block修饰的 局部变量, block存储在堆区;如果使用了 静态变量 和 全局变量 则存放在全局区。

  • ARC环境下,blockstrongcopy 修饰效果是一样的。

具体可以参考掘金 探寻block的本质

block循环引用(Retain Circle)

通常我们会在某个类中定义一个block类型的变量,当在该类中使用block时用到了该类的实例变量。这时候block对象会与该对象相互持有发生循环引用,引起内存泄漏的问题。如下代码所示:

1
2
3
4
5
- (void)doSome {
self.workBlock = ^{
NSLog(@"%@", self.name);
};
}

1、可以通过__weak修饰符解决循环引用:

__weak修饰符的变量被Block捕获时是对其进行弱引用持有的,可以避免循环引用。

1
2
3
4
5
6
7
8
9
10
11
// 方式一:__weak ClassName
__weak MyPerson *weakSelf = self;
self.workBlock = ^{
NSLog(@"%@", weakSelf.name);
};

// 方式二:typeof用来获取self的类型
__weak typeof(self) weakSelf = self;
self.workBlock = ^{
[weakSelf test];
};

我们通常会定义一个宏:

1
2
3
#define WEAKSELF __weak typeof(self) weakSelf = self;
// 或者
#define WEAKSELF typeof(self) __weak weakSelf = self;

2、__weak__strong

block执行时,如果__weak修饰的变量被提前释放了,则会出现野指针,导致内存泄漏。

如果不允许在block执行时self被释放,则需要在block内部使用__strong修饰一个局部变量来接收weakSelf,防止weakSelf提前释放,strongSelf会对self产生一个强引用,当block执行结束时便会释放。

1
2
3
4
5
6
7
8
9
10
11
__weak typeof(self) weakSelf = self;
_block = ^{
// strongSelf 不允许self在这个执行过程中释放
__strong typeof(weakSelf) strongSelf = weakSelf;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf);
});
};

_block();

3、@weakify、@strongify

@weakify@strongify,这两个关键字是RAC(ReactiveCocoa)中避免 Block 循环引用而开发的2个宏
其实 @weakify(self)@strongify(self) 就是比我们日常写的 weakSelfstrongSelf 多了一个 @autoreleasepool{} 而已,使用者两个宏,需要引入RACEXTScope.hmetamacros.h 源文件。

1
2
3
4
5
6
7
8
9
10
11
12
id foo = [[NSObject alloc] init];
id bar = [[NSObject alloc] init];

@weakify(foo, bar);

// this block will not keep 'foo' or 'bar' alive
BOOL (^matchesFooOrBar)(id) = ^ BOOL (id obj){
// but now, upon entry, 'foo' and 'bar' will stay alive until the block has finished executing
@strongify(foo, bar);

return [foo isEqual:obj] || [bar isEqual:obj];
};

并不是所有block里面都用weakSelf

在开发中,我们并不需要一定使用 weakSelf,这取决于是否存在循环引用。

1、系统的一些回调方法

1
2
3
[UIView animateWithDuration:0.25 animations:^{
self.view.alpha = 1.0;
}];

UIView 的某个负责动画的对象持有了 block,而block 持有了 self。因为 self 并不持有 block,所以就没有循环引用产生,因为就不需要使用 weak self 了。

2、常用的第三方库

1
2
3
[self.bgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
}];

通过查看Masonry的源代码,在执行过程中会创建MASConstraintMaker类型的变量持有 self.bgView,而这个block并没有被任何对象持有,因此在 Masnory 中使用 self.xxx 不会循环引用。


参考博客:

Blocks Programming Topics

zhangzr’s Block

HChong

掘金
一缕殇流化隐半边冰霜

深入研究 Block 用 weakSelf、strongSelf、@weakify、@strongify 解决循环引用

Reactive Cocoa中的@weakify、@strongify是如何装逼的

iOS 中的 block

文章作者: Czm
文章链接: http://yoursite.com/2020/09/22/Block/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm