block
文章目录
- block
- 什么是block?
- block语法
- Block变量
- 截获自动变量值
- __block说明符
- 截获的自动变量
- block的三种存储类型
- NSGlobalBlock
- NSStackBlock
- NSMallocBlock
- block的父类
- block循环引用
- 未完待续······
什么是block?
Blocks是带有自动变量(局部变量)的匿名函数。
block语法
标准格式:
例子:
^int (int count) {
return count + 1;
}
返回值类型默认void,如果是void 我们也可以默认省略(void)。
Block变量
在c中,我们可以将函数地址赋值给函数指针类型,同理,Block语法也可以赋值给Block类型的变量,声明Block类型变量示例如下:
int (^blk) (int);
Block类型变量和一般变量完全相同,可以作为以下用途:
- 自动变量
- 函数参数
- 静态变量
- 静态全局变量
- 全局变量
将Block赋值为Block类型变量:
void (^blk) (void) = ^{
};
函数中Block作为参数:
int func(int (^blk) (int));
在函数返回值中指定Block类型,可以将Block作为返回值:
blk func() {
return ^(void){
printf("!");
};
}
截获自动变量值
通过Block语法和Block类型变量的说明,我们己经理解了“ 带有自动变量值的匿名函数” 中的“匿名函数”。而“带有自动变量值” 究竟是什么呢?“带有自动变量值” 在Blocks中表现 为“ 截获自动变量值”。截获自动变量值的实例如下:
void interceptAutomaticVariable() { //截获自动变量
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
}
该源代码中,Block 语法的表达式使用的是它之前声明的自动变量fmt 和val 。Blocks 中, Block 表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block 表达式保存 了自动变量的值,所以在执行Block 语法后,即使改写Block 中使用的自动变量的值也不会影响 Block 执行时自动变量的值。该源代码就在Block语法后改写了Block中的自动变量val 和fmt。 下面我们一起看一下执行结果:
执行结果并不是改写后的值“These values were changed. val= 2”,而是执行Block语法时的自动变量的瞬间值。该Block语法在执行时,字符串指针“val = %d” 被赋值到自动变量fmt中,int 值10被赋值到自动变量val 中,因此这些值被保存(即被截获),从而在执行块时使用。
__block说明符
实际上,自动变量值截获只能保存执行Block 语法瞬间的值。保存后就不能改写该值。下面我们来尝试改写截获的自动变量值,看看会出现什么结果。下面的源代码中,Block 语法之前声明的自动变量val 的值被赋予1。
void the__block() { //__block测试
int val = 0;
void (^blk) (void) = ^{
val = 1;
};
blk();
printf("val = %d\n", val);
}
该代码会出现报错:
这时候就需要给val加上__block关键字了。
void the__block() { //__block测试
__block int val = 0;
void (^blk) (void) = ^{
val = 1;
};
blk();
printf("val = %d\n", val);
}
输出结果:
使用附有__block 说明符的自动变量可在Block 中赋值,该变量称为__block 变量。
截获的自动变量
那么截获Objective-C 对象,调用变更该对象的方法也会产生编译错误吗?
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
这是没有问题的,而向截获的变量array 赋值则会产生编译错误。该源代码中截获的变量值 为NSMutableArray 类的对象。如果用C语言来描述,即是截获NSMutableArray类对象用的结构体实例指针。虽然赋值给截获的自动变量array 的操作会产生编译错误,但使用截获的值却不会有任何问题。下面源代码向截获的自动变量进行赋值,因此会产生编译错误。
id array2 = [[NSMutableArray alloc] init];
void (^blk2) (void) = ^ {
array2 = array;
};
这时候就会有和之前类似的错误:
block的三种存储类型
- _NSConcreteStackBlock(栈区)
- _NSConcreteGlobalBlock(数据区)
- _NSConcreteMallocBlock(堆区)
那么三种储存方式有什么区别呢?
简单来说,没有捕获自动变量的就是数据区,捕获了自动变量但是没有进行copy操作就是栈区,copy之后就变成了堆区。
NSGlobalBlock
如果一个 block 没有访问外部局部变量,或者访问的是全局变量,或者静态局部变量,此时的 block 就是一个全局 block ,并且数据存储在全局区。
//block1没有引用到局部变量
int a = 10;
void (^block1)(void) = ^{
NSLog(@"hello world");
};
NSLog(@"block1:%@", block1);
// block2中引入的是静态变量
static int a1 = 20;
void (^block2)(void) = ^{
NSLog(@"hello - %d",a1);
};
NSLog(@"block2:%@", block2);
NSStackBlock
int b = 10;
NSLog(@"%@", ^{
NSLog(@"hello - %d",b);
});
输出结果:
我们神奇的发现,按照概念上所说写出的为什么不是NSStackBlock呢,这个问题困扰了我很久,经过查阅了很多资料后明白,在ARC环境下,系统会自动将block进行拷贝操作,我们改成MRC试试,果然,结果正确了。
那么在ARC下怎么让block存储在栈上呢?
int c = 10;
NSLog(@"%@", (__weak)^{
NSLog(@"hello - %d",c);
});
我们可以这么做,在之前我们学习过weak关键字,weak关键字的作用是弱引用避免循环引用
我们来看下面一段话:
基本可以明白,weak避免了循环引用。
除此之外还发现了一个神奇的问题:
我们分别打印这四个值:
int b = 10;
NSLog(@"%@", ^{
NSLog(@"hello - %d",b);
});
int c = 10;
NSLog(@"%@", (__weak)^{
NSLog(@"hello - %d",c);
});
int a = 10;
NSLog(@"%@", [^{
NSLog(@"hello - %d",a);
} description]);
int age = 15;
NSLog(@"%@", [^{
NSLog(@"block----%d", age);
} class]);
这个东西真的蛮抽象的,只有第一个打印了malloc类型,其他都是stack类型,这又是什么原因呢?
这个问题我也解释不太清楚,等后面学清楚了再来补充,敬请期待······
NSMallocBlock
int a = 10;
void (^block1)(void) = ^{
NSLog(@"%d",a);
};
NSLog(@"block1:%@", [block1 copy]);
__block int b = 10;
void (^block2)(void) = ^{
NSLog(@"%d",b);
};
NSLog(@"block2:%@", [block2 copy]);
输出结果:
block的父类
void (^block1)(void) = ^{
NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
block循环引用
如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block 从栈复制到堆时,该对象为Block 所持有。这样容易引起循环引用。我们来看看下面的源代码:
typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{
NSLog(@"self = %@", self);
};
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
int main() {
id o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
该源代码中MyObject 类的 dealloc 实例方法一定没有被调用。
MyObject 类对象的 Block 类型成员变量blk_持有赋值为Block 的强引用。即 MyObject 类对象持有Block。init 实例方法中执行的 Block 语法使用附有__strong 修饰符的 id 类型变量 self。并且由于Block 语法赋值在了成员变量blk_中,因此通过Block 语法生成在栈上的 Block 此时由栈复制到堆,并持有所使用的self。self持有Block,Block 持有 self。这正是循环引用。
为避免此循环引用,可声明附有__weak 修饰符的变量,并将 self 赋值使用。
-(id)init {
self = [super init];
id __weak tmp = self;
blk_ = ^{
NSLog(@"self = %@",tmp);
};
return self;
}
在该源代码中,由于Block存在时,持有该Block 的 MyObject 类对象即赋值在变量tmp中的 self 必定存在,因此不需要判断变量 tmp 的值是否为nil。
另外,还可以使用__block变量来避免循环引用。
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
blk_();
}
-(void)dealloc {
NSLog(@"dealloc");
}
@end
int main() {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
该源代码没有引起循环引用。但是如果不调用execBlock实例方法,即不执行赋值给成员变量blk_的 Block,便会循环引用并引起内存泄漏。在生成并持有MyObject 类对象的状态下会引起以下循环引用。
- MyObject 类对象持有Block
- Block 持有__block变量
- __block变量持有MyObject类对象
如果不执行 execBlock 实例方法,就会持续该循环引用从而造成内存泄漏。
通过执行 execBlock实例方法,Block 被实行,nil 被赋值在__block变量tmp中。
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
因此,__block 变量 tmp 对 MyObject 类对象的强引用失效。避免循环引用的过程如下所示:
- MyObject类对象持有 Block
- Block 持有__block变量
下面我们对使用block变量避免循环引用的方法和使用__weak修饰符及__unsafe_unretained 修饰符避免循环引用的方法做个比较。
使用__block变量的优点如下: - 通过__block 变量可控制对象的持有期间
在不能使用__weak修饰符的环境中使用__unsafe_unretained 修饰符即可(不必担心悬垂指针) - 在执行 Block 时可动态地决定是否将 nil 或其他对象赋值在__block 变量中。使用__block变量的缺点如下:
- 为避免循环引用必须执行Block
存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用__block变量、__weak 修饰符或__unsafe_unretained修饰符来避免循环引用。
关于block的知识很重要也很繁杂,包括书上一些源码和前面遇到的一些问题目前还没能太理解,后续会继续补充。