可以带着以下问题来阅读本文
基础问题
- 什么是 Block?请举例说明 Block 的使用场景。
- Block 如何捕获外部变量?请解释 Block 捕获变量的规则。
- 请解释 Block 的内存管理,特别是在非 ARC 和 ARC 环境下的区别。
进阶问题
__block
修饰符有什么作用?使用它有什么注意事项?- 请解释 Block 的三种类型及其特点。
高级问题
- 请解释 Block 的底层实现原理,包括如何从栈复制到堆。
__forwarding
指针的作用是什么?它如何支持__block
变量的内存管理?- 为什么要设计__block,__frowarding,其原理是什么?
1. 什么是block?什么是Block调用?
- Block是将函数及其上下文封装起来的对象。Block调用既是函数调用。
2. Block 的内存结构
Block 在底层是一个 Objective-C 对象,其内存结构包含以下主要部分:
-
Block 对象头部:
isa
指针:指向 Block 类的指针,表明这是一个对象。flags
:用于表示 Block 的一些属性,如是否已经被复制到堆上、是否有捕获的外部变量等。reserved
:保留字段,目前未使用。invoke
:函数指针,指向 Block 的执行代码,即 Block 体的实现。
-
Block 描述信息 (
descriptor
):size
:Block 结构体的大小。copy_helper
和dispose_helper
:当 Block 从栈复制到堆时,这两个函数指针用于管理捕获的外部变量的内存(如果有的话)。signature
:Block 的签名信息,用于描述 Block 的参数和返回值类型(可选)。
-
捕获的外部变量:
- Block 会捕获其定义时所在作用域的局部变量。这些变量的副本或引用会紧随 Block 对象头部和描述信息之后存储。
-
内存结构示例
假设有如下的 Block 定义:
objective-c
int a = 10;
void (^myBlock)(void) = ^{
NSLog(@"Value of a: %d", a);
};
其内存结构大致如下:
diff
+----------------------+
| isa 指针 |
+----------------------+
| flags |
+----------------------+
| reserved |
+----------------------+
| invoke |
+----------------------+
| descriptor |
| - size |
| - copy_helper |
| - dispose_helper|
| - signature |
+----------------------+
| 捕获的外部变量 a |
+----------------------+
3. block的内存管理
1. Block 的三种类型
Block 在内存中有三种类型,它们的内存管理方式不同:
- _NSConcreteGlobalBlock:全局 Block,存储在全局数据区,不需要手动管理内存。
- _NSConcreteStackBlock:栈 Block,存储在栈上,当离开定义它的作用域时会被自动销毁。
- _NSConcreteMallocBlock:堆 Block,存储在堆上,需要手动管理内存(在非 ARC 环境下)。
当 Block 发生拷贝时,不同类型的 Block 会有不同的行为和变化:
- 全局 Block(_NSConcreteGlobalBlock)
- 全局 Block 存储在全局数据区,不捕获任何外部变量。
- 当全局 Block 被复制时,实际上并不会发生真正的内存拷贝操作,而是简单地返回 Block 本身的指针。因此,全局 Block 的拷贝不会引起任何内存或状态的变化。
- 栈 Block(_NSConcreteStackBlock)
- 栈 Block 存储在栈上,它可能捕获外部变量。
- 当栈 Block 被复制到堆上时(例如,通过 Block_copy 函数或在 ARC 下赋值给强引用),会发生以下变化:
- 为 Block 分配堆内存,并将栈 Block 的内容(包括捕获的外部变量)复制到堆上的新位置。
- 如果 Block 捕获了
__block
变量,这些变量也会被一起复制到堆上,并且它们的__forwarding
指针会被更新,以确保后续对这些变量的访问和修改都指向堆上的副本。 - 复制后的 Block 成为堆 Block(_NSConcreteMallocBlock),其生命周期由引用计数管理。
- 堆 Block(_NSConcreteMallocBlock)
- 堆 Block 存储在堆上,它的生命周期由引用计数管理。
- 当堆 Block 被复制时,在 ARC 环境下,引用计数会增加,但不会发生实际的内存拷贝操作。在非 ARC 环境下,需要手动管理 Block 的引用计数和内存释放。
总结:
- 当 Block 发生拷贝时,全局 Block 不会发生变化,栈 Block 会被复制到堆上,并成为堆 Block,而堆 Block 的引用计数会增加。
4. block捕获变量
在 Objective-C 中,Block 可以捕获其定义时所在作用域的外部变量。这个特性使得 Block 能够访问并使用在其外部定义的变量。以下是 Block 捕获外部变量的一些细节:
捕获方式
-
基本数据类型变量:对于基本数据类型(如 int、float 等)的局部变量,Block 会捕获其值的副本。这意味着在 Block 内部对这些变量的修改不会影响原始变量的值。
objective-cint value = 10; void (^myBlock)(void) = ^{ NSLog(@"Value inside block: %d", value); }; value = 20; myBlock(); // 输出 "Value inside block: 10"
在这个例子中,
value
在 Block 内部的值仍然是 10,即使在 Block 外部value
被修改为 20。 -
对象类型变量:对于对象类型的局部变量,Block 会捕获对对象的强引用。这意味着在 Block 内部可以访问和修改对象的属性,这些修改会反映到原始对象上。
objective-cNSMutableArray *array = [NSMutableArray arrayWithObjects:@"a", @"b", nil]; void (^myBlock)(void) = ^{ [array addObject:@"c"]; }; myBlock(); NSLog(@"Array: %@", array); // 输出 "Array: (a, b, c)"
在这个例子中,
array
在 Block 内部被修改,添加了一个新元素 "c"。
总结
Block 捕获外部变量的能力使得它们非常灵活和强大,特别是在异步编程和回调中。正确理解 Block 如何捕获和使用外部变量对于编写正确和高效的代码非常重要。
5. block如何修改外部变量值:__block
__block
的示例
如果你需要在 Block 内部修改一个基本数据类型的局部变量的值,你可以使用 __block
修饰符。这样,Block 会通过引用而不是值来捕获这个变量,从而允许在 Block 内部对变量进行修改。
objective-c
__block int value = 10;
void (^myBlock)(void) = ^{
value = 20; // 修改 __block 变量的值
};
myBlock();
NSLog(@"Value: %d", value); // 输出 "Value: 20"
在这个例子中,使用 __block
修饰符允许 Block 修改 value
的值。
__block
是什么?
在 Objective-C 中,__block
是一个存储类型修饰符,用于修饰在 Block 中使用的变量。当一个变量被 __block
修饰时,它允许在 Block 内部对该变量进行修改。
原理
- 封装成结构体 :使用
__block
修饰的变量会被编译器封装成一个结构体。这个结构体包含变量的值和一个__forwarding
指针,用于支持变量从栈复制到堆的过程。 - 引用捕获 :与普通局部变量通过值捕获不同,
__block
变量是通过引用捕获的。这意味着 Block 内部访问的是__block
变量的地址,而不是它的副本。 - 栈到堆的复制 :当 Block 被复制到堆上时(例如,赋值给强引用变量或作为函数返回值),所有被 Block 捕获的
__block
变量也会被复制到堆上。复制过程中,__forwarding
指针会被更新,指向堆上的变量副本。
为什么需要 __block
?
- 修改外部变量 :在没有
__block
修饰符的情况下,Block 只能捕获外部变量的值,而不能修改这些变量。__block
允许在 Block 内部修改外部变量的值。 - 支持变量的生命周期 :当 Block 被复制到堆上时,
__block
变量也会被复制到堆上,以确保在 Block 的生命周期内,变量仍然有效。 - 简化内存管理 :在自动引用计数(ARC)环境下,
__block
变量的内存管理会被自动处理,包括当变量被复制到堆上时的内存管理。 - 适应异步编程模式 :在异步编程中,经常需要在 Block 中修改外部变量以存储异步操作的结果。
__block
使得这种模式更加容易实现。
总的来说,__block
修饰符是为了增强 Block 的功能,使其能够修改捕获的外部变量。这在异步编程、回调处理以及其他需要在 Block 中修改外部状态的场景中非常有用。了解 __block
的原理和作用有助于更有效地使用 Block 和管理内存。
6. __forwarding
__forwarding
是一个在 Objective-C 中与 __block
变量相关的机制。它是 __block
变量结构体中的一个指针,用于确保无论变量是在栈上还是堆上,对该变量的访问和修改都能正确进行。
原理
__block
变量的封装 :使用__block
修饰符声明的变量会被编译器封装成一个结构体,该结构体包含变量的值和一个__forwarding
指针。- 指针指向自身 :初始时,
__block
变量的__forwarding
指针指向变量自身(即指向结构体本身)。 - 复制到堆上 :当 Block 被复制到堆上时,所有被 Block 捕获的
__block
变量也会被复制到堆上。此时,__forwarding
指针会被更新,指向堆上的变量副本。 - 统一访问方式 :无论
__block
变量是在栈上还是堆上,对它的访问和修改都通过__forwarding
指针进行。这确保了变量的访问和修改总是指向正确的存储位置。
为什么需要 __forwarding
- 保持访问一致性 :
__forwarding
机制确保在 Block 中对__block
变量的访问和修改总是一致的,无论变量是在栈上还是堆上。 - 支持栈到堆的迁移 :当 Block 和
__block
变量从栈复制到堆时,__forwarding
指针使得变量的迁移过程更加平滑。它保持了对变量的引用不变,即使变量的存储位置发生了变化。 - 简化内存管理 :
__forwarding
机制简化了__block
变量在栈和堆之间迁移时的内存管理。开发者不需要关心变量的具体存储位置,只需通过__forwarding
指针进行访问和修改。
总的来说,__forwarding
是 __block
变量机制的一个重要组成部分,它确保了在 Block 中使用 __block
变量时的访问一致性和内存管理的简化。了解 __forwarding
的原理和作用有助于更深入地理解 Block 和 __block
变量的工作机制。
总结
__block
修饰符的原理涉及到 __block
变量的存储方式、捕获机制和内部结构。通过将变量封装为结构体并通过引用捕获,__block
允许在 Block 中修改外部变量的值。在 Block 被复制到堆上时,__forwarding
指针确保对变量的访问和修改都通过同一个地址进行,确保了数据的一致性和正确性。