文章目录
- 前言
- 一、Block如何捕获外界变量
- 二、__block修饰符
- 三、Block的类型
- 四、判断block存储在哪里
- 五、Block的copy操作
- 六、源码分析Block_copy()
- [七、__block 与 __forwarding](#七、__block 与 __forwarding)
- 八、block发生copy的时机
- 总结
前言
之前的Block写的没啥重点,这一次总结一下重点,比如Block如何捕获外界变量,__block的使用
一、Block如何捕获外界变量
1.捕获自动变量
首先先上代码,我们在Block中添加了localA局部变量
bash
int main() {
int localA = 7;
void(^block)(void) = ^{
NSLog(@"block - %d - %d", localA, globalB);
};
block();
localA += 10;
block();
return 0;
}
发现在外部修改localA并没有影响到Block内部
编译成源码
bash
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int localA;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到Block实现结构体中新增了localA
变量
我们具体看一下函数实现代码
bash
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int localA = __cself->localA; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_tm_rlw5m0t17cg39hpr19j64k140000gn_T_main_e839a5_mi_0, localA);
}
我们分析一下这段代码,在我们int localA = __cself->localA; // bound by copy
,localA 通过拷贝与 struct __main_block_impl_0
绑定,意思是 localA 是拷贝过来 的。那么得出结论:在外部改变 localA 不会影响到 block 内部。
当
Block
捕获基本数据类型(如 int、float 等)或结构体时,它会通过值捕获的方式复制变量的当前值。这意味着在 Block内部使用的是变量捕获时刻的快照。由于是值复制,所以之后即使原始变量的值发生改变,Block 内部的值也不会改变。
2.捕获静态局部变量
一样的来看一个例子
bash
#import <Foundation/Foundation.h>
//全局变量
int global_a = 10;
//静态全局变量
static int staic_global_a = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
//基本数据类型的局部变量
int a = 5;
//对象类型的局部变量
//局部静态变量
static int staic_a = 6;
void(^block)(void) = ^{
NSLog(@"局部变量.基本数据类型 %d",a);
NSLog(@"局部静态变量 %d",staic_a);
NSLog(@"全局变量 %d",global_a);
NSLog(@"静态全局变量 %d",staic_global_a);
};
block();
}
return 0;
}
编译源码
bash
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *staic_a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_staic_a, int flags=0) : a(_a), staic_a(_staic_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到只有自动变量a
与静态局部变量staic_a
被加到了Block实现结构体中,同时静态局部变量与Block建立关联的是指针(int *)
,也就是说Block捕获的静态局部变量捕获的是变量的指针,因此当我们对静态局部变量进行修改时,Block内部的静态局部变量的值也会随之改变
bash
//局部静态变量
static int staic_a = 6;
void(^block)(void) = ^{
NSLog(@"局部变量.基本数据类型 %d",a);
// NSLog(@"局部变量.__unsafe_unretained.对象类型 %@",unsafe_objc);
// NSLog(@"局部变量.__strong.对象类型 %@",strong_objc);
NSLog(@"局部静态变量 %d",staic_a);
NSLog(@"全局变量 %d",global_a);
NSLog(@"静态全局变量 %d",staic_global_a);
};
a++;
staic_a++;
global_a++;
block();
3.全局、全局静态变量
我们可以看到全局、全局静态变量并没有出现在我们的Block实现结构体中,说明二者无法被捕获
二、__block修饰符
我们先来引出一个问题,如果我们想在Block中修改我们捕获的自动变量该如何实现
如果我们直接进行修改会出现这样的错误
错误是:变量无法被赋值
我们一样给出例子:
bash
int main() {
int localA = 7;
__block int local__blockB = 8;
void(^block)(void) = ^{
NSLog(@"block - %d - %d", localA, local__blockB);
};
block();
localA += 10;
local__blockB += 10;
block();
return 0;
}
自由变量 localA 并没有改变(还是 7),但是被 __block 修饰的 local__blockB 改变了(8 += 10 >>> 18)。
查看源码
bash
struct __Block_byref_local__blockB_0 {
void *__isa;
__Block_byref_local__blockB_0 *__forwarding;
int __flags;
int __size;
int local__blockB;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int localA;
__Block_byref_local__blockB_0 *local__blockB; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, __Block_byref_local__blockB_0 *_local__blockB, int flags=0) : localA(_localA), local__blockB(_local__blockB->__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_local__blockB_0 *local__blockB = __cself->local__blockB; // bound by ref
int localA = __cself->localA; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main___block_71facf_mi_0, localA, (local__blockB->__forwarding->local__blockB));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->local__blockB, (void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->local__blockB, 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 localA = 7;
__attribute__((__blocks__(byref))) __Block_byref_local__blockB_0 local__blockB = {(void*)0,(__Block_byref_local__blockB_0 *)&local__blockB, 0, sizeof(__Block_byref_local__blockB_0), 8};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA, (__Block_byref_local__blockB_0 *)&local__blockB, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
localA += 10;
(local__blockB.__forwarding->local__blockB) += 10;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
被 __block
修饰的自由变量 local__blockB
竟然变成了一个结构体 struct __Block_byref_local__blockB_0
。
bash
// struct __Block_byref_local__blockB_0 实例化 local__blockB
{
__isa = (void *)0,
__forwarding = (____Block_byref_local__blockB_0 *)&local__blockB,
__flags = 0,
__size = sizeof(__Block_byref_local__blockB_0),
local__blockB = 8 // 这就是我们赋值给 local__blockB 的值 8
}
在这个结构体中主要封装了两个东西------__forwarding指针
与原本的local__blockB变量
结构体中的
local_blockB
是被__block
修饰的实际变量,这里存储的是变量的值 。在你的例子中,local__blockB 被初始化或赋值为 8。这个字段代表了原始变量在被__block 修饰后的存储位置。
我们来看block函数实现中,发现读取__block变量时拐了一个大弯
bash
local__blockB->__forwarding->local__blockB
在修改值的时候也是
bash
(local__blockB.__forwarding->local__blockB) += 10
我们暂且按下不表,这与下面将要讲的Block
的copy
操作有关
不过在这里还是总结一下block对变量的捕获情况
变量类型 | 是否捕获到block内部 | 访问方式 |
---|---|---|
自由变量 | 是 | 值拷贝 |
静态变量 | 是 | 指针拷贝 |
全局变量 | 无法捕获 | 直接使用 |
三、Block的类型
为了研究 block 的 copy 操作,我们先要搞清楚 block 到底存储在栈上还是堆上???
我们先前讲过了iOS的内存分区,拿一张图回忆一下
【iOS】内存分区
常用级别的 Block 分为三类:
_NSConcreteGlobalBlock
: 全局 block,存储在全局内存中,相当于单例;
_NSConcreteMallocBlock
: 堆 block,存储在堆内存中,是一个带有引用计数的对象,需要自行管理器内存;
_NSConcreteStackBlock
: 栈 block,存储在栈内存中,超出作用域立马销毁。
四、判断block存储在哪里
说完了block的类型,我们来分情况讨论不同情况下block的存储位置
- 情况一 不创建 Block 变量,不访问变量
结果:NSGlobalBlock
- 情况二 创建 Block 变量,不访问变量
结果:NSGlobalBlock
- 情况三 不创建 Block 变量,访问自由变量
结果:NSStackBlock - 情况四 创建 Block 变量,访问自由变量
结果:NSMallocBlock
- 情况五 不创建 Block 变量,访问全局变量
结果:NSGlobalBlock
- 情况六 创建 Block 变量,访问全局变量
结果:NSGlobalBlock
总结一下:
结论:
1、访问全局变量与没有访问变量是相同的,因此都没有捕获操作,全局变量直接引用
2、由于不创建Block
就没有copy
操作,因此创建Block
变量并且捕获自由变量时Block会被拷贝到堆上,如果没有copy操作使用完就直接释放了
五、Block的copy操作
我们这里讲一下为什么我们的Block访问自由变量时会自动进行copy操作存储到堆上
在 Objective-C 中,Block 最初是在栈上创建的。栈上的 Block(NSStackBlock )生命周期与其定义的作用域相关联,一旦该作用域结束,栈上的 Block 将不再有效。这意味着如果你需要在 Block 的定义作用域外使用它,比如将它作为回调传递或保存为后续使用,你需要将它复制到堆上(成为 NSMallocBlock)。
如何理解作用域结束后Block不再有效,如果想要继续使用就要拷贝到堆上?
给出一个例子
bash
// 假设这个方法从网络获取数据
- (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟网络请求
NSData *data = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
// 模拟一个成功的获取数据的操作
sleep(2); // 模拟耗时操作
// 回到主线程来执行回调
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion(data, error); // 调用 Block 传递数据或错误信息
}
});
});
}
当在子线程执行完操作需要回到主线程,我们就用Block进行回调,如果Block在栈上,那么超出作用域就会被销毁,无法回到主线程被调用,因此需要拷贝到堆上
在ARC环境下编译器会自动完成拷贝到堆上的操作,在MRC下需要我们手动拷贝与释放
我们通过[block copy]
操作进行拷贝,不同类型的Block
进行copy
效果也不同
六、源码分析Block_copy()
我们通过源码研究一下copy操作
bash
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
简化一下里面的操作
bash
/* 拷贝 Block,或者增加 Block 的引用计数。若需要拷贝,调用拷贝协助方法(如果存在) */
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
///Junes 1、若不存在源 Block ,则返回 NULL
if (!arg) return NULL;
///Junes 2、将源 Block 指针转换为 (struct Block_layout *)
aBlock = (struct Block_layout *)arg;
///Junes 3、若源 Block 的 flags 包含 BLOCK_IS_GC,则其为堆块。 \
/// 此时增加其引用计数,并返回这个源 Block
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
///Junes 4、源 Block 是全局块,直接返回源 Block(全局 Block 就是一个单例)
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
///Junes 5、源 Block 是一个栈 Block,执行拷贝操作。首先申请相同大小的内存
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
///Junes 6、使用 memmove 方法将栈区里的源 Block 逐位复制到刚申请的堆区 Block 内存中。这样做是为了保证完全复制所有元数据。
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
///Junes 7、更新 result 的 flags。
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed ///Junes 确保引用计数为 0。注释表示没这个必要,可能因为此时引用计数早已为 0。但是为了防止 bug 被保留下来。
result->flags |= BLOCK_NEEDS_FREE | 1; ///Junes 为 result 的 flags 添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1。表明这是一个堆 Block(一旦引用计数降为 0,则其内存将被回收)
///Junes 8、将 result 的 isa 指向 _NSConcreteMallocBlock。这意味着 result 是一个堆 Block。
result->isa = _NSConcreteMallocBlock;
///Junes 9、如果 result 存在拷贝协助方法,调用它。
/// 如果 block 捕获对象,编译器将会生成这个协助方法。
/// 这个协助方法将会 retain 被捕获的对象。
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock);
}
return result;
}
源码中对copy操作进行了分类
1、如果源Block不存在则返回NULL
2、如果源 Block 是 _NSConcreteMallocBlock
,增加其引用计数,然后返回源 Block;
3、如果源 Block 是 _NSConcreteGlobalBlock
,直接返回源 Block,因为_NSConcreteGlobalBlock
是一个单例;
4、如果源 Block 是 _NSConcreteStackBlock
,那么操作就比较复杂
- 申请一块相同大小的内存
- 拷贝栈上的block的所有元数据到新申请的内存空间上,也就是将数据拷贝到堆上,堆上的block我们叫做result
- 更新 result 的 flags,确保其引用计数为 0;
- 更新 result 的 flags,添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1;
- 将 result 的 isa 指向
_NSConcreteMallocBlock
。标明 result 是一个堆 Block;- 如果 result 捕获了对象,调用编译器生成的拷贝协助方法 retain 被捕获的对象。
既然我们的Block被拷贝了,那么Block中的捕获的变量也会一起被拷贝到堆区,这里我们直接看一下总结
这里需要注意被捕获的__block结构体,我们分析一下源码
bash
static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2;
/*
* 当拷贝目标为 __block 修饰变量而生成的结构体时,则执行
*/
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
struct Block_byref **destp = (struct Block_byref **)dest;
struct Block_byref *src = (struct Block_byref *)arg;
///Junes __block 变量结构体还在栈区,拷贝它
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
///Junes 判断这是否为一个弱引用
bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
///Junes 申请相同大小的空间
struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
///Junes 将新结构体标记为堆,并将引用计数置为 2。一份给调用者,一份给栈。
copy->flags = src->flags | _Byref_flag_initial_value;
///Juens 将栈区结构体与新结构体的 __forwarding 都之上堆区中的新结构体
copy->forwarding = copy;
src->forwarding = copy;
///Junes 赋值 size
copy->size = src->size;
///Junes 如果是弱引用,isa 指向 _NSConcreteWeakBlockVariable。标记为 Block 的弱引用
if (isWeak) {
copy->isa = &_NSConcreteWeakBlockVariable;
}
///Junes 如果存在 copy_dispose 内存管理方法,执行
if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
///Junes 将新结构体的内存管理方法指针指向全区源结构体的相应方法
copy->byref_keep = src->byref_keep;
copy->byref_destroy = src->byref_destroy;
///Junes 调用源结构体的 byref_keep 方法(也就是 _Block_object_assign),管理被捕获的对象内存。不过会加上 BLOCK_BYREF_CALLER 标记
(*src->byref_keep)(copy, src);
}
else {
///Junes 仅适用于普通变量(非对象),全字节拷贝 byref_keep
_Block_memmove(
(void *)©->byref_keep,
(void *)&src->byref_keep,
src->size - sizeof(struct Block_byref_header));
}
}
///Junes 这个结构体已经在堆区,引用计数 +1
else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
// assign byref data block pointer into new Block
///Junes 将源结构体指针也指向堆区的这个新结构体
*destp = src->forwarding; // _Block_assign(src->forwarding, (void **)destp);
}
这里解释了拷贝后forwarding
指针的变化
bash
copy->forwarding = copy;
src->forwarding = copy;
当拷贝到堆上后,栈上的forwarding
指向了堆上的新结构体,堆上的结构体仍然指向自身,这里就引出我们在前面一直遗留的问题,forwarding
指针的作用
七、__block 与 __forwarding
我们在前面知道了,如果一个block捕获了自由变量,编译器会自动将栈上的block拷贝到堆区,从一个 _NSConcreteStackBlock
变为一个 _NSConcreteMallocBlock
。
被 __block 标记的自由变量 local__blockB,Block 并不是简单的值拷贝,而是拷贝了 local__blockB 这个结构体(自动被重写成 sturct __Block_byref_local__blockB_0
的一个实例,而 __Block_byref_local__blockB_0.local__blockB
承载之前的自由变量 a)的指针。
当变量被拷贝到堆区后,我们会发现一个问题,现在有两个变量,一个在栈上一个在堆上,此时访问 local__blockB 到底是要访问栈上的 local__blockB 还是堆上的 local__blockB 呢?于是, __forwading
登场了。
我们来看一张经典的图
当一个变量被__block
修饰符声明时,编译器会将这个变量包装在一个结构体 中,这个结构体除了存储变量值外,还包含一个名为forwarding
的指针。这个forwarding
指针的主要功能是指向包含实际变量值的最新版本的地址。
初始化时,__block
变量仍然存储在栈上,forwarding
指向自身
被拷贝后栈上的forwarding
指向堆上的被拷贝的结构体,因为后续栈上的变量可能被释放
这也使无论变量在栈上还是在堆上,我们都能访问到正确的同一个变量
所以才会存在绕一大圈的访问方法:
bash
(local__blockB.__forwarding->local__blockB) += 10;
八、block发生copy的时机
- 手动copy
bash
void (^stackBlock)(void) = ^{
NSLog(@"This is a block on the stack.");
};
void (^heapBlock)(void) = [stackBlock copy]; // 明确复制到堆
- block作为函数返回值
bash
typedef void (^CompletionBlock)(void);
CompletionBlock myFunction() {
return [^{ NSLog(@"Block returned from a function."); } copy]; // 返回时复制到堆
}
CompletionBlock block = myFunction();
block();
- Block 被赋值给 __block 修饰的变量时
如果你将一个栈上的 Block 赋值给一个 __block 修饰的变量,编译器会自动将这个 Block 复制到堆上。__block 变量用于存储指向堆上 Block 的指针。
bash
__block int (^blockVar)(void);
int value = 42;
blockVar = ^{
return value; // 编译器会自动将该 Block 复制到堆上
};
NSLog(@"%d", blockVar()); // 输出 42
- Block 访问了 __block 修饰的变量时
如果 Block 内部访问了 __block 修饰的变量,编译器会自动将该 Block 复制到堆上,以确保变量在 Block 执行时是有效的。
bash
__block int value = 42;
int (^blockObj)(void) = ^{
value = 100; // 访问了 __block 变量,编译器会自动将该 Block 复制到堆上
return value;
};
NSLog(@"%d", blockObj()); // 输出 100
- Block 被 GCD API 持有时
如果你将一个 Block 传递给 Grand Central Dispatch (GCD) API (如 dispatch_async),GCD 会自动将该 Block 复制到堆上,以确保在异步执行期间 Block 是有效的。
bash
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
int value = 42;
dispatch_async(queue, ^{
NSLog(@"%d", value); // GCD 会自动将该 Block 复制到堆上
});
总结
Block的重点其实就在于捕获自动变量与使用__block修饰时forwarding
指针的变化,还有其发生拷贝的时机:使用__block变量,使用dispatch API, 手动copy与作为函数返回值
另外重要的还有block的循环引用,后面会一起总结循环引用