__block说明符
Block只能保存局部变量瞬间的值,所以当我们尝试修改截获的自动变量值,就会报错。例如:
int val = 0;
void (^blk)(void) = ^(val = 1);
blk();
printf("val = %d\n", val);
该源代码会产生编译错误:
error: variable is not assignable (missing __block type specifier)
因此,若想修改截获的局部变量值,就必须用__block
修饰
__block int val = 0;
void (^blk)(void) = ^(val = 1);
blk();
printf("val = %d\n", val);
执行结果为:
val = 1;
下面我们再看一个例子:
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
这是没问题的。如果向截获的变量array赋值则会产生编译错误。
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
同样,使用__block
修饰array就行了。
另外在使用c语言数组时,必须小心使用其指针。
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
看似没有向截获的自动变量赋值,只是使用了字符串数组。但是Block并没有实现截获c语言数组。此时可以使用指针解决问题
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
Block的实质
我们通过一个实例来看看block的实质
void (^blk)(void) = ^{
printf("Block\n");
};
通过clang来转换为c++的代码
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
我们看一下转换后的代码:
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() {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//使用block
(void (*) (struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk)
}
我们可以看到Block的匿名函数转化为c语言函数来处理:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
__cself
是指向实例自身的变量self
。
__main_block_impl_0
结构体声明如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//这是__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_impl_0
结构体由__block_impl和__main_block_desc_0两个成员变量和一个构造函数组成。
其中__block_impl
结构体声明如下:
struct __block_impl {
void *isa;
int Flags;//标志
int Reserved;//今后版本升级所需的区域
void *FuncPtr;//函数指针
};
第二个成员变量为Desc
指针,声明如下:
struct __main_block_desc_0 {
unsigned long reserved;//今后版本升级所需的区域
unsigned long Block_size//Block的大小
}
因此,如果展开__main_block_impl_0,可记述如下形式:
struct __main_block_impl_0 {
void *isa;
int Flags;//标志
int Reserved;//今后版本升级所需的区域
void *FuncPtr;//函数指针
struct __main_block_desc_0 * Desc
};
接下来看看__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;
}
_NSConcreteStackBlock用来初始化block_impl结构体的isa成员变量
flags用于初始化block_impl结构体的flags
fp用于初始化block_impl结构体的FuncPtr
接下来我们再看一下原来构造函数的调用:
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
该源代码将__main_block_impl_0结构体类型的变量(即栈上生成的__main_block_impl_0结构体实例的指针)赋值给blk。
然后再看看是如何使用block的
(void (*) (struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
去掉转换部分:
(*blk->impl.FuncPtr)(blk);
就是通过函数指针FuncPtr来调用blk本身。
这也证明了__main_block_func_0的参数__cself指向block值。
直到现在我们明白了block的本质:
- block本质就是一个__main_block_impl_0。
- block通过内部的函数指针FuncPtr来调用它本身。
还有一点刚才没有说明,刚才在初始化__block_impl 的isa成员变量的_NSConcreteStackBlock又是什么呢???
要理解_NSConcreteStackBlock
,我们结合objc_object,它也有一个isa指针,用于指向该对象所属的类。
同理:在将Block作为对象处理时,__block_impl的isa指针指向的类的信息保存在_NSConcreteStackBlock上。即它是在栈上生成的__block_impl结构体实例。
截获自动变量值
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
};
blk();
return 0;
}
clang之后:
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);
}
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 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));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
我们先看一下不同之处:
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;
}
};
Block将所使用的局部变量作为成员变量追加到了__main_block_impl_0
结构体中。
注意:未使用的的变量将不会追加。
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val)
将追加的局部变量作为参数来初始化结构体。
__main_block_impl_0展开代码如下:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = _main_block_func_0;
Desc =&__main_block_desc_0_DATA;
fmt = "val = %d\n"
由此可见,在__main_block_impl_0实例中,自动变量被截获。
我们再来看看Block的实现:
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);
}
因为__main_block_impl_0截获了fmt和val变量,所以就可以直接执行了。
总的来说,所谓“截获自动变量值”意味着在执行block语法时,block语法所使用的局部变量被保存到block的结构体实例中。
__block说明符
__block说明符类似于static、auto,他们用于指定将变量值设置到哪个存储域中。auto表示作为局部变量存储在栈中,static表示作为静态变量存储在数据区中。
__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; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_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; // bound by ref
(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((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t 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};
int main() {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
}
这个__block变量是怎样转换过来的呢?
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
__block居然变成了结构体类型的局部变量,即栈上生成的__Block_byref_val_0结构体实例。
它包含原自动变量的成员变量val
我们再来看看__main_block_impl_0
这个结构体有什么不同
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
新增了一个成员变量__Block_byref_val_0
,将这个成员变量作为参数来初始化这个结构体
__block赋值的代码又如何呢?
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
__block变量赋值比较复杂,__main_block_impl_0结构体实例持有指向__block变量的__Block_byref_val_0结构体实例的指针。
__Block_byref_val_0结构体实例的成员变量__forwarding持有该实例自身的指针。通过成员变量__forwarding访问成员变量val。
至此为止,我们只是大概了解了__block类型,但是还有两个问题没有解决:
*Block超出变量作用域可存在的理由
- __block结构体中__forwording成员变量是干嘛的。
我们继续分析。
Block存储域
由前面的讲解,我们知道和
Block的本质:栈上生成的__main_block_impl_0结构体实例
__block的本质:栈上生成的__Block_byref_val_0结构体实例
我们之前说过Block其实也是一个OC对象,它所属的类是_NSConcretStackBlock
,除了这个,还有其他两个:
- _NSConcretStackBlock:设置在栈上
- _NSConcretGlobalBlock:设置在程序的数据区域(.data)中
- _NSConcretMallocBlock:设置在堆上
配置在全局变量的block,从变量作用外也可以通过指针安全的使用。
配置在栈上的block,如果其所属的变量作用域结束,该block就会被废弃。由于__block变量也配置在栈上,如果其所属的变量作用域也结束了,__block变量也会被废弃。
什么时候可以设置在程序的数据区域中呢?
- 使用的block是全局的block
- 即使不是全局的block,如果不截获的自动变量,也会设置在.data区。
除以上这两种情况,Block都是_NSConcretStackBlock类对象,设置在栈上。
现在我们回答第一个问题:Block超出变量作用域可存在的理由??
因为Blocks提供了将Block和__block变量从栈上复制到对上的办法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block变量作用域结束,堆上的Block也可以继续存在。
复制到堆上的Block就是_NSConcretMallocBlock类对象。
impl.isa = &_NSConcretMallocBlock
接下来我们重点看看Blocks提供的复制方法:
实际上在ARC下,大多数情形下编译器会自动的判断,自动生成将Block从栈上复制到堆上的代码。我们看一下将Block作为函数值返回的代码。
typedef int (^blk_t) (int);
blk_t func(int rate) {
return ^(int count) {
return rate * count;
};
}
该源代码返回配置在栈上的Block,当变量作用域结束时,这个Block就会被废弃。虽然如此,但该源代码通过编辑器可转换如下:
blk_t func(int rate) {
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValur(tmp);
}
objc_retainBlock就是_Block_copy函数。
因此当Block作为函数值返回时,编译器会自动生成复制到堆上的代码。
如果编译器不能生成复制到堆上的代码,就需要手动调用alloc/new/copy/mutableCopy的任意一个代码。
哪些情况编译器会自动将block从栈复制到堆上:
- 使用Block的copy实例方法。
- Block作为函数返回值返回时。
- 将Block赋值给附有__strong修饰符的id类型的类或者Block类型的实例变量时。
- 在方法名中使用usingBlock或者使用GCD的API中传递Block时
第二个问题:__block结构体中__forwording成员变量是干嘛的??
它可以实现无论__block变量配置在栈上还是堆上都能够正确访问__block变量。
有时__block变量配置在堆上,也可以访问栈上的__block变量,这是因为栈上的结构体成员变量__forwarding指向堆上的结构体成员变量。