Blocks?
Blocks是C语言的扩充功能,可以用一句话概括:带有自动变量的匿名函数。 匿名函数就是不带名称的函数,这在C语言函数中是不被允许的,但block就可以实现这个功能。
Blocks模式
Blocks语法
下面是一个block
^{printf(fmt, val);};
实际上上面的例子使用了省略方法,完整形式如下:
^void (void){printf(fmt, val);};
完整的block语法和c函数相比较有两个不同之处: 1.没有函数名。 2.返回值类型前带有"^" (插入记号,caret)记号。 下面是Block语法格式:^ 返回值类型 参数列表 表达式 其中返回值类型和参数列表都是可以省略的。
Block类型变量
在c中,我们可以将函数地址赋值给函数指针类型,同理,Block语法也可以赋值给Block类型的变量,声明Block类型变量示例如下:
int (^blk) (int);
Block类型变量和一般变量完全相同,可以作为以下用途:
自动变量 函数参数 静态变量 静态全局变量 全局变量
将Block赋值为Block类型变量:
void (^blk) (void) = ^{printf(fmt, val);};
函数中Block作为参数:
int func(int (^blk) (int));
在函数返回值中指定Block类型,可以将Block作为返回值:
直接使用block类型变量作为返回值和参数记述比较复杂,还会有bug 这里盲猜是因为编译器无法识别前面整体的block类型。 如果我们使用typedef就可以解决这个问题。
typedef int (^blk) (int);
blk testBlk = ^(int count){
return count + 1;
};
blk func(int (^blk) (int));
blk func() {
return ^(int count){
return 1;
};
}
定义完之后可以这样调用:int ans = blk(10); .
截获自动变量值
之前了解到Blocks是一个带有自动变量的匿名函数,那么我们解析一下自动变量,带有自动变量在Blocks中体现为“截获自动变量值”。 示例如下:
int dmy = 256;
int val = 10;
const char* fmt = "val = %d\n";
val = 3;
void (^blk) (void) = ^{printf(fmt, val);};
val = 2;
fmt = "val changed = %d\n";
blk();
打印结果是val = 3; 而不是我们修改后的val changed = 2; 这就是Block截获自动变量,当Blocks执行时,使用的是自动变量的瞬间值,Block保存了这个值,在执行语句时会使用保存的值。
__block说明符
那么我们既然可以在block中调用自动变量,那么我们尝试一下去修改这些变量的值。
int val = 0;
void (^blk) (void) = ^{ val = 1;};
blk();
printf("%d", val);
看上去很合理,但实际会报错,如果想在block中给语法外的自动变量赋值,那么在定义自动变量时,应该加上__block说明符,就可以实现赋值,如下:
__block int val = 0;
void (^blk) (void) = ^{ val = 1;};
blk();
printf("%d", val);
这样就可以修改值了,加入该说明符的变量被称为__block变量。
截获的自动变量
上面我们尝试改变一个语法外的自动变量,不加说明符时会出现问题,那么我们尝试一下截取OC对象。
id array = [[NSMutableArray alloc] init];
__block id array2 = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
这里我们可以调用对象的方法,试一试赋值:
void (^blk1) (void) = ^{
};
可以看到这里是错误的,正确写法和上面一样,加上说明符。
这里提一下截获c风格数组如下: 如果不使用指针,截获自动变量的方法并没有实现对C语言数组的截获,使用指针可以解决这一问题。
Blocks的实现
block实质
Block究竟是什么呢?他的本质是什么? block其实本质上是被作为很简单的c语言来实现的,clang(LLVM编译器)具有转换为我们可读源代码的功能。我们可以通过“-rewrite-objc”选项就能将含有 Block 语法的源代码变换为C++的源代码。说是C++,其实也仅是使用了struct 结构,其本质是C语言源代码。 先简单些一个Block;
int main() {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
经过clang转化后的c++代码:
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(int argc, const char * argv[]) {
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
return 0;
}
短短的几行代码,经过转化后变成了几十行,但其实无非是加入了一些结构体,我们逐步分析一下。 先看一下比较像的地方。
^{
printf("Block\n");
};
转化后为:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
这里我们可以观察到通过Blocks使用的匿名函数实际上被作为简单的C语言函数来处理了。 该函数的参数__cself相当于C++实例方法中所指的自身变量this,或是OC实例方法中指向对象自身的变量self,即参数__cself为指向Block值的变量。 下面我们看看struct __main_block_impl_0* __cself 这个参数,这个参数是一个结构体指针,该结构体声明如下:
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;
}
};
这里加入了构造函数所以看着有些多 其实也就是两个结构体。 第一个结构体:__block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
第二个结构体:__main_block_desc_0
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block的大小。 那么,下面我们来看看初始化含有这些结构体的__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;
}
下面看一下main函数里面关于block的代码:
nt main(int argc, const char * argv[]) {
void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
return 0;
}
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;
void(^blk)(void)=^{printf("Block\n");};
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct_block_impl *)blk);
(*blk->impl.FuncPtr)(blk);
截获自动变量实现
之前提到了Block可以截获自动变量,那么转化后到底是如何实现的 下面是转化后的代码:
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;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself {
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
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(int argc, const char * argv[]) {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
return 0;
}
这与前面转换的源代码稍有差异。下面来看看其中的不同之处。首先我们注意到,Block语法表达式中使用的自动变量被作为成员变量追加到了__main_block_impl_0结构体中。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
}
同样的在构造函数也多加入了参数去初始化结构体实例:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
fmt = "val = %d\n";
val = 10;
初始化时对fmt和val进行了赋值。由此可知,在__main_block_impl_0结构体实例中(即Block),自动变量被截获。 再看一下使用Block的匿名函数的实现:
^{printf(fmt, val)};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
转换后的源代码中,截获到__main_block_impl_0 结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义。因此,原来的源代码表达式无需改动便可使用截获的自动变量值执行。 总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(即Block 自身)中。
__block说明符
之前提到想要修改自动变量的值就需要加上__block说明符,如前所述,因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。 解决这个问题有两种方法。第一种:C语言中有一个变量,允许Block改写值。具体如下:
- 静态变量
- 静态全局变量
- 全局变量
虽然Block语法的匿名函数部分简单地变换为了C语言函数,但从这个变换的函数中访问静态全局变量/全局变量并没有任何改变,可直接使用。 但是静态变量的情况下,转换后的函数原本就设置在含有Block语法的函数外,所以无法从变量作用域访问。 我们来看看下面这段源代码。
int global_val = 1;
static int static_global_val = 2;
int main()
{
static int static_val = 3;
void (^blk)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
};
return 0;
}
源代码使用了Block改写静态变量static_val 、静态全局变量static_global_val 和全局变量 global_val 。该源代码转换后如下:
int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val;
global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;
}
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct__main_block_impl_0)
};
int main() {
static int static val = 3;
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
return 0;
}
其实这里的全局变量和静态全局变量都很好理解,那么静态变量是如何修改的?
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val;
(*static_val) *= 3;
}
使用静态变量static_val的指针对其进行访问。将静态变量static_val的指针传递给main_block_impl_0结构体的构造函数并保存。这是超出作用域使用变量的最简单方法。 静态变量的这种方法似乎也适用于自动变量的访问。但是我们为什么没有这么做呢? 实际上,在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃,因此 Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。这些在下节详细说明。 解决Block中不能保存值这一问题的第二种方法是使用“__block说明符”。更准确的表述方式为“__block存储域类说明符”(__block storage-class-specifier)。
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
该源代码转化后如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block impl_0 {
struct __block_impl impl;
struct __main block desc 0* Desc;
__Block_byref_val_0 *val;
__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr=fp;
Desc = desc;
};
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {
__Block_byref_val_0 *val =__cself->val;
(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_imp1_0*src) {
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
}__main_block_desc_0_DATA = {
0,
sizeof(structmain_block_impl_0),
__main_block_copy_O,
__main_block_dispose_0
};
int main() {
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
blk = &__mainblock_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
另外,__block变量的__Block_byref_val_0 结构体并不在 Block 用__main_block_impl_0 结构体中,这样做是为了在多个Block 中使用__block 变量。我们看一下下面的源代码。
__block int val = 10;
void (^blk0)(void) = ^{val = 0;};
void (^blk1)(void) = ^{val = 1;};
__Block_byref_val_0 val = {0, &val, 0, sizeof(_Block_byref_val_0), 10};
blk0 = &__main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA, &val, 0x22000000);
blkl = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &val, 0x22000000);
两个Block 都使用了__Block_byref_val_0 结构体实例val 的指针。这样一来就可以从多个 Block 中使用同一个__block 变量。当然,反过来从一个 Block中使用多个__block 变量也是可以的。只要增加 Block 的结构体成员变量与构造函数的参数,便可对应使用多个__block 变量。
Block存储域
这里解释一下之前的ias指针。 通过之前的说明可知 Block也是Objective-C 对象。将Block当作Objective-C对象来看时,该Block的类为_NSConcreteStackBlock。虽然该类并没有出现在已变换源代码中,但有很多与之类似的类。
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
首先,我们能够注意到_NSConcreteStackBlock 类的名称中含有“栈”(stack)一词,即该类的对象Block设置在栈上。 同样地,_NSConcreteGlobalBlock类对象如其名“全局”(global)所示,与全局变量一样,设置在程序的数据区域(.data区)中。 NSConcreteMallocBlock类对象则设置在由malloc 函数分配的内存块(即堆)中。 这里内存分区可以参考一下我之前写的博客:iOS内存分区 到现在为止出现的Block例子使用的都是_NSConcreteStackBlock 类,且都设置在栈上。但实际上并非全是这样,在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象,如:
void (^blk)(void) = ^{printf("Global Block\n");}
int main() {
该 Block 的类为_NSConcreteGlobalBlock 类。此 Block 即该 Block 用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将Block用结构体实例设置在与全局变量相同的数据区域中即可。 只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。例如以下源代码中,虽然多次使用同一个Block语法,但每个for循环中截获的自动变量的值都不同。
typedef int(^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
blk_t blk = ^(int count) {return rate * count;};
}
也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用应截获的自动变量时
在以上这些情况下,Block 为_NSConcreteGlobalBlock 类对象。即Block配置在程序的数据域中。除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。 那么将 Block配置在堆上的_NSConcreteMallocBlock类在何时使用呢? 下面先解释一下:
- Block 超出变量作用域可存在的原因。
- __block变量用结构体成员变量__forwarding 存在的原因
配置在全局变量 上的Block,从变量作用域外也可以通过指针安全地使用。但设置在栈上 的 Block,如果其所属的变量作用域结束,该 Block 就被废弃。由于__block 变量 也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block 变量 也会被废弃。 变量作用域结束后,栈上的__block变量和Block也被废弃。 Blocks 提供了将Block和__block变量从栈上复制到堆上 的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在。 复制到堆上的Block将_NSConcreteMallocBlock 类对象写入 Block 用结构体实例的成员变量isa。
impl.isa = &_NSConcreteMallocBlock;
而__block 变量 用结构体成员变量__forwarding 可以实现无论__block 变量配置在栈上还是堆上时都能够正确地访问__block变量 。 只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block 变量还是从堆上的__block 变量都能够正确访问。 Block copy: 在ARC环境下编译器会进行判断,以下情况会自动复制
- Block作为函数返回值返回
- Cocoa框架的方法且方法名中含有
usingBlock 等时,GCD的API - 将Block赋值给类的附有
__strong修饰符的id类型 或 Block类型 的成员变量时 其余的情况我们要自行判断是否添加copy,例如下面的例子:
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:
[^{NSLog(@"blk:%d", val);} copy],
[^{NSLog(@"blk1:%d",val);} copy], nil];
}
id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk =(blk_t)[obj objectAtIndex:0];
blk();
虽然看起来有点奇怪,但像这样,对于Block 语法可直接调用copy方法。当然对于Block类型变量也可以调用copy方法。
typedef int (^blk_t)(int);
blk_t blk = ^(int count){return rate * count;};
blk = [blk copy];
另外,对于已配置在堆上的Block以及配置在程序的数据区域上的Block,调用copy方法又会如何呢?下面按配置Block的存储域,将copy方法进行复制的动作总结了出来。
不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。
__block变量存储域
Block从栈复制到堆时对__block变量产生的影响: 若在1个Block中使用__block变量,则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部被从栈复制到堆。此时,Block 持有__block 变量。即使在该Block已复制到堆的情形下,复制Block也对所使用的__block变量没有任何影响。
在多个 Block 中使用__block 变量时,因为最先会将所有的 Block 配置在栈上,所以__block变量也会配置在栈上。在任何一个 Block 从栈复制到堆时,__block 变量也会一并从栈复制到堆并被该 Block所持有。当剩下的 Block 从栈复制到堆时,被复制的 Block 持有__block 变量,并增加__block变量的引用计数。 如果配置在堆上的Block被废弃,那么它所使用的 __block变量也就被释放。 到这里我们可以看出,此思考方式与OC 的引用计数式内存管理完全相同。使用 block变量的 Block 持有__block 变量。如果 Block 被废弃,它所持有的__block变量也就被释放。
__forwarding 指针
了解了__block的存储域之后,对于__block变量所转换的结构体中的__forwarding指针,之前说他是指向自身的指针。 栈上的__block变量在__block变量从栈上复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量的结构体实例的地址。 通过该功能,无论是在 Block 语法中、Block 语法外使用__block变量,还是__block 变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。
截获对象
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
变量作用域结束的同时,变量array 被废弃,其强引用失效,因此赋值给变量array的 NSMutableArray 类的对象必定被释放并废弃。但是该源代码运行正常,其执行结果如下:
array count = 1
array count = 2
array count = 3
这一结果意味着赋值给变量array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。 通过编译器转换后的源代码如下:
struct __main_block_impl_0 {
struct __block_impl_impl;
struct __main_block_desc_0* Desc;
id __strong array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}
static void __main_block_copy_0(struct __main_block_impl_o *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void_main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
static struct __main block desc_0 {
unsigned long reserved;
unsigned long Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0),
__main_block_copy_0,
__main_block_dispose_0
};
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
blk = &_main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
在Objective-C中,C语言结构体不能含有附有strong修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。 但是Obiective-C的运行时库能够准确把握Block从栈复制到堆以及堆上的Block 被废弃的时机,因此Block 用结构体中即使含有附有__strong修饰符或__weak 修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy 和 dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和_main_block_dispose_0函数。 由于在该源代码的Block用结构体中,含有附有__strong修饰符的对象类型变量array,所以需要恰当管理赋值给变量array的对象。因此__main_block_copy_0 函数使用_Block_object_assign 函数将对象类型对象赋值给Block用结构体的成员变量array中并持有该对象。
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
_Block_object_assign 函数调用相当于retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。 另外,__main_block_dispose_0 函数使用_Block_object_dispose 函数,释放赋值在 Block 用结构体成员变量array中的对象。
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
_Block_object_dispose 函数调用相当于 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。 虽然此__main_block_copy_0 函数(以下简称 copy 函数)和__main_block_dispose_0 函数(以下简称 dispose 函数)指针被赋值在__main_block_desc_0 结构体成员变量copy 和 dispose 中,但在转换后的源代码中,这些函数包括使用指针全都没有被调用。那么这些函数是从哪调用呢? 在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。 调用copy函数和dispose 函数的时机: 那么什么时候栈上的 Block 会复制到堆呢?
- 调用Block的copy实例方法时 Block作为函数返回值返回时
- 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
- 在方法名中含有usingBlock的 Cocoa 框架方法或 Grand Central
Dispatch 的 API 中传递 Block 时在调用Block的 copy实例方法 时,如果Block配置在栈上,那么该Block 会从栈复制到堆。Block作为函数返回值返回时、将Block赋值给附有__strong 修饰符id类型的类或Block类型员变量时,编译器自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法的效果相同。在方法名中含有usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的API 中传递 Block 时,在该方法或函数内部对传递过来的Block 调用 Block 的 copy 实例方法或者_Block_copy函数。 也就是说,虽然从源代码来看,在上面这些情况下栈上的Block 被复制到堆上,但其实可归结为_Block_copy函数被调用时 Block 从栈复制到堆。 相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的 dealloc 实例方法。 有了这种构造,通过使用附有__strong修饰符的自动变量,Block 中截获的对象就能够超出其变量作用域而存在。 虽然这种使用copy函数和dispose函数的方法在前面没做任何说明,但实际上在使用 block变量时已经用到了。
static void __main_block_copy0(
struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
转换后的源代码在Block用结构体的部分基本相同,其不同之处: 截获对象时和使用__block变量时的不同
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修饰符来避免循环引用。
|