神奇的Block
神奇的Block
本文不做Block
的基本介绍和底层实现原理,有兴趣的同学直接戳这篇文章,写得灰常好,本文只在应用层面上带领读者进行思考,并整理出一些结论.这些结论是我从书上和上网资料收集所得,并通过实践进行验证而来,希望能和高手们共同探讨 :)
在看例子之前,至少要知道block有几个类型.
- _NSConcreteGlobalBlock(全局块)
- _NSConcreteStackBlock(栈块)
- _NSConcreteMallocBlock(堆块)
废话不说,直接看例子.测试环境为ARC,就不做MRC的测试了.
精神病入门
例子一:
typedef void (^blk_t) ();
int main(int argc, const char * argv[]) {
blk_t block = ^{
printf("I'm just a block\n");
};
block();
return 0;
}
很简单的一段代码,执行block
之后结果是I'm just a block
.但如果问你,这个block
是什么类型的block
,你会怎么回答?
在代码中打一个断点,通过打印block
的isa
,可以知道该block
是什么类型的.
第一步:打个断点
第二步:打印isa
然后就能看到结果了:
看到结果,尼玛居然是个全局块
,可是我明明是在栈上创建的一个block
呀!
再来看一个例子,这时定义了一个局部变量,并在block
中使用了这个局部变量.
例子二:
typedef void (^blk_t) ();
int main(int argc, const char * argv[]) {
int i = 1;
blk_t block = ^{
printf("%d\n",i);
};
block();
return 0;
}
按照以上步骤再看看block
的isa
.
……我去,怎么成堆块
了?
别急,再举个🌰.
例子三:
typedef void (^blk_t) ();
int main(int argc, const char * argv[]) {
int i = 1;
__weak blk_t block = ^{
printf("%d\n",i);
};
block();
return 0;
}
虽然编译器在__weak blk_t block = ^{
这行爆出了警告,但是程序还是能够正常运行.block
并未因为一个弱引用立即释放.然后看看结果:
终于看到
栈块
了,全家福终于齐人了.
下面开始总结了.
在哪些情况下,Block
为_NSConcreteGlobalBlock
类对象?
-
记述全局变量的地方创建的
Block
,比如下面的例子.blk_t block = ^{ printf("I'm just a block"); }; int main(int argc, const char * argv[]) { block(); return 0; }
-
不截获自动变量的时候.
即
例子一
这种情况下.虽然是在栈上创建的一个block
,但由于闭包内不截获外部的自动变量(局部变量),将会被编译器编译为_NSConcreteGlobalBlock
.
再来总结一下第二个例子.之所以是一个堆块,是因为编译器为块进行了copy
操作(实质上是调用_Block_copy函数).以下方式会让块从栈复制到堆上.
-
调用
Block
的copy
实例方法.[^{ printf("a heap block"); } copy]; // 对block调用copy,会把栈上的block复制到堆上.
-
将
Block
赋值给附有__strong
修饰符id
类型的类或Block
类型成员变量时.也就是说,有个__strong修饰的变量指向这个block就会让编译器为block调用copy方法.
例子二就是将Block赋值给了一个
__strong
(默认都是strong)修饰的Block
类型成员变量——blk_t block
.因此,在例子三中,我将强引用变成弱引用,创建了一个栈上的block.虽然编译器会有警告,因为编译器在这里可能还不知道那个块也是栈上的,而这个栈上的块,显然不会立即释放.
-
Block
作为函数返回值时例如:
blk_t return_A_Block(){ int val = 10; return ^{NSLog(@"%d",val);}; } int main(int argc, const char * argv[]) { NSLog(@"%@",return_A_Block()); return 0; }
打印所得是一个堆块.
Block
的副本
Block的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
精神病进阶
例子四:
typedef void (^blk_t) (id obj);
int main(int argc, const char * argv[]) {
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"%ld",[array count]);
};
}
// array超出了作用域,在括号外已经不能被使用了
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
return 0;
}
打印台输出结果:
可以看到,在超出了作用域后,array
依旧能够被访问到.
例子五:
int main(int argc, const char * argv[]) {
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
id __weak array2 = array;
blk = ^(id obj){
[array2 addObject:obj];
NSLog(@"%ld",[array2 count]);
};
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
return 0;
}
打印台结果:
大相径庭的结果.
在例子四中,Block
中截获了外部的自动变量,并且根据上面说过的结论,编译器为我们调用了copy
方法,这个Block
是个堆块.
我们将例子四稍加改写,将块改为栈块(即不让编译器为我们调用copy
方法):
int main(int argc, const char * argv[]) {
__weak blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"%ld",[array count]);
};
}
blk([NSObject new]);
blk([NSObject new]);
blk([NSObject new]);
return 0;
}
打印出来的结果和例子五一致.
从该例子得出的结论是:
- 只有调用了
Block
的copy
方法,才能持有截获的附有__strong
修饰符的对象类型的自动变量值.
基于这个结论,我们还可以得出,在ARC环境下,定义block
类型的属性时,可以用strong
,并不是非得用copy
才是正确的.
// 两者效果一样
@property (strong, nonatomic) blk_t *block;
@property (copy, nonatomic) blk_t *block;
在例子五中,虽然截获的自动变量是__weak
修饰符修饰的对象类型.但是作用域过后array
被释放,nil
被赋值给了array2
,并不能持有对象.这让我们想起了平时为了防止循环引用,我们会用一个弱指针指向self
,并让block
捕获弱指针而不是让block
持有self
.
注意,即使你不使用
self.object
访问实例变量,而是通过_object
访问,也同样会造成循环引用.因为无论用什么形式访问实例变量,经过编译后,最终都会转换成self+变量内存偏移的形式
来进行访问,还是会造成循环引用.
那么,截获和__block修饰有何不同呢?
block本质也是一个结构体,截获的对象会成为结构体成员的一部分.
例如:
int main(int argc, const char * argv[]) {
id obj;
^{
obj;
};
return 0;
}
其中生成的block会是这样子的:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
id __strong obj;
};
注:在C语言中,结构体不能含有附有
__strong
修饰的变量.因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存.而OC却可以,它能够准确的把握block从栈复制到堆以及堆上的block被废弃的时机.
如果是通过__block
修饰的一个变量呢?
int main(int argc, const char * argv[]) {
__block int a;
^{
a = 10;
};
return 0;
}
其中生成的block会是这样子的:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
};
此时,被__block
修饰的变量变成了一个结构体(__Block_byref_a_0
类型).至于这个结构体又长什么样,就不贴代码了,只需知道a
被包装到了这个结构体中,成为其中一个成员变量,其他成员变量描述了该结构体的一些信息.
- 当
Block
被拷贝到堆上的时候,附有__strong
修饰的变量因为Block
结构体内有强指针持有,使得该指针所指向的对象在作用域外还有引用计数,因此存活着. - 当
Block
被拷贝到堆上的时候,被__block
修饰的变量被包装
到了一个新的结构体中,被block
结构体持有,该结构体跟随Block
也被拷贝到堆上了. - 截获的方式并不能修改截获的变量本身,而
__block
修饰的方式却可以,因为它本质是复制了一份该变量.
根据结论,可以知道用__block
修饰的方式也能够避免循环引用.只要在块中将需要避免循环引用的�变量置为nil
.
如:
- (id)init{
self = [super init];
__block id tmp = self;
blk_t blk = ^{
tmp.name = @"ye";
tmp = nil;
}
return self;
}
如果最后不置为nil,那么
self
持有block结构体
,block结构体
持有__block变量结构体
,__block变量结构体
持有self
,只有在最后将_block变量结构体
中的self
置空,才能手动破除循环.这个方式比weak方式优点的地方在于可控制对象的持有期间.