一、block执行体里无法修改外界的普通局部变量,可以用
__block
修饰符修饰一下二、
__block
修饰符的底层实现和__block变量的本质三、__block变量的内存管理
1、系统把栈block
copy
到堆区时,也会复制一份__block变量到堆区,并让堆block持有它2、并且让栈__block变量的
forwarding
指针指向堆上面的__block变量
由于捕获变量,会导致无法修改外界的普通局部变量。
一、block执行体里无法修改外界的普通局部变量,可以用__block
修饰符修饰一下
上一篇我们说过了,“block会捕获普通局部变量,即如果block的执行体里使用了外界的局部变量,那么block内部会生成一个与普通局部变量同名的成员变量,并且普通局部变量还会通过值传递的方式把值传递给这个成员变量,那么接下来block执行体里使用的这个变量就不是外界的普通局部变量了,而是block体内的成员变量”。所以block执行体里无法修改外界的普通局部变量,就是因为block会捕获这个普通局部变量,然后block执行体里使用的这个变量就不是外界的普通局部变量了,而是block体内的成员变量,这还怎么改人家嘛。但是系统对我们屏蔽了block捕获变量的机制,这就导致在我们开发者眼里修改这个“外界的普通局部变量”修改的就是外界的普通局部变量啊,可实际上修改的却是block体内的成员变量,为了避免造成歧义,系统索性直接让编译器报错了。
但实际开发中,我们可能确实有这样的需求啊,怎么办呢?办法其实有很多,比如我们可以把这个普通局部变量搞成静态局部变量,因为block捕获静态局部变量是指针传递,所以那个函数可以通过block内部的指针顺利修改外界的变量。我们还可以把这个局部变量搞成全局变量、静态全局变量,这当然能修改了,全局变量哪里都能访问得到,block的实现部分自然也能访问得到。美则美矣,了则未了,因为我们的需求就是临时定义一个局部变量用用,用完系统就自动销毁它,不想像上面那样搞一个静态全局区的变量一直存在内存中,那还能怎么办呢?可以用__block
修饰符修饰一下。
现在我们就用__block
修饰一下age
变量,发现block实现部分确实能修改这个局部变量了。
typedef void (^INEBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 25;
INEBlock block = ^{
age = 26;
NSLog(@"%d", age);// 26
};
block();
}
return 0;
}
那为什么用__block
修饰符修饰之后,就能修改了呢?这就需要看看__block
修饰符的底层实现、__block变量的本质了,内容比较多,我们专门开一个部分来看看。
二、__block
修饰符的底层实现和__block变量的本质
我们来看下第一部分两段代码——普通局部变量和__block变量——的C/C++实现。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 创建局部变量
int age = 25;
// 创建block
INEBlock block = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age,
570425344
);
// 调用block
block->impl.FuncPtr(block);
}
return 0;
}
// block的本质
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 捕获的是局部变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 创建__block变量
__Block_byref_age_0 age = {
0, // isa指针,值为0
&age, // __forwarding指针,把age这个__block变量自己的地址给传进去了,所以说这个指针指向它自己
0,
sizeof(__Block_byref_age_0),
25 // age成员变量,其实就是外界那个局部变量,值为25
};
// 创建block
INEBlock block = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
&age,
570425344
);
// 调用block
block->impl.FuncPtr(block);
}
return 0;
}
// __block变量的本质
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
// block的本质
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // 捕获的是__block变量的地址
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block的实现部分(即生成的那个函数)
void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_age_0 *age = __cself->age; // __cself可以看做是block(其实是block的地址),这里取出block捕获的__block变量的地址
(age->__forwarding->age) = 26; // __forwarding指针不是指向__block变量自己嘛,所以这里就是相当于 (age->age) = 26,即获取__block变量的局部变量并修改
NSLog(age->__forwarding->age);
}
可见:用__block
修饰符修饰后的局部变量,已经不再是一个局部变量了,而是一个__block变量。所谓__block变量就是一个结构体,系统会把局部变量作为一个成员变量包装进它体内,它内部还有一个__forwarding
指针指向它自己。我们千万不能再把它当成一个局部变量来分析问题了,那会分析出错误的结果,倒是可以把__block变量当成静态局部变量来分析问题,会比较稳。
同时我们也看到:block不是直接捕获__block变量,而是捕获__block变量的地址,这就类似于捕获静态局部变量了。
所以我们就能在block的执行体里(即block对应的函数体里)通过“block --> block捕获的__block变量地址 --> __block变量 --> __block变量内部的局部变量”这条路线来修改“外界的局部变量”的值了(其实根本就没有“外界的局部变量”这个东西,只是我们开发者看起来像是这样),类似于通过指针修改静态局部变量那种方式。
回到需求的出发点:我们就是想搞一个局部变量,并不想把局部变量搞成一个全局的东西。新生成的__block变量就是存储在栈区的,该什么时候销毁还是什么时候销毁,只不过是对原来的局部变量做了一下包装,并没有改变局部变量的存储域,它还是一个局部变量,这就完美地实现了我们的需求。你若问“为什么新生成的__block变量存储在栈区”,这是因为新生成的__block变量默认的也是个auto
局部变量(即普通局部变量),所以存储在栈区。
上面仅仅是演示了
__block
修饰基本数据类型的局部变量,那么当__block
修饰局部对象类型的指针变量时呢?全都一样的,只不过多了一点,__block变量会根据它修饰的强指针还是弱指针来决定要不要持有该指针指向的对象。
三、__block变量的内存管理
1、系统把栈blockcopy
到堆区时,也会把栈block内部使用的__block变量复制一份到堆区,并让堆block持有它
新生成的__block变量是在栈区的,新创建的block也是在栈区(因为系统就是会为特定的block开辟栈区,这里我们也不谈全局block),此时即便block内部使用了__block变量,block也不会持有__block变量(上面说了__block变量是个对象哦),因为栈block永远不会持有别人。
可是接下来当我们把栈blockcopy
到堆区时(ARC下系统会自动做这个操作),系统也会把栈block内部使用的__block变量复制一份到堆区(为什么?因为block都去堆区了,你__block变量还在栈区,说不准什么时候就被销毁了,那block还访问个啥),并调用block内部的copy
函数让copy
出来的堆block强引用这个__block变量(注意这个和__block变量是强指针还是弱指针没有关系,这里系统设计就是要强引用它,__block变量虽然是个OC对象,但它和普通的OC对象又不太一样,没有强指针、弱指针一说)。
而且如果有多个block使用了同一个__block变量,则只会在
copy
第一个block到堆区时复制一份__block变量到堆区,后面的blockcopy
到堆区时就不会再复制__block变量到堆区了,因为所有的__block变量都是同一个嘛,只需要增加它的引用计数就可以了。
而当block销毁时,系统就会调用block内部的dispose
方法来释放对__block函数的强引用。
2、并且让栈__block变量的forwarding
指针指向堆上面的__block变量
当我们把栈block和它内部使用的__block变量都复制到堆区后,栈block销毁后大家使用的就是堆上的这份block和__block变量了,但是栈block销毁前呢?我们得保证栈block销毁前,它内部对__block变量数据的写入和读取都指向堆区的那个__block变量,只有这样当栈block销毁后,我们开始使用堆block访问__block变量时,数据才是最新的,否则堆__block变量的数据就又可能滞后于栈__block变量的数据,这种现象是不应该发生的,所以系统把栈blockcopy
到堆区时,会让栈__block变量的forwarding
指针指向堆上面的__block变量,当然堆__block变量的forwarding
指针还是指向它自己。
网友评论