一、Block本质
Block是“带有自动变量值的匿名函数”。
所谓的匿名函数就是不带有名称的函数
typedef int (^blk_t)(int)
blk_t = ^(int count){
return count+1;
}
但它究竟是什么呢?
转码
通过-rewrite-objc
选项将含有Block语法的源代码变换为C++代码
变换前:
#include<stdio.h>
int main() {
void (^blk)(void) = ^{printf("Block");};
blk();
return 0;
}
终端:clang -rewrite-objc 源代码文件名
变换后:(变换后有568行,精简后如下)
struct __block_impl {
void *isa; //isa指针
int Flags; //标志位
int Reserved;
void *FuncPtr; //函数指针
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc; //block描述信息
//构造函数(类似于OC的init方法),返回结构体对象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指针
impl.Flags = flags; //标志位
impl.FuncPtr = fp;//函数指针
Desc = desc; //block描述信息
}
};
/*
^{printf("Block");};变换后的样子
Block匿名函数实际上被作为简单的C函数来处理
函数名的命名规则:根据Block语法所在的函数名(此处为mian)和该Block语法在该函数出现的顺序值(此处为0)来命名的
__cself相当于OC中的self
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block");
}
int main() {
/*
void (^blk)(void) = ^{printf("Block");};转换后的代码
构造函数构造后,__main_block_impl_0结构体结果如下
isa = &_NSConcreteStackBlock;
Flags = 0;
FuncPtr = __main_block_func_0; //函数指针,指向__main_block_func_0函数
Desc = &__main_block_desc_0_DATA;
*/
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
/*
blk();转换后的代码如下
*/
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
分析
1、分析void (^blk)(void) = ^{printf("Block");}
上面C++代码
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));
//将结构体实例的指针赋给blk
struct __main_block_impl_0 * blk = &tmp;
通过简化后的代码可知,源代码将__main_block_impl_0
结构体类型的自动变量,即栈上生成的__main_block_impl_0
结构体实例的指针,赋值给__main_block_impl_0
结构体指针类型的变量blk。
而最初的Block源代码是void (^blk)(void) = ^{printf("Block");};
因此,将Block语法生成的Block赋给Block类型变量blk。它等同于将__main_block_impl_0
结构体实例的指针赋给变量blk。
-
堆:动态分配内存,需要程序员自己申请,程序员自己管理
-
栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况
2、分析blk()
blk();转换后的代码如下:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉转换后的部分如下:
(*blk->impl.FuncPtr)(blk);
可见,这就是简单地使用函数指针调用函数。__main_block_func_0
的函数指针被赋值到成员变量FuncPtr中。另外也说明了,__main_block_func_0
函数的参数__cself
指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行传递。
__main_block_impl_0
结构体相当于基于objc_object结构体的Objective-C类对象的结构体。
_NSConcreteStackBlock
相当于class_t结构体实例。在将Block作为Objective-C对象处理时,关于该类的信息放置于_NSConcreteStackBlock
中。
因此,Block本质上也是一个OC对象(最终继承NSObject),它内部也有个isa指针。
二、Block捕获变量值
Block是带有自变量的匿名函数,其中的"带有自变量值"是什么意思呢?"带有自变量值"在block中表现为"捕获自变量值"。
为了保证Block内部能够正常访问外部的变量,Block有个变量捕获机制:
局部变量(auto):捕获到Block内,值传递;
局部变量(static):捕获到Block内,指针传递;
全局变量:不捕获到Block内,直接访问;
Q:下列代码输出值分别为多少?
int val = 10;
const char * fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt,val);
};
val = 2;
fmt = "Change the value of val,val = %d\n";
blk();
输出结果为:val = 10
原因:在执行Block语法时,会捕获自动变量值,即Block语法表达式所使用到的自动变量值被保存到Block的结构体实例中。
源码证明:
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; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt,val);
}
int main() {
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));
val = 2;
fmt = "Change the value of val,val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
Q:下列代码输出值分别为多少?
auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
printf("age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();
输出结果为:age:10,num:11
原因:auto变量Block访问方式是值传递,static变量Block访问方式是指针传递
源码证明:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_num, int flags=0) : age(_age), num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *num = __cself->num; // bound by copy
printf("age:%d,num:%d",age,(*num));
}
int main() {
auto int age = 10;
static int num = 25;
void (*Block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &num));
age = 20;
num = 11;
((void (*)(__block_impl *))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block);
return 0;
}
上述代码可知static修饰的变量,是根据指针访问的
Q:为什么block对auto和static变量捕获有差异?
auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可
三、__block说明符
__block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中(参见下文)。如auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。
自变量被Block截获后,Block保存的是当前的瞬间值,保存后就不能修改该值。若想在Block语法中修改捕获到的自变量的值,则需要在该值变量前加上__block说明符,如果不加该说明符,则运行会报错
__block int val = 0;
void (^blk)(void) = ^{
val = 1; //修改捕获到的自变量的值
};
blk();
printf("val = %d\n",val);
系统对__block int val = 0;做了什么?
编译器会将__block变量包装成一个对象,具体的C++代码如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding; //val的地址
int __flags;
int __size;
int val; //val的值
};
从C++代码可知,结构体持有相当于原自动变量的成员变量。通过成员变量__forwarding访问成员变量val。(成员变量val是该实例自身持有的变量,它相当于原自动变量本身),如下图:
访问__block变量.png
因此,加了__block修饰的变量,Block截获后是通过指针去操作该变量,因此可以修改变量的值。
栈上__block
的__forwarding
指向本身
栈上__block
复制到堆上后,栈上block的__forwarding
指向堆上的block,堆上block的__forwarding
指向本身
捕获OC对象(不用__block修饰),调用变更对象的方法是可以的,而向捕获的变量赋值则会产生编译错误
id arr = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[arr addObject:obj]; //这样是可以的
arr = [[NSMutableArray alloc] init]; //这样是不行的(编译报错)
}
现在的Block中,捕获自变量的方法并没有实现对C语言数组的截获,因此在访问C语言数组时会产生编译错误,可以通过使用指针解决该问题
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n",text[2]); //这样使用时不行的
};
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n",text[2]); //改成指针可以
};
__block总结
- __block可以用于解决block内部无法修改auto变量值的问题
- __block不能修饰全局变量、静态变量(static)
- 当__block变量在栈上时,不会对指向的对象产生强引用
- 编译器会将__block变量包装成一个对象
- __block修改变量:
age->forwarding->age
- __Block_byref_val_0结构体内部地址和外部变量val是同一地址
四、Block类型
Block的类型取决于isa指针,可以通过调用class方法查看具体类型,最终都继承自NSBlock。
- __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
- __NSStackBlock __ ( _NSConcreteStackBlock )
- __NSMallocBlock __ ( _NSConcreteMallocBlock )
代码示例
void (^block1)(void) = ^{
NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);
输出结果:
NSGlobalBlock
__NSGlobalBlock
NSBlock
NSObject
null
上述代码输出了block1的类型,也证实了block是对象,最终继承NSObject
代码展示block的三种类型:
/*
全局block
没有访问auto变量的block是__NSGlobalBlock__,放在数据段
因为在使用全局变量的地方不能使用自动变量,所有不存在对自动变量进行截获
由于此Block结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例
因此,将Block结构体实例设置在与全局变量相同的数据区域中即可
*/
void (^blk1)(void) = ^{ NSLog(@"blk1"); };
NSLog(@"%@",[blk1 class]);
/*
堆block
将Block赋值给__strong指针时,ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上
如果void (^blk2)(void) = ^{ 写成 void __weak (^blk2)(void) = ^{
则是栈block(编译器没有将其复制到堆上)
*/
int age = 1;
void (^blk2)(void) = ^{
NSLog(@"blk2:%d",age);
};
NSLog(@"%@",[blk2 class]);
/*
栈block
访问了变量,并且没有做copy操作
*/
NSLog(@"%@",[^{
NSLog(@"blk3:%d",age);
} class]);
输出结果:
NSGlobalBlockNSMallocBlock
NSStackBlock
-
__NSGlobalBlock __ 在数据区
-
__NSMallocBlock __ 在堆区
-
__NSStackBlock __ 在栈区
-
堆:动态分配内存,需要程序员自己申请,程序员自己管理
-
栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况
Block存储域.png
如何判断Block是哪种类型 -
没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段
-
访问了auto变量的block是__NSStackBlock __
-
[__NSStackBlock __ copy]
操作就变成了__NSMallocBlock __
在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上的情况有如下几种
- Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码;
- 将Block赋值给__strong指针时;
- Cocoa框架的方法,并且方法名中包含有usingBlock等时;
- Block作为GCD API的方法参数时;
如在使用NSArray类的enumerateObjectsUsingBlock实例方法以及dispatch_async函数时,不用手动复制。相反
在NSArray类的initWithObjects实例方法上传递Block时需要手动复制。
备注:将Block从栈上复制到堆上是相当消耗CPU的。
对每种类型Block调用copy操作后是什么结果?
不管Block配置在何处,用copy方法复制都不会引起任何问题,在不确定时调用copy方法即可。
__block变量存储域
若一个Block中使用
__block
变量,则当该Block从栈复制到堆时,由于其使用的所有__block
变量也必定配置在栈上。所以这些__block
变量也会一并从栈复制到堆,并且被该Block所持有。复制__block变量.png
当多个Block使用同一个
__block
变量时,因为Block最先是配置在栈上的,所以__block
变量也都配置在栈上。当其中一个Block被复制到堆上时,__block
变量也会一并从栈复制到堆,并被Block所持有。当其他的Block从栈复制到堆时,被复制的Block持有__block
变量,并增加__block
变量的引用计数。如下图:多个block持有同一个__block变量..png
如果配置在堆上的Block被废弃,那么它所使用的
__block
变量也就被释放。
MRC下Block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
ARC下Block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
Block的属性修饰词为什么是copy
Block需要通过copy才会被复制到堆上,只有在堆上,程序员才能对它做内存管理、控制Block生命周期等操作。
五、Block循环引用
__strong强引用
__strong修饰符是id类型和对象类型,默认的所有权修饰符,可以不写
__weak弱引用
__weak弱引用,不持有对象,在超出其变量作用域时(如函数花括号之外),对象立即被释放
__weak可以避免循环强引用
__weak在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效,且被置为nil
__weak修饰符只能在iOS5以上使用
__unsafe_unretained
在iOS5以下用__unsafe_unretained修饰符
尽管ARC式的内存管理是编译器的工作,但符有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。
赋值给__unsafe_unretained修饰的变量的对象,需确保对象不为空,否则会产生悬垂指针,导致运行奔溃。
如下:
id __unsafe_unretained obj1 = nil
{
id __strong obj0 = [[NSObject alloc] init]; //obj0持有对象
obj1 = obj0; //虽然obj0变量赋给obj1,但obj1变量既不持有对象的强引用,也不持有对象的弱引用
NSLog(@"A:%@",obj1);
}
//obj0超出其作用域,强引用失效,所以自动释放自己持有的对象,因为[[NSObject alloc] init]对象没有持有者,所以废弃该对象
NSLog(@"B:%@",obj1);
//obj1变量表示的对象已被废弃(悬垂指针),因此访问出错
@autoreleasepool自动释放池
@autoreleasepool {
id __strong obj = [NSMutableArray array];
} //到这个花括号释放池代码块结束,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放
解决循环引用:
__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil
MyObject * object = [[MyObject alloc] init];
object.age = 10;
__weak typeof(object) weakObject = object;
object.blk = ^{
NSLog(@"age is %d", weakObject.age);
};
object.blk();
__block:必须把引用对象置为nil,并且要调用该block
__block MyObject * object = [[MyObject alloc] init];
object.age = 10;
object.blk = ^{
NSLog(@"age is %d", object.age);
object = nil;
};
object.blk();
参考资料
Objective-C高级编程
网友评论