block是将函数及其执行上下文封装起来的对象。
Objective-C的函数式编程也是通过Block实现的,Block的作用与函数类似,其使用更加灵活,可以像变量一样进行传递。
Block的定义与函数非常类似,一个完整的block结构如下:
ReturnType (^name)(params)
其中ReturnType为返回值的类型,name为Block变量的名字,params为参数列表。
例如:
NSInteger num = 3;
NSInteger (^block)(NSInteger) = ^NSInteger(NSInteger n) {
return num * n;
};
block(2);
通过clang -rewrite-objc BlockTest.m
命令编译该.m文件,发现该Block被编译成如下形式:
NSInteger num = 3;
NSInteger (*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, num));
((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);
其中BlockTest是文件名,test是方法名,可以忽略。
__BlockTest__test_block_impl_0
结构体为:
struct __BlockTest__test_block_impl_0 {
struct __block_impl impl;
struct __BlockTest__test_block_desc_0* Desc;
NSInteger num;
__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__block_impl
结构体为:
struct __block_impl {
void *isa; // isa指针,所以Block是对象
int Flags;
int Reserved;
void *FuncPtr; // 函数指针
};
Block内部则为:
static NSInteger __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself, NSInteger n) {
NSInteger num = __cself->num; // bound by copy
return num * n;
}
所以说Block是将函数及其执行上下文封装起来的对象,既然Block内部封装了函数,那么它同样也有参数和返回值。
一、Block类型
使用下面的代码可以将block对象所属的类以及继承链中的类打印出来:
- (void)viewDidLoad {
[super viewDidLoad];
void (^b) (void) = ^() {
NSLog(@"Block");
};
b();
id c = [b class];
while (c) {
NSLog(@"%@", c);
c = [c superclass];
}
}
控制台输出如下:
2021-09-04 16:02:29.204551+0800 MyProject[4277:94703] Block
2021-09-04 16:02:29.204679+0800 MyProject[4277:94703] __NSGlobalBlock__
2021-09-04 16:02:29.204818+0800 MyProject[4277:94703] NSBlock
2021-09-04 16:02:29.204928+0800 MyProject[4277:94703] NSObject
可以发现,Block最终是继承自NSObject,与普通的Objective-C对象并无本质区别。
- Block分为全局Block(_NSConcreateGlobalBlock)、栈Block(_NSConcreateStackBlock)、堆Block(_NSConcreteMallocBlock)三种类型。
其中全局Block存储在已初始化数据(.data)区,栈Block存储在栈(stack)区,堆Block存储在堆(heap)区。
栈Block被存放在内存区域中的栈区。当一个作用域结束后,与之相关的栈中的数据都会被清理,因此对于栈Block,超出了其所在的作用域就被会回收。
堆Block与Objective-C对象一样,内存是否释放会受到引用计数的管理。当对一个栈Block进行copy操作时,就会创建出堆Block。
在不同场景创建的Block,其类型也会不同。
1.不使用外部变量的Block是全局Block
不管Block对象本身是局部的还是全局的,只要没有使用外部变量,即为全局Block,例如:
@implementation ViewController
int count = 10;
void (^b) (void);
- (void)viewDidLoad {
[super viewDidLoad];
static NSString *s = @"hello";
b = ^() {
NSLog(@"%d, %@", count, s);
};
b();
}
@end
2.使用外部变量并且未进行copy操作的Block是栈Block
例如:
NSInteger num = 5;
NSLog(@"%@", [^{
NSLog(@"block test:%zd", num);
} class]);
输出__NSStackBlock__
。
日常开发,常用于如下情况:
[self testWithBlock:^{
NSLog(@"%@", self);
}];
- (void)testWithBlock:(dispatch_block_t)block {
block();
NSLog(@"%@", [block class]);
}
如果在Block内部有使用到局部变量,则此时创建的Block为栈Block。注意,在ARC环境下,编译器会自动对栈Block进行复制操作,使其变成堆Block,可以在MRC环境下测试。
将Xcode的编译选项修改为非ARC环境,编写如下测试代码:
@implementation ViewController
void (^b) (void);
- (void)viewDidLoad {
[super viewDidLoad];
NSString *s = @"hello";
b = ^() {
NSLog(@"%@", s);
};
b();
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
b();
}
@end
3.对栈Block进行copy操作,就是堆Block
示例如下:
NSInteger num = 5;
// 赋值即进行copy操作
void(^block)(void) = ^{
NSLog(@"block test:%zd", num);
};
NSLog(@"%@", [block class]);
输出为__NSMallocBlock__
。
在MRC环境下运行上面的代码会产生野指针异常,由于我们在创建Block对象时使用了局部变量string,因此该Block会被存储在栈内。当viewDidLoad方法结束时,其中的变量都会被回收,此栈Block也会销毁,虽然使用的全局指针,但是此时指针变成野指针,在viewWillAppear方法中再次调用该Block会产生异常。
要解决上面的野指针问题其实也非常容易,只需要对栈Block进行一次copy操作即可(ARC环境下,编译器会帮助我们做这个copy操作),此时Block就变成堆Block,例如:
@implementation ViewController
void (^b) (void);
- (void)viewDidLoad {
[super viewDidLoad];
NSString *s = @"hello";
b = [^() {
NSLog(@"%@", s);
} copy];
b();
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
b();
}
@end
对栈Block进行copy后,栈Block并不会消失,被copy的仍然是栈Block,示例如下:
[self testWithBlock:^{
NSLog(@"%@", self);
}];
- (void)testWithBlock:(dispatch_block_t)block {
block();
dispatch_block_t tempBlock = block;
NSLog(@"%@, %@", [block class], [tempBlock class]);
}
输出如下:
2021-10-21 16:06:39.463203+0800 Test[5562:75981] <ViewController: 0x7fd74880ba80>
2021-10-21 16:06:39.463333+0800 Test[5562:75981] __NSStackBlock__, __NSMallocBlock__
需要注意的是,如果对全局Block进行copy操作,得到的仍然是全局Block。
总结:
对栈Block进行copy,将会copy到堆区;
对堆Block进行copy,将会增加其引用计数;
对全局Block进行copy,因为已经是初始化的,所以不做任何处理,仍是全局Block。
二、Block中变量的截获
在Block中如果使用到了外部的变量,则会对外部的变量进行截获。在不同的场景下,Block对变量的截获方式也会不同。
如果在Block中使用了全局变量或静态变量,Block会直接对其进行访问,并不会做其他额外的操作,例如:
#import "ViewController.h"
int a = 10;
@interface ViewController ()
@property (nonatomic, copy) void (^block) (void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%p", &a);
self.block = ^() {
NSLog(@"%p", &a);
a = 20;
};
self.block();
NSLog(@"%d", a);
}
@end
运行上面的代码,控制台打印如下:
2021-10-20 15:54:17.231457+0800 Test[50301:863187] 0x1095f15a0
2021-10-20 15:54:17.231600+0800 Test[50301:863187] 0x1095f15a0
2021-10-20 15:54:17.231709+0800 Test[50301:863187] 20
从打印信息可以看到,在Block外部和内部使用的全局变量a的地址相同,所以他们实际是同一个变量,在Block内部可以对其进行访问和修改。Block内部并不会对所有用到的变量进行复制,例如全局变量或静态变量就不会复制。
如果在Block中使用到了局部变量(也称为自动变量),则Block会对其进行复制:如果所使用到的变量是值类型的,则会直接复制值;如果所使用的变量是引用类型的,则会复制引用。示例如下:
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
NSArray *array = @[[NSObject new]];
NSLog(@"%p, %p", &a, &array);
self.block = ^() {
NSLog(@"%p, %p", &a, &array);
};
self.block();
}
运行代码,控制台输出如下:
2021-10-20 16:04:30.045456+0800 Test[50653:871056] 0x7ffeed28304c, 0x7ffeed283040
2021-10-20 16:04:30.045604+0800 Test[50653:871056] 0x600001c26488, 0x600001c26480
在Block内部尝试对外部的局部变量进行修改时,会产生编译错误,这是编译器为我们提供的一种错误预警,因为在Block内部的变量已经和外部的变量不再是同一个变量。注意,这里说的不可修改是指变量本身,对于变量引用的对象,如果对象可以修改(例如NSMutableArray类型),在Block内部依然可以对其进行操作。
在ARC环境下,栈Block会被自动复制成堆Block。在这种情况下,被截获的指针变量也会根据修饰符的情况进行内存管理,对于__weak修饰的变量,Block内部不会对其进行强引用,它的引用计数不变,但当外部变量被释放后,Block内部的变量也将不可用。对于__strong修饰的变量,则Block内部会对其也进行强引用,增加其引用计数,当Block对象本身被释放时,其内部截获的这类变量也都会被调用release和释放。
1.局部变量截获,是值截获
例如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n) {
return n*num; // 已经截获到num值为3,相当于 return n*3
};
num = 1; // 对于Block内部的num无效
NSLog(@"%zd", block(2));
这里输出的是6而不是2,原因就是对局部变量num的截获是值截获。
NSMutableArray *array = [NSMutableArray arrayWithObjects:@"1", @"2", nil];
void(^block)(void) = ^{
NSLog(@"%@", array);
[array addObject:@"4"];
};
[array addObject:@"3"]; // 值截获,此处值变了
array = nil; // 值截获,置nil无效,其他修改也无效,如:array = [NSMutableArray arrayWithObjects:@"5", @"6", nil];
block();
打印结果为(
1,
2,
3
)
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置nil,对Block没有影响,而该对象调用方法会影响。
2.局部静态变量截获,是指针截获
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n) {
return n*num;
};
num = 1;
NSLog(@"%zd", block(2));
输出为2,意味着num = 1
这里的修改是有效的,即指针截获。同样,在block内部去修改变量num,也是有效的。
3.全局变量、全局静态变量,不截获,直接取值
static NSInteger num3 = 3;
NSInteger num4 = 4;
- (void)test {
NSInteger num1 = 1;
static NSInteger num2 = 2;
__block NSInteger num5 = 5;
void(^block)(void) = ^{
NSLog(@"%zd", num1); // 局部变量
NSLog(@"%zd", num2); // 静态变量
NSLog(@"%zd", num3); // 全局静态变量
NSLog(@"%zd", num4); // 全局变量
NSLog(@"%zd", num5); // __block修饰变量
};
block();
}
将上面代码用clang编译如下:
struct __BlockTest__test_block_impl_0 {
struct __block_impl impl;
struct __BlockTest__test_block_desc_0* Desc;
NSInteger num1; // 局部变量
NSInteger *num2; // 静态变量
__Block_byref_num5_0 *num5; // by ref // __block修饰变量
__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, NSInteger _num1, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num1(_num1), num2(_num2), num5(_num5->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
impl.isa = &_NSConcreteStackBlock;
这句说明,该block是栈block。
可以看到局部变量被编译成值形式,而静态变量被编译成指针形式,全局变量并未截获。而__block修饰的变量也是以指针形式截获的,并且生成了一个新的结构体对象:
struct __Block_byref_num5_0 {
void *__isa;
__Block_byref_num5_0 *__forwarding;
int __flags;
int __size;
NSInteger num5;
};
该对象有个属性:num5,即我们用__block修饰的变量。
这里__forwarding是指向自身的(栈block)。
一般情况下,如果我们要对block截获的局部变量进行复制操作,需要添加__block修饰符,而对全局变量、静态变量是不需要添加__block修饰符的。
另外,block里访问self或成员变量都会去截获self。
三、__block关键字
在Block内部对外部的局部变量本身进行修改是不被允许的,如果想要在Block内部修改外部的局部变量,只需要将局部变量声明成__block类型即可,示例如下:
- (void)viewDidLoad {
[super viewDidLoad];
__block int a = 10;
self.block = ^() {
a = 20;
};
self.block();
NSLog(@"%d", a); // 20
}
对于使用__block修饰的变量,实际上会被包装成对象,这种操作有些类似于使用指针实现对值类型变量的修改,使用下面的代码也可以实现Block内部修改外部局部变量的值:
- (void)viewDidLoad {
[super viewDidLoad];
int *a = malloc(sizeof(int));
*a = 10;
NSLog(@"%p, %p", a, &a);
self.block = ^() {
*a = 20;
NSLog(@"%p, %p", a, &a);
};
self.block();
NSLog(@"%d", *a);
free(a);
}
运行代码,控制台输出如下:
2021-10-20 16:18:02.356189+0800 Test[51124:880533] 0x600000c04600, 0x7ffee7f31058
2021-10-20 16:18:02.356360+0800 Test[51124:880533] 0x600000c04600, 0x60000001e510
2021-10-20 16:18:02.356484+0800 Test[51124:880533] 20
__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,对__block的修改,实际上是在修改堆上的__block.
所以,__forwarding指针存在的意义就是,无论在任何内存位置,都可以顺利访问同一个__block变量。
另外,由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block持有__block变量,而__block变量持有self,造成内存泄露。
例如:
__block typeof(self) weakSelf = self;
_testBlock = ^{
NSLog(@"%@", weakSelf);
};
_testBlock();
要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完weakSelf后,将其置nil。但是这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。
所以,最好还是用__weak来修饰self。