Objective-C Block初探
1. 概述
一个闭包包含:
- 一个函数(或指向函数的指针)
- 该函数执行时需要的上下文变量
在Objective-C中,Block就是闭包。
Block有以下特点:
- 可以嵌套定义,定义Block方法和定义函数方法类似(所以也常称Block为匿名函数)
- Block可以定义在方法的内部或者外部
- 和函数很像,Block()调用时才会执行内部的方法
- Block的本质是对象,使代码高聚合
2. Block的定义
很多人觉得Block的定义很怪异,很难记住。但其实和C语言的函数指针的定义对比一下,你很容易就可以记住。
// Block
returnType (^blockName)(parameterTypes)
// 函数指针
returnType (*c_func)(parameterTypes)
例如输入和返回参数都是字符串:
(char *) (*c_func)(const char *);
(NSString *) (^block)(NSString *);
-
本地变量
returnType (^blockName)(parameterTypes) = ^returnType(parameters) { ... };
-
属性
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
-
方法参数
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
-
方法调用的参数
[someObject someMethodThatTakesABlock:^returnType (parameters) { ... }];
-
C函数的参数
void SomeFunctionThatTakesABlock(returnType (^blockName)(parameterTypes));
-
typedef
typedef returnType (^TypeName)(parameterTypes); TypeName blockName = ^returnType(parameters) {...};
3. Block与外部变量
3.1 捕获自动变量(局部变量)
typedef void (^MyBlock)(void);
默认情况
对于Block外部的变量,Block默认是将其复制到数据结构中来实现访问的:
- Block内未使用外部变量,不发生捕获
- Block内部使用了外部变量,发生捕获,Block结构体重会存储相应的数据,导致其体积增大。
默认情况下Block只访问局部变量的值,而不需要修改它。
int age = 10;
MyBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
//输出
10
__block修饰
如果需要修改外部变量的值,则需要对变量使用__block进行修饰。此时,Block会复制变量的引用地址来实现访问,所以可以修改外部的变量值。
__block int age = 10;
MyBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
//输出
18
一句话就是:没有__block修饰是值引用,有__block修饰是指针引用。
3.2 __block修饰原理
为什么使__block修饰就可以修改变量值呢?我们创建一个main.m
文件。
#import <Foundation/Foundation.h>
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[])
{
@autoreleasepool
{
__block int age = 10;
MyBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
}
return 0;
}
然后在终端中输入:
clang -rewrite-objc main.m
会生成一个main.cpp
文件。
__block int age = 10;
...
age = 18;
//转换为
__Block_byref_age_0 age = {
0,
&age,
0,
sizeof(__Block_byref_age_0),
10
};
...
age.__forwarding->age = 18;
我们发现__block修饰之后,局部变量age
变成了__Block_byref_age_0结构体类型变量实例。
现在我们要访问age
变量,则需要通过__forwarding成员变量来间接访问。
4. Block的copy操作
4.1 Block的存储域
Block是存储在栈上还是堆上呢?
其实,Block有三种不同的类型:
- 全局块(_NSConcreteGlobalBlock),存在于全局内存中,相当于单例。
- 栈块(_NSConcreteStackBlock),存在于栈内存中,超出作用域后会被销毁。
- 堆块(_NSConcreteMallocBlock),存在于堆内存中,是一个带引用计数的对象,需要自己管理内存。
看到一个Block,我们如何确认Block的存储区域呢?
Block不访问外界变量(包括栈和堆中的变量)
MRC和ARC中都一样,Block在代码段中,即全区块。
Block访问外界变量
MRC:默认存储在栈区。
ARC:默认存储在栈区,然后ARC情况下,按需自动拷贝到了堆区,自动释放。
4.2 Block的copy操作
那么问题来了:为什么ARC下,访问外界变量的Block会自动拷贝到堆区呢?
栈块存在于栈内存中,超出作用域后会被销毁。使用__block的变量也会被回收。为了解决这个问题,需要把栈块复制到堆上,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断,是否需要将Block从栈复制到堆。如果需要,自动生成复制代码。Block的复制操作执行的是copy实例方法。Block调用了copy
方法就会从栈块变成堆块。
例如下面这个返回类型为blk_t
Block的函数:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) { return rate * count; };
}
返回之前的Block是在栈上的,返回之后作用域结束,会被释放。ARC开启时,编译器就会自动完成复制。而MRC时,需要我们执行copy
方法手动执行。
将Block从栈上拷贝到堆上相当消耗CPU,所以Block在栈上够用,就不要执行复制了。不然就会浪费CPU资源。
Block的复制实际是调用copy
实例方法,不同类型的Block执行copy
效果也稍微有些不同。
Block类 | 副本源的存储域 | 复制效果 |
---|---|---|
_NSConcreteGlobalBlock | 程序的数据区 | 什么也不做 |
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
虽然栈块也是对象,但它没有引用计数。因为栈区的内存由编译器自动分配释放,不需要引用计数。
4.3 Block的__forwarding
在copy
之后,Block变量也会被复制到堆上。那么访问变量是访问栈上的还是堆上的呢?之前看到的__block变量的__forwarding就是解决这个问题的。
通过__forwarding,无论是在Block中还是Block外访问__block变量,也不管该变量是在栈上或者堆上,都可以顺利访问同一个__block变量。
5. 防止Block循环引用
某个类将Block作为自己的变量,同时又在Block内部使用了类实例对象本身,就会出现循环引用:
self.someBlock = ^{
[self doSomething];
}
即自己持有Block,Block又捕获了自己,造成循环引用。
-
MRC,使用
__block
__block typeof(self) blockSelf = self; self.someBlock = ^{ [blockSelf doSomething]; }
-
ARC,使用
__weak
__weak typeof(self) weakSelf = self; self.someBlock = ^{ [weakSelf doSomething]; }
注意:在ARC中使用
__block
也可能出现循环应用。typedef void(^Block)(void); @interface SomeObj: NSObject @property (nonatomic, copy) Block block; @end @implementation SomeObj - (instancetype)init { self = [super init]; if (self) { __block typeof(self) blockSelf = self; self.block = ^{ NSLog(@"Self: %@", blockSelf); blockSelf = nil; }; } return self; } - (void)execBlock { self.block(); } @end //使用类 SomeObj *obj = [[SomeObj alloc] init]; [obj execBlock]; //如果不执行execBlock,blockSelf永远不会置空,循环引用一直存在。