[iOS] Block 的使用
前言
在做 Spotify 项目时在网络请求部分用到了很多的回调相关的知识,当时在 Block 上面有很多的疑问,以这篇博客做一个系统的学习 Block 相关的知识的回顾。
一、什么是 Block
一句话总结:Blocks 是带有 局部变量 的 匿名函数(不带名称的函数)。
Blocks 也被称作 闭包 、代码块。展开来讲,Blocks 就是一个代码块,把你想要执行的代码封装在这个代码块里,等到需要的时候再去调用。
二、Block 的基本语法
objc
// 1. 最简单的无参数无返回值的 Block
^ {
NSLog(@"Hello Block");
};
// 2. 有参数有返回值的 Block
^int (int a, int b) {
return a + b;
};
// 3. 简化写法(省略返回值类型,编译器会自动推导)
^(int a, int b) {
return a + b;
};
// 4. 如果没有参数,括号可以省略
^ {
NSLog(@"无参数 Block");
};
这就是 Block 的几种形式

然后下面是 Block 的几种类型
objc
// 1. NSGlobalBlock(全局区)
^ { NSLog(@"全局 Block"); }
// 2. NSStackBlock(栈上)------ 只在没捕获外部变量且没被强引用时存在
void (^stackBlock)(void) = ^{
NSLog(@"我是栈 Block");
};
// 3. NSMallocBlock(堆上)------ 被 copy 后就会变成这个
__block int a = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"捕获了变量 a = %d", a);
};
但一般情况下只关心 NSMallocBlock,因为只有它才能在超出作用域后继续存活。
下面是 Block 的声明定义还有 typedef
objc
// 声明一个返回 void,参数为 int 的 Block 类型
typedef void (^MyBlock)(int);
// 使用
MyBlock block = ^(int count) {
NSLog(@"count = %d", count);
};
block(100);
// 更复杂的例子
typedef NSString * _Nullable (^StringTransformBlock)(NSString * _Nonnull input);
StringTransformBlock transform = ^NSString *(NSString *input) {
return [input stringByAppendingString:@"!!!"];
};
三、Block 捕获外部的变量
下面就是 Block 外部变量相关的图表
| 外部变量类型 | 默认捕获方式 | 是否能在 Block 内修改 | 说明 |
|---|---|---|---|
| 普通局部变量 (int a) | 值捕获(copy) | 不能直接修改 | 只能读 |
| static 变量 | 指针捕获 | 可以修改 | 因为本身在数据区 |
| 全局变量 | 不捕获,直接访问 | 可以修改 | 全局可见 |
| __block 修饰的变量 | 指针捕获(特殊对象) | 可以在 Block 内修改 | 必须加 __block |
| 对象类型(NSString*) | 强引用捕获(copy) | 不能改变指针指向,但能调用方法 | 想改指针要加 __block |
objc
int val = 100;
__block int blockVal = 200;
NSString *str = @"origin";
__block NSString *blockStr = @"block";
void (^testBlock)(void) = ^{
NSLog(@"val = %d", val); // 100,值捕获
// val = 300; // 编译错误,不能改
blockVal = 999;
NSLog(@"blockVal = %d", blockVal); // 999
NSLog(@"str = %@", str);
// str = @"new"; // 编译错误
NSLog(@"blockStr = %@", blockStr);
blockStr = @"changed"; // 可以实现,因为加了 __block
};
testBlock();
下面就是打印结果

四、Block 的循环引用相关的知识
在这里我们会介绍到三个修饰符,分别是__ weak, __ __ strong , block。
__ weak
__ weak ,这个修饰符只能在ARC 模式下使用,他只能修饰对象,比如NSString 等,不能修饰基本的数据类型,同时__ weak 修饰的 Block 不可被重新赋值。
objc
self.block = ^{
NSLog(@"%@", self.title); // self -> block -> self
};
这就是一个典型的循环引用现象,你可能会好奇他为什么会是循环引用,不就是读取了一个属性值吗?
下面是详细的解释
在这里self.block 肯定是一个属性,他的声明方式肯定是长这个样子的
objc
@property (nonatomic, copy) void (^block)(void);
然后因为他是 copy 属性,所以当我们开始执行这行代码的时候
objc
self.block = ^ {...};
他等价于
objc
self->_block = [^{...} copy]; // Block 从栈 copy 到堆
Block 被 copy 到堆上的同时,会把外部用到的对象(这里是 self)强引用一次(因为对象默认是按值捕获,也就是强引用捕获)。
所以现在的情况是:
- self 强引用了 block(因为 self 有一个 copy 属性的 ivar 指向这个 Block)
- block 也强引用了 self(因为 Block 内部捕获了 self)
形成了一个环形强引用
这样就会导致一个现象就是两个对象互相强引用,谁都释放不了导致内存泄漏。
所以这时我们就有一个方法来防止内存泄露
那就是用__ __weak__修饰符来修饰 self
objc
__weak typeof(self) weakSelf = self;
这样就可以防止强引用。
__ strong
__ strong ,这个修饰符有一个特点他一般要和__ weak__一起使用,因为他要先用 __weak__打破循环,然后再局部强引用防止被释放。
objc
__weak typeof(self) weakSelf = self; // 第1步:打破 retain cycle
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf; // 第2步:局部强引用
if (!strongSelf) return;
// 安全使用 strongSelf
};
weakSelf 是为了block不持有self,避免Retain Circle循环引用。 在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。
strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,不会存在循环引用问题。 如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。
__ block
__ block__是一个和 strong__还有 __weak__完全不一样的修饰词。他主要专注于外部就比如下面的这个例子
objc
// 例子1:累加器
__block int count = 0;
void (^counter)(void) = ^{
count++;
NSLog(@"第 %d 次", count);
};
counter(); // 1
counter(); // 2

加了 __block:变量变成一个"包装对象",Block 和外面拿的是同一个对象变成了可以改的。
小结
这篇博客只是对 Block 浅尝辄止的了解,还有很多深层次的底层原理没有了解到位,后续的博客都会进行完善的。