基础
首先,理解 Block 的本质:Block 本质上是一个自包含的匿名函数代码块,可以捕获上下文中的常量或变量,像普通对象一样在代码中传递和使用,这在 iOS 开发中非常常见。
全局函数和嵌套函数实际上也是特殊的闭包。闭包有三种形态:
- 全局函数:有名称,但不捕获任何值。
- 嵌套函数:有名称,并且能捕获它所在封闭函数域中的值。
- 闭包表达式:用轻量级语法编写的匿名闭包,能够捕获上下文中的变量或常量。
OC-Block
分类
OC 中的 Block 根据内存位置和引用方式可以分为三类:
NSGlobalBlock
- 位于全局区。
- 内部不使用外部变量,或者只使用静态变量和全局变量。
NSMallocBlock
- 位于堆区。
- 被强持有。
- 内部使用局部变量或 OC 属性,可以赋值给用强引用或 copy 修饰的变量。
NSStackBlock
- 位于栈区。
- 没有被强持有。
- 内部使用局部变量或 OC 属性,不能赋值给强引用或 copy 修饰的变量。
下面这个 demo 很直观地展示了这三种 Block:
int a = 10; // 局部变量
void(^Global)(void) = ^{
NSLog(@"Global");
};
void(^Malloc)(void) = ^{
NSLog(@"Malloc,%d",a);
};
void(^__weak Stack)(void) = ^{
NSLog(@"Stack,%d",a);
};
NSLog(@"%@",Global); // <__NSGlobalBlock__: 0x101aa80b0>
NSLog(@"%@",Malloc); // <__NSMallocBlock__: 0x600003187900>
NSLog(@"%@",Stack); // <__NSStackBlock__: 0x7ff7b12c22f0>
接下来重点聊聊堆 Block(NSMallocBlock)的内存管理细节。
NSMallocBlock
Block 何时会被拷贝到堆上?有几个关键时机:
- 手动调用
copy。 - Block 作为函数返回值。
- 被强引用或
copy修饰的变量持有。 - 系统 API 中使用了 using Block 的场景(如 GCD、动画等)。
判断堆 Block 的依据可以总结为:
- Block 内部是否使用了外部变量。
- 使用的变量类型:局部变量、OC 属性、全局变量、静态变量?
- 是否被强引用或
copy修饰。
源码探究
拿一个捕获了局部变量的 Block 来拆解其底层实现:
#importvoid test() { int a = 10; void(^Malloc)(void) = ^{ NSLog(@"%d",a); }; }
通过 clang -rewrite-objc main.m -o main.cpp 生成 C++ 代码,可以清晰看到 Block 的结构:
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a; // 内部存储了变量a
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 创建Malloc闭包
void(*Malloc)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a));
// 函数实现
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog(···);
}

打开 LLVM 调试可以看到,这个 Block 原本分配在栈上,随后调用了 objc_retainBlock 方法,而它实际调用的是 _Block_copy 方法。在 Block.h 的源码中,_Block_copy 的注释明确指出:“创建一个基于堆的 Block 副本,或者简单地添加一个对现有 Block 的引用。” 其核心实现如下:
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, true);
}
static void *_Block_copy_internal(const void *arg, const bool wantsOne) {
struct Block_layout *aBlock;
aBlock = (struct Block_layout *)arg;
// 分配堆内存
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// isa重新标记为Malloc Block
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;
}
Block 的底层结构体是 Block_layout:
struct Block_layout {
void *isa;
volatile int32_t flags;
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
};
一句话总结:Block 在运行时才被拷贝到堆上,会在堆中开辟新的内存空间,从而延长其生命周期。
循环引用
解决方案
__weak + __strong
思路很简单:在 Block 内部短暂持有 self 的生命周期,利用 weak 自动置空机制避免循环引用。示例代码如下:
self.name = @"YK";
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.callFunc();
};
__block
另一种思路:通过 __block 进行值拷贝,但需要手动在适当位置将变量置空来打破循环。
来看一个使用 __block 的例子,并分析其 C++ 代码:
void test() {
__block int a = 10;
void(^Malloc)(void) = ^{
a++;
NSLog(@"%d",a);
};
Malloc();
}
// 注意第三个参数传入的是__Block_byref_a_0结构体类型的a变量地址
void(*Malloc)(void) =
((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__attribute__((__blocks__(byref))) __Block_byref_a_0 a =
{(void*)0, (__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a; // 值拷贝存储
};
__block 的原理可以概括为:
- 系统自动创建
__Block_byref_a_0结构体。 - 将结构体的指针传给 Block。
- Block 内部修改的是与原始值同一片内存空间,从而实现变量在 Block 内外共享。
注意点
根据上面的分析很容易得出一个结论:在 OC 的 Block 中,如果捕获的外部变量没有加 __block,编译时就会把变量值拷贝到 Block 内部(值拷贝);如果加了 __block,则传入的是变量指针对应的内存地址(引用拷贝)。看下面这段代码验证:
int a = 1;
__block int b = 2;
void(^Malloc)(void) = ^{
NSLog(@"a,%d",a);
NSLog(@"b,%d",b);
};
a = 3;
b = 4;
Malloc();
// 输出:
// a,1
// b,4
Swift-Closure
- Swift 的闭包表达式语法极为简洁,常见的优化写法包括:
- 利用上下文推断参数类型和返回值类型。
- 单表达式闭包隐式返回,可以省略
return。 - 参数名称缩写:用
$0、$1表示按顺序的参数。 - 尾随闭包:如果函数的最后一个参数是闭包,可以写在括号外面,显著提升可读性。
- Swift 的闭包是引用类型。这一点可以通过检查是否调用
swift_allocObject来验证:
// 未调用swift_allocObject
let closure1 = { () -> () in
print("closure1")
}
// 调用swift_allocObject
let a = 10
let closure2 = { () -> () in
print("closure2 (a)")
}
捕获值
- 如果在闭包中用
[variable1, variable2]的形式捕获外部变量,捕获到的变量是let类型,不可变。 - 如果直接捕获外部变量,获取的是指针,闭包内修改会影响到原始变量。
- 如果捕获的是引用类型(比如
Class实例),无论是否使用[],修改对象的属性都会影响原始对象。
简单验证:
var variable = 10
let closure = { () -> () in
variable += 1
print("closure (variable)")
}
closure() // closure 11
print(variable) // 11
这里直接捕获了变量,因此闭包内修改了原始值。如果改成用 [variable] 则编译报错:“可变运算符的左侧不可变”。
var variable = 10
let closure = { [variable] () -> () in
variable += 1 // ❌ 编译报错
print("closure (variable)")
}
再看引用类型的例子:
class YKClass {
var name = "old"
}
let demoC = YKClass()
let closure1 = { [demoC] () -> () in
demoC.name = "new"
print("closure1 (demoC.name)")
}
closure1() // closure1 new
print(demoC.name) // new
let closure2 = { () -> () in
demoC.name = "new2"
print("closure2 (demoC.name)")
}
closure2() // closure2 new2
print(demoC.name) // new2
可以看到,即使使用了 [demoC] 捕获,依然能修改对象的属性,因为引用类型的“不可变”仅指指针本身不能改变,但指针指向的对象内容仍然可以修改。
