Block可以修改的变量
我们可能都知道,不使用__block
关键字,我们不能在Block内修改变量的值。但是严格说来,只有不使用__block
修饰的局部变量是不能在Block内修改的。全局变量、静态变量和对象的属性都可以不使用__block
而在Block内部被修改,下面这段代码是可以正常编译通过的,但是如果我们将main函数中的变量前面的static修饰符去掉,在编译的时候就会报错:error: variable is not assignable (missing __block type specifier)
,错误就定位在static_val++
这行代码。
int global_val = 1;
static int static_global_val = 2;
int main()
{
static int static_val = 3;
void (^blk)(void) = ^{
global_val++;
static_global_val++;
static_val++;
};
return 0;
}
Block为什么可以修改非局部变量
为什么修改全局变量和静态变量可以不使用__block
修饰符呢?
我们需要了解各种不同变量存放的内存区域才能解答这个问题。
Objective-C程序运行的时候,内存空间会被分为数据区、堆区和栈区。数据区在程序加载的时候静态分配,堆区在程序运行的时候由内存分配函数malloc、calloc等或者NSObject的alloc方法动态分配。数据区的内存空间不受程序的控制,只有在程序运行结束后才能被释放;堆区的内存空间,需要由程序自行释放,否则会导致内存泄漏。简而言之,数据区的空间在整个程序运行过程中都是可读写的,堆区的空间什么时候可以读写我们是可以控制的。
栈空间不需要我们手动管理,在程序运行过程中(主要发生在函数调用时)系统会自动进行pop和push操作来管理栈空间。
堆空间和栈空间不同的地方在于,栈空间上存储的信息,一旦过了作用域就会被系统自动pop出去而变得不可合法的读写,而动态分配的堆空间上的数据,就算过了作用域,我们还是可以通过一定的方式能够合法读写到(比如将这块空间的地址存储下来)。
在ARC配置下,很多Objective-C对象的释放已经由编译器自行添加了,但是如果我们使用Core Foundation对象依然需要手动释放。
在Block的使用过程中,给我们的主观感受是,Block内部引用的变量跟Block外部定义的变量是同一个变量。根据我们之前的分析,显然内部实现不是同一个变量。所以我们的主观期望是,如果我们在Block内部修改了某个变量的值,那么Block外部的变量值也会被修改。如何实现这个期望呢?
上述代码中,全局变量global_val
、静态全局变量static_global_val
和静态局部变量static_val
都是存储在数据区的,内存空间不会被释放,所以Block在执行的时候可以直接读写。但是实现方式有所不同,拿上面的例子来说,转化后的主要代码如下:
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; // bound by copy
global_val++;
static_global_val++;
(*static_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()
{
static int static_val = 3;
void (*blk)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
return 0;
}
我们发现global_val
和static_global_val
都不是Block结构体的成员,而static_val
虽然是Block结构体的成员,但是是通过指针的方式实现的。
为什么会有这样的区别实现呢?
在这个例子中,Block是一个局部数据,而global_val
和static_global_val
这两个变量的作用域都是文件级作用域,超过了Block的作用域,所以Block内部可以无障碍的使用global_val
和static_global_val
变量名来读写这两个变量,所以无需在Block内部再存有这两个变量的引用了。
但是static_val
不同,因为Block在执行的时候,有可能已经超出了static_val
的作用域,所以这个例子中使用了指针的方式实现,指针是最简单的读写超出作用域的变量的方法了。
那么Block为什么可以修改成员变量呢?道理跟static_val
的访问方式类似,因为成员变量是随着self对象在堆空间动态分配的,在Block的内部有一个self指针,通过self指针就可以正常的读写self的成员变量了。
Block为什么不可以修改局部变量
其实Block不可以直接修改局部变量的原因上面跟上面的分析类似:
- Block执行的时候有可能它引用的局部变量已经超出作用域了;
- 局部变量超出作用域会被系统自动释放掉,即使保存了它的指针也无法合法的访问了;
- Block内部对局部变量的修改需要反映到外部的局部变量上。
基于上面的几个原因,Block内部读写全局变量和静态变量的方式已经不再适用于读写局部变量了。
__block的实现
为什么被__block
修饰的变量可以在Block内部被修改呢,我们先看一个简单的例子:
int main(int argc, const char * argv[]) {
__block int val = 10;
printf("1. %d, address 0x%lx\n", val, &val);
void (^blk)(void) = ^{
val++;
printf("3. %d, address 0x%lx\n", val, &val);
};
printf("2. %d, address 0x%lx\n", val, &val);
blk();
return 0;
}
为了便于分析,我们先给出这个例子的执行结果:
val的值我们很容易理解,我们重点关注val的地址。从结果我们发现,在Block表达式之后,val的地址竟然发生了变化!
为了分析原因,我们同样把上面的代码转化一下:
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)++;
printf("3. %d, address 0x%lx\n", (val->__forwarding->val), &(val->__forwarding->val));
}
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(int argc, const char * argv[]) {
__Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
printf("1. %d, address 0x%lx\n", (val.__forwarding->val), &(val.__forwarding->val));
void (*blk)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
printf("2. %d, address 0x%lx\n", (val.__forwarding->val), &(val.__forwarding->val));
blk->FuncPtr(blk);
return 0;
}
我们首先看main函数,发现经过转化后,val已经不是一个普通的int型变量了,而是成为了__Block_byref_val_0
结构,这个结构的定义在最开头。
另外一个很大的不同点在于struct __main_block_desc_0
多了一个copy和一个dispose成员。copy函数会调用_Block_object_assign
,在本例中这个函数使用的参数是Block里的val成员和main函数里的val变量。虽然没有显式调用,但是我们可以猜测在Block实现之前,系统会调用这个函数将val从栈上拷贝到堆上,拷贝到堆上后,Block就可以通过指针来读取val了。
可以通过查阅GNUStep上的开源代码获取一点信息,
_Block_object_assign
实现在blocks_runtime.m
文件内。我们猜测的_Block_object_assign
会被调用,可以通过在Xcode中加Symbolic Breakpoint并将Symbol设置为_Block_object_assign
来验证。
那么怎么实现在Block内部修改val变量的值,能够影响到外部呢?答案就在__Block_byref_val_0
结构的forwarding
成员上,我们也发现,经过转化后,所有对val变量的访问都是通过forwarding来中转的,我们通过下图描述一下在main函数里Block实现代码前后内存里究竟发生了什么:
在Block未实现时,val存储在栈上,forwarding指向的是它本身;但是当Block实现后,会在堆上分配一个相同的val的对象,同时将栈上val的forwarding指向堆上的val,Block内部持有的是堆上的对象,后续对val的修改都是修改了堆上的对象。因为我们获取val对象里的val值是通过forwarding指针获取的,其指向的对象都是堆上的,所以能保证获取的都是修改后的值。
即使栈上的val超过了作用域,堆上的对象也依然存在,我们也可以正常的访问。
Block修改超出作用域的局部变量
下面是一个Block修改超出作用域的局部变量的例子:
typedef void (^blk_t)(void);
blk_t getBlk() {
__block int val = 1;
printf("Begin block, val = %d, address of val = 0x%lx\n", val, &val);
blk_t blk = ^{
++val;
printf("In block, val = %d, address of val = 0x%lx\n", val, &val);
};
++val;
printf("In function, val = %d, address of val = 0x%lx\n", val, &val);
blk();
return [blk copy];
}
int main(int argc, const char * argv[]) {
blk_t blk = getBlk();
printf("In main\n");
blk();
return 0;
}
执行结果见下图:
这段代码的原理与上面的是一样的,各位看官可以自行分析理解。