文章目录
之前写过Blocks,
Block的实质
先写一个简单的block
int main() {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
该代码转化为C++可变换为以下形式:
struct __block_impl { //block的一些基础属性,像是block的基类
void *isa; //block存放位置,取值为_NSConcretGlobalBlock(全局区)、_NSConcretStackBlock(栈区)、_NSConcretMallocBlock(堆区)
int Flags; //用于按bit位表示一些block的附加信息,block copy的实现代码可以看到对该变量的使用。
int Reserved; //保留变量
void *FuncPtr;
}
struct __main_block_impl_0 { //block变量 命名规则:“main”为block所在函数名,如果定义在函数外为block的名字,末尾的“0”表示为当前函数中第“0”个block
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;
}
};
//struct __main_block_impl_0 简化以后为
/*
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
size_t reserved;
size_t Block_size;
__main_block_impl_0(#block匿名函数的指针#,#__main_block_desc_0的实例指针#, int flags=0);
};
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //block的匿名函数。存放block内的语句
printf("Block\n");
}
static struct __main_block_desc_0 { //block的描述,他有一个实例__main_block_desc_0_DATA
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
int main() {
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;
}
两个main函数对照:
int main() {
//定义block
void (^blk)(void) = ^{
printf("Block\n");
};
//执行block
blk();
return 0;
}
//为了更容易观察对比,C++部分去掉转化的部分
int main() {
void (*blk)(void) =
//定义block
&__main_block_impl_0 (__main_block_func_0, &__main_block_desc_0_DATA);
//执行block
(*blk -> impl.FuncPtr)(blk);
return 0;
}
上述定义代码中,可以发现,block定义中调用了__main_block_impl_0函数,并且将__main_block_impl_0函数的地址赋值给了block。那么我们来看一下__main_block_impl_0函数内部结构。
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;
}
};
代入main函数中,得知__main_block_func_0存在impl.FuncPtr中,我们再回顾一下struct __block_impl的内部结构
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
而在__main_block_func_0中可以看到有我们的输出语句,说明__main_block_func_0用于存放我们在block中的代码
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)
};
可以看出,__main_block_desc_0的参数reserved默认值是0,我们暂时不管它,而Block_size则是存放结构体的大小。最后看__main_block_impl_0的第一个参数的结构体
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
有isa指针说明block的本质是OC对象
接着通过上面对__main_block_impl_0结构体构造函数三个参数的分析我们可以得出结论:
- __block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
- block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。
- Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。
__block说明符
首先我们知道,如果block外有一auto变量,需要在block中改变变量的值,例如:
int main(int argc, const char *argv[]) {
@autoreleasepool {
int a = 1;
void (^blk)(void) = ^{
a = 2;
NSLog("a = %d", a);
};
blk();
}
return 0;
}
这样的话就会报错
我们看一下C++代码,为什么不能更改呢,在这里我只截取了部分C++代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int a;
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int a, 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) {
int a = __cself -> a; //这里的a是从__main_block_impl_0访问到的
NSLog((NSString *)&__NSConstantStringImpl__val_floders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d84812_mi_0, a);
}
int main(int argc, const char *argv[]) {
/* @autoreleasepool*/{ __AtAutoreleasePool __autoreleasepool;
int a = 1;
void (*blk)(void) =
((void (*)())& __main_block_impl_0 (
(void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(struct __block_impl *))(
(__block_impl *)blk) -> FuncPtr)((__block_impl *)blk);
}
return 0;
}
很明显,我们不能在一个函数里改变另外一个函数中的值,访问不到那块地址,所以这样会报错。
如果为了解决这一点,可以把变量设为全局变量或者静态局部变量。
如果变为全局变量或者静态局部变量,以上C++代码会变为
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int *a;
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int *a, 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) {
int *a = __cself -> a;
(*a) = 2;
NSLog((NSString *)&__NSConstantStringImpl__val_floders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d84812_mi_0, (*a));
}
int main(int argc, const char *argv[]) {
/* @autoreleasepool*/{ __AtAutoreleasePool __autoreleasepool;
int a = 1;
void (*blk)(void) =
((void (*)())& __main_block_impl_0 (
(void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
((void (*)(struct __block_impl *))(
(__block_impl *)blk) -> FuncPtr)((__block_impl *)blk);
}
return 0;
}
变成了指针,我们就可以通过指针来改变对应的值
但有时我们并不希望变量成为全局变量或是静态局部变量,就需要用到__block关键字
int main(int argc, const char *argv[]) {
@autoreleasepool {
__block int a = 1;
void (^blk)(void) = ^{
a = 2;
NSLog("a = %d", a);
};
blk();
}
return 0;
}
这样就可以运行成功了,而且a只是一个局部变量
注意:__block不能修饰全局变量,静态变量
这时候看对应的C++代码,就会变成:
struct __Block_byref_a_0 {
void *isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__Block_byref_a_0 *a;
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *a, 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) {
__Block_byref_a_0 *a = __cself -> a;
(a -> __forwarding -> a) = 2;
NSLog((NSString *)&__NSConstantStringImpl__val_floders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d84812_mi_0, (a -> forwarding -> a));
}
int main(int argc, const char *argv[]) {
/* @autoreleasepool*/{ __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void *)0, (__Block_byref_a_0 *) &a, 0, sizeof(__Block_byref_a_0), 1);
//为了方便阅读,去掉转化
/*
__Block_byref_a_0 a = {
0,
&a, //把自己的地址传给forwarding
0,
sizeof(__Block_byref_a_0),
1
};
*/
void (*blk)(void) =
((void (*)())& __main_block_impl_0 (
(void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0)&a, 578425344));
//去掉转化
/*
void (*blk)(void) =
& __main_block_impl_0 (
__main_block_func_0,
&__main_block_desc_0_DATA,
&a,
578425344));
*/
((void (*)(struct __block_impl *))(
(__block_impl *)blk) -> FuncPtr)((__block_impl *)blk);
}
return 0;
}
编译器会将__block包装成一个对象,就可以修改block外面的变量了
注意:
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSMutableArray *array = [NSMutableArray array];
void (^blk)(void) = ^{
[array addObject: @"1"];
};
blk();
}
return 0;
}
不会报错,因为它没有修改指针对应的值,而是使用这个指针,当array = nil时才会报错。
Block循环引用
举个例子
@interface MyObject : NSObject {
Block 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);
}
由打印结果可知,该段代码的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,也可使用__unsafe_unretained修饰符。
另外,以下源代码中Block内没有使用self也同样截获了self,引起了循环引用
@interface MyObject : NSObject {
Block blk;
id obj;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk = ^{
NSLog(@"obj = %@", obj);
};
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
由警告信息可知Block语法内使用的obj实际上截获了self,对编译器来说只不过是对象用结构体对成员变量,该源代码和上面的一样,声明附有 __weak修饰符的变量并赋值obj使用来避免循环引用,同理,也可用__unsafe_unretained
我认为这里与内存管理中的用弱引用避免保留环相似
另外,还可以使用 __block变量来避免循环引用
typedef void (^Block)(void);
@interface MyObject : NSObject {
Block 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中,因此, __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的copy/release
我们先回顾一下Block类型
NSGlobalBlock 没有访问auto变量
NSStackBlock 访问了auto变量
NSMallocBlock NSStackBlock 调用了copy
copy/release
ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放复制的Block。这时我们用copy实例方法用来复制,release实例方法用来实现
即:
void (^blk_on_heap)(void) = [blk_on_stack copy];
[blk_on_heap release];
只要Block有一次复制并配置在堆上,就可通过retain实例方法持有。
[blk_on_heap retain];
但是对于配置在栈上的Block调用retain实例方法不起任何作用
[blk_on_stack retain];
该源代码中,虽然对赋值给blk_on_stack栈上的Block调用了retain实例方法,但实际上对此源码不起任何作用。因此推荐使用copy实例方法来持有Block。
另外,由于Blocks是C语言对扩展,所以在C语言中也可以使用Blocks语法。此时使用“Block_copy函数”和“Block_release函数”代替copy和release实例方法。使用方法以及引用计数的思考方法和OC相同
在ARC环境下,编译器会自动根据情况将栈上的block复制到堆上,比如以下情况
block作为函数返回时
// 先定义一个Block
typedef void (^Block)(void);
Block blk(){
int a = 0;
return ^{
NSLog(@"blk, %d", a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//Block作为函数返回
Block useBlk = blk();
useBlk();
NSLog(@"%@", [useBlk class]);
}
return 0;
}
正常打印出结果
但是如果是在MRC情况下,我们定义Block时等同于在Block里定义了一个block然后返回赋值给useBlk,没有自动copy的话内存在blk使用完就已经释放了,再在useBlk里调用肯定就不对了
从打印出的__NSMallocBlock__ 我们也能知道useBlk对blk进行了copy操作
在Block中使用附有__strong修饰符的对象类型自动变量
首先,当block里不访问强指针,即:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^blk)(void) = ^{
NSLog(@"Block");
};
blk();
NSLog(@"%@", [blk class]);
}
return 0;
}
打印结果:
而当访问强指针时:
int main(int argc, const char * argv[]) {
@autoreleasepool {
//将__block赋值给 __strong指针
int a = 0;
void (^blk)(void) = ^{
NSLog(@"Block, a = %d", a);
};
blk();
NSLog(@"%@", [blk class]);
}
return 0;
}
打印结果
而当我们在MRC环境下,同样的代码,打印结果:
block作为CocoaAPI中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时
最后
正好在ARC有效时能够同 __unsafe_unretained 修饰符一样来使用。由于ARC有效时和无效时 __block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码时在ARC有效情况下编译还是在无效情况下编译