标签:
Blocks是C语言的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓的匿名函数就是不带有名称的函数。c语言的标准不允许存在这样的函数。例如:
int func (int count);
int resule = func(10);
如果想使用函数指针来代替直接调用函数,那么似乎不用知道函数名也能够使用该函数
int func (int count);
int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
C语言函数中可能使用的变量:
(1)、^ 返回值类型 参数列表 表达式;
^void (int i){ printf("%d", i);}
(2)、^ 参数列表 表达式;
^(int i){printf("%d", i);}
(3)、^ 表达式;
^{printf("123");}
当你省略返回值类型的时候,你的表达式里return返回什么类型,那么你的返回值类型就是什么。
当你不适用参数的时候,(void) 参数列表可以省略。
int (^myBlock) (int) = ^(int num){
return num * num;
};
说明:
(1)、使用block的时候,我们可以声明block变量,他同c中的函数指针:
int f(int a) {
return a;
}
int (*fa)(int) = &f;
(2)、使用Block语法将Block赋值为Block类型变量;
int (^blk)(int) = ^(int a){return a;};
(3)、由^开始的Block语法生成的Block被赋值给变量blk中。因为与通常的变量相同,所有当然也可以由Block类型变量向Block类型变量赋值;
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;
(4)、在函数参数中使用Block类型的变量可以向函数传递Block
void func (int (^blk) (int)){
NSLog(@"%d",blk(1));
}
(5)、在函数返回值中指定Block类型,可以将Block作为函数的返回值返回
int (^funk())(int){
return ^(int count){return count + 1;};
}
由此可见,在函数参数和返回值中使用Block类型变量时,记述方式极为复杂。这时,我们可以像使用函数指针类型时那样,使用typedef来解决该问题:
typedef int (^blk_t)(int);
- (void)viewDidLoad {
[super viewDidLoad];
int myCount = 1;
int (^blk)(void) = ^{
NSLog(@"%d",myCount);
return myCount;
};
myCount ++;
blk();
NSLog(@"%d",myCount);
}
打印结果如下:
2016-05-20 15:26:43.499 class_01[42917:1552743] 1
2016-05-20 15:26:43.500 class_01[42917:1552743] 2
Blocks中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写Block中使用的自动变量的值也不会影响Block执行自动变量的值。
实际上,自动变量值截获只能保存执行Block语法的瞬间的值。保存后就不能改写该值。若尝试改写截获的自动变量值,会出现编译错误。如下:
int val = 0;
void (^blk)(void) = ^{
val = 1;
}
若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block说明符。
- (void)viewDidLoad {
[super viewDidLoad];
__block int myCount = 1;
int (^blk)(void) = ^{
NSLog(@"%d",myCount);
myCount = 4;
return myCount;
};
myCount ++;
blk();
NSLog(@"%d",myCount);
}
2016-05-20 15:33:30.154 class_01[42969:1556701] 2
2016-05-20 15:33:30.155 class_01[42969:1556701] 4
注意:
使用附有__block说明符的自动变量可在Block中赋值,该变量称为__block变量。
由上述可得如果将值赋值给Block中截获的自动变量,就会产生编译;
如果截获Objective-C对象,调用变更该对象的方法如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray* arr = [NSMutableArray array];
void (^blk)(void) = ^{
[arr addObject:@"1"];
};
blk();
NSLog(@"%@",arr);
}
打印:
2016-05-20 16:10:02.051 class_01[43053:1570116] (
1
)
arr是一个指针,指向一个可变长度的数组。在block里面,并没有修改这个指针,而是修改了这个指针指向的数组。换句话说,arr是一个整数,保存的是一块内存区域的地址,在block里,并没有改变这个地址,而是读取出这个地址,然后去操作这块地址控件的内容。这是允许的,因为声明的block的时候实际上是把当时的临时变量又复制了一份,在block里即使修改这些复制的变量,也不影响外面的原始变量,这就是所谓的闭包。但是当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。所以在block里面对指针指向内容做的修改,再block外面也一样生效。
Blocks默认不能修改相同作用域范围内的变量,但是如果这个相同作用域的变量如果使用了__block关键字进行修饰,则可以通过blocks进行修改。
Block是”带有自动变量值的匿名函数”。
在实际编译时无法转换成我们能够理解的源代码,但是clang(LLVM编译器)具有转换为我们可读源代码的功能。通过”-rewrite-objc”选项就能将含有Block语法的源代码转换为C++的源代码。
首先进入该源文件的绝对路径;
clang -rewrite-objc 源代码文件名
我们先来转换一个简单的block代码:
int main() {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
生成一个block.cpp的文件
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
可以通过转换后的源代码看到,通过blocks使用的匿名函数,实际上被作为简单的c语言函数来处理。
在转换后的代码的命名上,是根据block语法所属的函数名和该block语法在该函数出现的顺序值。
来看block的语法:
^{printf();};
对应的是
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf();
}
这里的__cself相当于c++中的this。
函数参数声明:struct __main_block_impl_0 *__cself
与c++的this和oc的self相同,参数__cself 是 __main_block_impl_0结构体的指针。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
这里是取出构造函数的代码,
其中impl是__block_impl结构体。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
Desc是指针,是__main_block_desc_0结构体的。
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
下面初始化含有这些结构体的__main_block_impl_0结构体的构造函数;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
_NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。
main中构造函数是如何调用的:
void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
拆分:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
这样就容易理解了。 该源代码在栈上生成__main_block_impl_0结构体实例的指针。我们将栈上生成的__main_block_impl_0结构体实例的指针赋值给__main_block_impl_0结构体指针类型的变量blk;
注意到生成tmp的函数的参数, 第一个参数是由block语法转换的c语言函数指针,第二个参数是作为静态全局变量初始化的__main_block_desc_0结构体实例指针。
看一下结构体初始化:
__main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
这里使用__main_block_impl_0结构体实例的大小进行初始化。
下面看看栈上的_main_block_impl_0结构体实例(即Block)是如何根据这些参数进行初始化的。如果展开__main_block_impl_0结构体的__block_impl结构体,如下:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
该结构体根据构造函数:
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
Block中所使用的被截获自动变量就如:带有自动变量值的匿名函数。 所说的,仅截获自动变量的值。
Block中使用自动变量后,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。
如果在Block中视图改变自动变量,将引起编译错误。
但是这样,我们就不能再block中保存值了。
解决方案有两种方法:
(1)、c语言中有一个变量,允许Block改写值。具体如下:
在Block中,访问静态全局变量/全局变量没有什么改变,可以直接使用。
静态变量中,转换后的函数原本就设置在含有block语法的函数外,所以无法从变量作用域访问。
(2)、 使用__block说明符
__block存储域类说明符 __block storage-class-specifier
c中的存储域类说明符:
__block说明符类似于static、auto和register,它们用于作为静态变量值设置到那个存储区域中。例如,auto表示作为自动变量存储在栈中,static表示作为静态变量存储再数据区中。
Block和__block变量的实质就是在栈上的结构体实例。Block转换为Block的结构体型的自动变量,__block变量转换为__block变量的结构体类型的自动变量。所谓的结构体类型的自动变量,即栈上生成的该结构体的实例;
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例 |
__block变量 | 栈上__block变量的结构体实例 |
其中Block也是oc的对象,该OC的类为:_NSConreteStackBlock。
类似的类还有:
类 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) |
_NSConcreteMallocBlock | 堆 |
void (^blk)(void) = ^{
printf("Global Block\n");
};
该Block的类为_NSConcreteGlobalBlock类。此Block即该Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Block用结构体实例的内容不依赖执行时的状态,所以整个程序只需要一个实例。因此将Block用结构体实例设置在于全局变量相同的数据区域中即可。
只在截获自动变量时,Block用结构体实例截获的值才会根据执行时的状态变化。
只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
在以下情况Block为_NSConcreteGlobalBlock类对象:
NSArray *testArr = @[@"1", @"2"];
void (^TestBlock)(void) = ^{
NSLog(@"testArr :%@", testArr);
};
需要截获自动变量的为_NSConcreteStackBlock类对象,换句话说:
除_NSConcreteGlobalBlock的Block语法生成的Block都为_NSConcreteStackBlock类对象;
分配在栈上的Block和__block变量 其所属的变量作用域结束,该Block或者__block变量也会被废弃。
但是Blocks提供了 将Block和__block变量从栈上复制到堆上的方法来解决这个问题, 这样即使语法记述其作用域结束,堆上的Block也能继续存在。
如图:
此时,赋值到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa;
impl.isa = _NSConcreteMallocBlock;
上面的情况下只是Block,而__block变量需要用结构体成员变量__forwarding可以实现 无论__block变量配置在栈上还是堆上时都能够正确地访问__block变量。
在下面我们会说:在__block变量配置在堆上的状态下,也可以访问栈上的__block变量。此时,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,不管是从栈上的__block变量还是从堆上的__block变量都能正确访问。
Blocks提供的复制方法,如何从栈上复制到堆上的? ARC有效的时候,编译器可以自动判断。
来看Block函数:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate*count;};
}
此时将会返回配置在栈上的Block的函数。 即 程序执行中,从该函数返回函数调用方时变量作用域结束,因此栈上的Block也被废弃。 虽然有问题,但是ARC的编译如下:
blk_t func(int rate) {
//将通过Block语法生成的Block, 即配置在栈上的Block用结构体实例, 赋值给相当于Block类型的变量tmp中,此处的tmp相当于blk_t __strong tmp;
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
//_Block_copy函数 将栈上的Block复制到堆上,复制后,将堆上的地址作为指针赋值给变量tmp,此处的objc_retainBlock相当于_Block_copy
tmp = objc_retainBlock(tmp);
//将堆上的Block作为OC对象, 注册到autoreleasepool中,然后返回该对象。
return objc_autoreleaseReturnValue(tmp);
}
将Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码。
对于配置在堆上的Block以及配置在程序的数据区域上的Block,调用copy方法如下:
Block的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
注意:
Block从栈复制到堆上时,对__block变量产生的影响
__block变量的配置存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
(1)、如果一个Block中使用__block变量,当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部被从栈复制到堆上。 此时Block持有__block变量。 即使在该Block已复制到堆的情况下,复制Block也对所使用的__block变量没有任何影响。
(2)、在多个Block中使用__block变量时,因为最先会将所有的Block配置在栈上,所以__block变量也会配置在栈上。在任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block所持有。当剩下的Block从栈复制到堆时,被复制的Block持有__block变量,并增加__block变量的引用计数;
(3)、如果配置在堆上的Block被弃用,那么它所使用的__block变量也就被释放。
什么时候栈上的Block赋值到堆?
在调用Block的copy的时候,如果Block配置在栈上,那么该Block会从栈复制到堆。 当Block作为函数返回值的时候,将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量的时候,编译器会自动将对象的Block作为参数,并调用_Block_copy函数。这与调用Block的copy实例方法效果相同。在方法中含有usingBlock的Cocoa框架方法或Crand Central Dispatch的API中传递Block时,在该方法或函数内部对传递过来的Block调用Block的copy实例方法或者_Block_copy函数。
也就是说,虽然从源代码来看,在上面这些情况下栈上的Block被复制到了堆上, 但其实可归结为_Block_copy函数被调用时Block从栈复制到堆。
相反,释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数, 相当于对象的dealloc实例方法。
这样,通过使用附有__strong修饰符的自动变量,Block中截获的对象就能超出其变量作用域存在了。
截获对象和使用__block变量时的不同:
对象:BLOCK_FIELD_IS_OBJECT
__block变量:BLOCK_FIELD_IS_BYREF
通过标志来区分是对象还是__block变量。
但是与copy函数持有截获的对象,dispose函数释放截获的对象相同,copy函数持有所使用的__block比那里,dispose函数释放所使用的__block变量。
因此,Block中使用的赋值给附有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可超出其变量作用域而存在。
在Block中使用对象类型自动变量时,除了以下3种情况,推荐调用Block的copy实例方法。
__block id obj = [[NSObject alloc]init];
clang 转化为
//__block变量用结构体部分
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
__strong id obj;
};
static void __Block_byref_id_object_copy_131(void* dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void**)((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void**)((vhar*)src + 40), 131);
}
//__block变量声明部分
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
ARC有效时:
当Block从栈复制到堆时,使用_Block_object_assign函数,持有Block截获的对象。 当堆上的Block被废弃时,使用_Block_object_dispose函数,释放Block截获的对象;
在__block变量为附有__strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block变量从栈复制到堆上,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。
由此可见,即使对象复制到堆上的附有__strong修饰符的对象类型__block变量中,只要__block变量在堆上存在,那么该对象就会继续处于被持有状态。这与Block中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。
ARC有效时:
当我们在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;
}
下面代码也会引起循环:
@interface MyObject:NSObject {
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id) init {
self = [super init];
blk_ = ^{NSLog(@"obj_ = %@", obj_);};
return self;
}
@end
Block中没有self,也同样截获了self,引起循环。
其实在block语法中使用了obj_,其实就已经截获了self: self->obj_。
与前面一样,我们可以声明__weak的临时变量来避免循环引用;
我们还可以使用__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,便会循环引用并引起内存泄露。原因如下:
①、使用__block变量的优点:
②、使用__block变量的缺点
标签:
原文地址:http://blog.csdn.net/xiaoxiaobukuang/article/details/51455405