赞
踩
Blocks是C语言的扩充功能,可以用一句话概括:带有自动变量的匿名函数。
匿名函数就是不带名称的函数,这在C语言函数中是不被允许的,但block就可以实现这个功能。
下面是一个block
^{printf(fmt, val);};
实际上上面的例子使用了省略方法,完整形式如下:
^void (void){printf(fmt, val);};
完整的block语法和c函数相比较有两个不同之处:
1.没有函数名。
2.返回值类型前带有"^"
(插入记号,caret)记号。
下面是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作为返回值:
//blk func() {
// return ^(void){
// printf("!");
// };
//}
直接使用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中体现为“截获自动变量值”。
示例如下:
//Block截获自动变量值
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中调用自动变量,那么我们尝试一下去修改这些变量的值。
int val = 0;
void (^blk) (void) = ^{ val = 1;};
blk();
printf("%d", val);
看上去很合理,但实际会报错,如果想在block中给语法外的自动变量赋值,那么在定义自动变量时,应该加上__block说明符,就可以实现赋值,如下:
// 倘如要在block中修改自动变量,应该加上__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) = ^{
// array = [[NSMutableArray alloc] init];
};
可以看到这里是错误的,正确写法和上面一样,加上说明符。
这里提一下截获c风格数组如下:
如果不使用指针,截获自动变量的方法并没有实现对C语言数组的截获,使用指针可以解决这一问题。
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++代码:
//经过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; //构造函数 初始化对象 //参数1 fp:函数指针 //参数2 desc:作为静态全局变量初始化的 __main_block_desc_0 结构体实例 指针 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { //这里说明一下,Block就是OC对象 impl.isa = &_NSConcreteStackBlock; //先理解为block的类型 impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
这里加入了构造函数所以看着有些多
其实也就是两个结构体。
第一个结构体:__block_impl
struct __block_impl {
void *isa; //Block类型
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) {
// 设置了Block类型
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); // 这里的两个参数第一个为 转换后匿名函数的地址 // 第二个为作为静态全局变量初始化的_main_block_desc_0指针 struct __main_block_impl_0 *blk = &tmp; // 这样就容易理解了。该源代码将__main_block_impl0结构体类型的自动变量,即栈上生成的__main_block_impl_0 结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量 blk。 // 对应下面的源代码 void(^blk)(void)=^{printf("Block\n");}; // 下面为__main_block_desc_0 结构体实例的初始化部分代码。 static struct __main_block_desc_0 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)// block实例大小 }; // 下面我们看一下blk();部分 // 转化后代码 ((void (*)(struct __block_impl *))( (struct __block_impl *)blk)->FuncPtr)((struct_block_impl *)blk); // 去掉转换: (*blk->impl.FuncPtr)(blk); // 这就是简单地使用函数指针调用函数。正如我们刚才所确认的,由Block 语法转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了,__main_block_func_0函数的参数__cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行了传递。
之前提到了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说明符,如前所述,因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。
解决这个问题有两种方法。第一种: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 中向静态变量赋值时,使用了指向该静态变量的指针。而向__block变量赋值要比这个更为复杂。Block 的__main_block_impl_0 结构体实例持有指向__block 变量的__Block_ byref_val_0 结构体实例的指针。 __Block_byref_val_0 结构体实例的成员变量__forwarding 持有指向该实例自身的指针。通过成员变量__forwarding 访问成员变量val。(成员变量 val 是该实例自身持有的变量,它相当于原自动变量。) */ __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变量也同 Block一样变成__Block_byref_val_0结构体类型的自动变量,同时该结构体持有相当于原自动变量的成员变量。*/ __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
变量。
这里解释一下之前的ias指针。
通过之前的说明可知 Block也是Objective-C 对象。将Block当作Objective-C对象来看时,该Block的类为_NSConcreteStackBlock。虽然该类并没有出现在已变换源代码中,但有很多与之类似的类。
首先,我们能够注意到_NSConcreteStackBlock 类的名称中含有“栈”(stack)一词,即该类的对象Block设置在栈上。
同样地,_NSConcreteGlobalBlock类对象如其名“全局”(global)所示,与全局变量一样,设置在程序的数据区域(.data区)中。
NSConcreteMallocBlock类对象则设置在由malloc 函数分配的内存块(即堆)中。
这里内存分区可以参考一下我之前写的博客:iOS内存分区
到现在为止出现的Block例子使用的都是_NSConcreteStackBlock 类,且都设置在栈上。但实际上并非全是这样,在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象,如:
void (^blk)(void) = ^{printf("Global Block\n");}
int main() {
// 这里的isa指针就指向_NSConcreteGlobalBlock;
该 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 为_NSConcreteGlobalBlock 类对象。即Block配置在程序的数据域中。除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
那么将 Block配置在堆上的_NSConcreteMallocBlock类在何时使用呢?
下面先解释一下:
配置在全局变量
上的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环境下编译器会进行判断,以下情况会自动复制
usingBlock
等时,GCD的API__strong修饰符的id类型 或 Block类型
的成员变量时- (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变量产生的影响:
若在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的执行部分超出其变量作用域而存在。
通过编译器转换后的源代码如下:
/*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 };
/*Block语法,使用Block部分 */
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 会复制到堆呢?
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中使用附有__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 类对象的状态下会引起以下循环引用。
如果不执行 execBlock 实例方法,就会持续该循环引用从而造成内存泄漏。
通过执行 execBlock实例方法,Block 被实行,nil 被赋值在__block变量tmp中。
blk_ = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
因此,__block 变量 tmp 对 MyObject 类对象的强引用失效。避免循环引用的过程如下所示:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。