☰
Block本质:编译器生成的结构体而非匿名函数
2026/10/14 10:05:41 网站建设 项目流程

1. 为什么Block不是“匿名函数”,而是编译器生成的结构体实例

很多人第一次接触Block时,会下意识把它类比成JavaScript里的箭头函数、Python里的lambda表达式——写法简洁、能捕获外部变量、可以当参数传。但这种类比在iOS开发中不仅不准确,而且非常危险。我带过几期移动端进阶训练营,几乎每期都有学员在面试时被问到“Block为什么会造成循环引用”,结果答成“因为闭包会持有外部作用域”,然后被追问“那Swift Closure是不是也一样?Objective-C里为什么ARC不能自动解决?”就卡壳了。问题就出在这里:Block根本不是语言层面的“闭包语法糖”,它是Clang编译器在预处理阶段硬生生构造出来的一个C结构体+函数指针组合体,而这个结构体的内存布局、字段语义、生命周期管理规则,全部由编译器严格定义,和ARC、Runtime、堆栈分配机制深度耦合。

举个最直观的例子。你写这样一段代码:

int x = 10; void (^myBlock)(void) = ^{ NSLog(@"x = %d", x); };

Clang不会把它编译成一个“函数对象”,而是生成类似这样的C结构体(简化示意):

struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int x; // 捕获的外部变量,按值拷贝 }; struct __block_impl { void *isa; // 指向Class对象,用于支持retain/release int Flags; int Reserved; void *FuncPtr; // 指向实际执行体的函数指针 }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int x = __cself->x; // 从结构体字段读取捕获值 NSLog(@"x = %d", x); }

注意两个关键点:第一,x不是动态从栈上查找,而是被静态复制进结构体字段;第二,impl.isa字段真实存在,且指向一个真实的NSStackBlock或NSMallocBlock类——这意味着Block实例本身就是一个Objective-C对象,它参与ARC管理、能响应retain/copy消息、甚至能调用class方法。我在某跨平台SDK重构项目中就踩过坑:把Block当普通函数指针传给C层回调,结果在Block内部访问self时崩溃,调试发现isa字段是野指针——因为Block还在栈上,而回调发生时栈帧已销毁。后来加了一行[block copy]才稳定,但这行代码背后,是编译器生成的__main_block_copy_0函数在起作用,它负责把栈上的Block复制到堆,并正确设置isa指向NSMallocBlock。

所以,“Block本质”第一个必须厘清的认知是:它不是语法特性,而是编译器为实现闭包语义而强制注入的一套C结构体协议。这个协议规定了三件事:如何捕获变量(by value / by reference)、如何存储(stack / heap / global)、如何管理生命周期(copy / dispose)。理解这点,才能真正看懂__block修饰符为什么能修改外部变量,为什么__weak typeof(self) weakSelf = self;能破循环,而不是靠死记硬背“弱引用避免强引用”。

提示:你可以用clang -rewrite-objc xxx.m命令把任意含Block的.m文件转成.cpp,亲眼看到编译器生成的完整结构体定义和函数实现。这是验证Block本质最直接的方式,比任何文档都可靠。

2. Block的三种类型:栈、堆、全局,它们的内存行为差异远超想象

Block在运行时有且仅有三种类型:NSStackBlock、NSMallocBlock、NSGlobalBlock。很多开发者只知道“栈Block要copy,否则出作用域就失效”,但不知道这三者在内存分配策略、ARC行为、甚至sizeof()结果上都存在根本性差异。我在某图像处理Demo中曾遇到一个诡异问题:同一个Block,在Debug模式下正常,在Release模式下崩溃。最后定位到是编译器优化级别影响了Block的类型判定——这恰恰说明,Block类型不是开发者写的,而是编译器根据上下文静态推导出来的。

我们来逐个拆解这三类Block的生成条件和行为特征:

2.1 NSGlobalBlock:只读常量,零开销

当Block不捕获任何外部变量(包括self、局部变量、全局变量),它会被编译为NSGlobalBlock。例如:

void (^globalBlock)(void) = ^{ NSLog(@"Hello"); }; // 或 NSString *str = @"abc"; void (^globalBlock2)(void) = ^{ NSLog(@"%@", str); }; // str是字面量,编译期确定

这类Block的特点是:

  • 内存位于数据段(.data section),和C函数一样,整个进程生命周期只有一份;
  • sizeof()返回固定值(通常64字节左右),与捕获变量无关;
  • copy操作是空操作,返回自身地址;
  • retain/release无效,isa指向NSGlobalBlock类,但该类的retain方法直接返回self。

实测验证:在Xcode中打印[globalBlock class],输出NSGlobalBlock;对它连续调用100次copy,地址始终不变。这说明它本质上就是一段只读代码,没有任何运行时状态需要管理。

2.2 NSStackBlock:栈上快照,危险但高效

只要Block捕获了栈变量(非__block修饰),它默认就是NSStackBlock。例如:

int a = 1; void (^stackBlock)(void) = ^{ NSLog(@"a = %d", a); // 捕获栈变量a };

关键特征:

  • 内存分配在当前函数栈帧内,随函数返回自动销毁;
  • 结构体中isa字段指向NSStackBlock类,但该类的retain方法会触发_Block_copy,将其升级为NSMallocBlock;
  • sizeof()结果随捕获变量数量线性增长(每个int变量+4字节,指针+8字节);
  • 最大风险:栈Block不能作为返回值传出。常见错误写法:
// ❌ 危险!返回栈Block指针,调用方拿到的是野指针 - (void(^)(void))getStackBlock { int x = 10; return ^{ NSLog(@"%d", x); }; }

Clang在编译时会警告Returning block that lives on the local stack,但很多团队禁用了该警告,导致线上偶发崩溃。

2.3 NSMallocBlock:堆上实体,ARC可管可控

NSMallocBlock是Block的“完全体”,它具备完整的Objective-C对象行为:

  • 内存分配在堆区,由malloc分配,受ARC管理;
  • retainCount有效,retain/release会增减引用计数;
  • copy操作返回新地址(深拷贝结构体),dispose函数负责释放捕获的OC对象;
  • isa指向NSMallocBlock类,该类实现了完整的内存管理方法。

它如何产生?两种途径:

  1. 显式copy:[stackBlock copy]或[globalBlock copy](后者虽无实际分配,但返回类型统一为NSMallocBlock);
  2. 隐式copy:当Block作为参数传入系统API时,如dispatch_async、NSArray enumerateObjectsUsingBlock:等,这些API内部会调用Block_copy。

这里有个重要细节:NSMallocBlock的dispose函数不是编译器自动生成的,而是由__main_block_desc_0结构体中的dispose字段指向。该函数由编译器根据捕获的变量类型生成,例如捕获了NSString *,dispose就会调用objc_release;捕获了__block变量,则会调用_Block_byref_release。我在某SDK性能优化中发现,一个频繁创建又释放的Block,dispose函数耗时占总耗时15%,原因就是它捕获了5个NSDictionary,每次释放都要遍历调用objc_release。后来改用__block修饰其中3个,让它们共享引用,dispose逻辑大幅简化。

Block类型分配位置是否可copyretainCount是否有效sizeof()是否固定典型场景
NSGlobalBlock数据段是(空操作)否是纯函数式Block,无捕获
NSStackBlock栈是(升级)否(调用即升级)否(随捕获变量变)局部作用域内使用,未传出
NSMallocBlock堆是(深拷贝)是是(固定大小)作为返回值、传入系统API、长期持有

注意:__block修饰的变量本身不决定Block类型,但它会影响dispose函数的复杂度。__block变量被封装在__Block_byref_xxx结构体中,该结构体有自己的引用计数,Block的dispose函数需调用_Block_byref_release来管理它。

3. __block变量的底层机制:不是“引用传递”,而是独立结构体托管

几乎所有讲Block的文章都会说“__block让变量变成引用传递”,但这句话严重误导。__block修饰的变量既不是C语言的指针,也不是Objective-C的对象引用,而是编译器为它单独生成一个堆分配的结构体,Block通过指针访问这个结构体。这个设计初衷是为了支持Block内外对同一变量的双向修改,同时保证内存安全。我在某实时音视频SDK中就依赖这个特性:主线程配置参数,子线程Block回调中动态更新,双方都能看到最新值。

我们来看__block的实际编译效果。原始代码:

__block int counter = 0; void (^block)(void) = ^{ counter++; // 修改__block变量 }; counter = 100; // 主线程修改 block(); // 输出101

Clang生成的核心结构体如下(简化):

struct __Block_byref_counter_0 { void *__isa; // 指向_NSConcreteStackBlock或_NSConcreteMallocBlock struct __Block_byref_counter_0 *__forwarding; // 关键!指向自身或堆拷贝 int __flags; int __size; int counter; // 实际存储的变量值 }; // Block结构体中不再直接存counter,而是存指向__Block_byref_counter_0的指针 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; struct __Block_byref_counter_0 *counter; // 注意:是指针! };

关键点在于__forwarding字段。当__block变量在栈上时,__forwarding指向自身;一旦Block被copy到堆,编译器会调用_Block_byref_copy,在堆上分配新结构体,将原栈结构体内容复制过去,并让所有相关Block的counter指针指向新的堆结构体,同时更新原栈结构体的__forwarding指向堆结构体。这样,无论访问栈上还是堆上的__block变量,最终都通过__forwarding找到唯一真实数据。

这个机制带来两个重要结论:

  1. __block变量的修改是线程安全的吗?不是。__forwarding指针的更新、结构体复制过程都不是原子操作。如果多个线程同时对同一__block变量做copy和修改,可能造成数据竞争。我在某高并发日志模块中就遇到过:多个GCD队列同时触发__block NSMutableArray *logs的copy,导致__forwarding指向不一致,部分日志丢失。解决方案是加锁,或改用dispatch_once初始化堆结构体。
  2. __block能修饰OC对象吗?可以,但要注意:__block id obj捕获的是对象指针,__block结构体中存的是id类型字段,dispose函数会调用objc_release。但如果对象本身是__weak,__block不会改变其弱引用语义——这常被误认为“__block能破循环”,其实它只是让Block能修改weakSelf指针本身(比如置为nil),而非避免对self的强引用。

实操技巧:当你需要在Block中修改外部变量,且该变量可能被多个Block共享时,优先考虑__block;但如果只是避免循环引用,__weak+__strong组合更轻量。__block的代价是额外的堆分配和__forwarding跳转,而__weak只是ARC层面的弱引用标记。

4. 循环引用的根因与破局:为什么weakSelf/strongSelf模式不是银弹

“Block循环引用”是iOS开发中最经典的陷阱,但绝大多数教程只教“用__weak typeof(self) weakSelf = self;”,却不说清为什么这个模式有时依然会崩溃。我在某电商App的购物车模块重构中就遇到过:明明写了weakSelf,但在网络请求回调中访问weakSelf.view时仍崩溃。调试发现,weakSelf在Block执行前已被释放,view访问触发EXC_BAD_ACCESS。这说明,weakSelf/strongSelf模式解决的是“强引用链”,但没解决“对象提前释放”的时序问题。

我们先看循环引用的形成原理。典型场景:

// self持有一个Block属性 @property (nonatomic, copy) void(^completionBlock)(void); // 在方法中赋值 self.completionBlock = ^{ [self doSomething]; // Block强引用self }; // self -> completionBlock -> self,闭环形成

此时self的retainCount永远不会归零。ARC无法释放self,self也无法释放Block,形成内存泄漏。

__weak方案的原理是:weakSelf是一个弱引用,不增加self的retainCount,从而打破引用链。但它的局限性在于:

  • weakSelf可能为nil,访问weakSelf.xxx前必须判空;
  • 如果Block执行时间较长,weakSelf可能在Block执行中途被释放,后续访问weakSelf.xxx仍会崩溃;
  • 对于需要多次访问self的长Block,反复判空代码冗余。

strongSelf模式(__strong typeof(weakSelf) strongSelf = weakSelf;)试图解决第二个问题:在Block开头将weakSelf转为强引用,确保Block执行期间self不被释放。但它引入新风险:如果weakSelf非nil,strongSelf会重新建立强引用,若Block执行时间极长,可能延迟self释放,造成内存驻留。

真正的破局思路,是分层解耦引用关系。我在某IM SDK中采用的方案是:

4.1 业务层:用delegate替代Block回调

对于需要长期持有回调的场景(如连接状态监听),改用<IMConnectionDelegate>协议,由调用方实现connectionDidConnect:方法。self持有了IMConnection对象,但IMConnection只持有了delegate弱引用(@property (nonatomic, weak) id<IMConnectionDelegate> delegate;),彻底避免循环。

4.2 网络层:用Completion Token管理生命周期

对于单次网络请求,不直接传Block,而是返回一个RequestToken对象:

@interface RequestToken : NSObject @property (nonatomic, weak) id target; // 弱引用目标对象 @property (nonatomic, copy) void(^completion)(id); // 强引用Block @end // 使用时 RequestToken *token = [networkManager requestWithParams:params]; token.target = self; // 弱引用 token.completion = ^(id response) { if (token.target) { // 安全访问 [(UIViewController*)token.target updateUI:response]; } };

RequestToken在请求完成或取消时自动置空target和completion,确保无残留引用。

4.3 UI层:用View Model隔离

对于ViewController中大量Block操作UI的场景,将UI逻辑抽离到ViewModel,ViewController持ViewModel强引用,ViewModel持ViewController弱引用(通过@property (nonatomic, weak) UIViewController *owner;)。Block全部在ViewModel内部定义,只捕获owner,而owner是弱引用,无循环。

这三种方案的共同点是:不依赖__weak/__strong的临时规避,而是从架构上切断强引用路径。weakSelf/strongSelf只是战术级补丁,而分层解耦是战略级设计。我在某金融App的K线图模块中,用View Model方案将ViewController内存占用降低了40%,因为ViewModel可以复用,且不随VC销毁而重建。

经验总结:判断是否该用weakSelf,先问三个问题:1)Block是否可能比self生命周期长?2)Block执行中是否有多次访问self?3)self是否在Block执行前就可能被释放?如果答案都是“是”,那么weakSelf不够,必须用架构手段。

5. Block的高级应用:如何安全地在多线程间传递并执行

Block最强大的能力之一,是作为“可执行单元”在不同线程间传递。但直接传递NSStackBlock会导致崩溃,NSMallocBlock又面临线程安全问题。我在某跨平台渲染引擎中,需要将用户自定义的像素处理Block从主线程传入渲染线程执行,初期方案是简单copy后dispatch_async,结果在高负载下频繁崩溃。根源在于:copy操作本身不是线程安全的,且Block捕获的OC对象可能被多线程同时访问。

安全传递Block的完整流程,必须包含四个环节:

5.1 类型校验与强制升级

在Block传出前,必须确保它是NSMallocBlock。不能依赖copy,因为NSGlobalBlock的copy是空操作,NSStackBlock的copy可能触发竞态。正确做法是:

// 安全升级函数 static inline id safeBlockCopy(id block) { if (!block) return nil; // 检查是否已是堆Block if (strcmp(object_getClassName(block), "NSMallocBlock") == 0) { return [block retain]; // 直接retain,避免重复copy } // 否则强制copy return [block copy]; } // 使用 id safeBlock = safeBlockCopy(userBlock);

5.2 捕获对象的线程安全封装

Block捕获的OC对象(如NSArray、NSDictionary)默认不是线程安全的。如果Block在子线程执行时,主线程同时修改这些对象,会引发EXC_BAD_ACCESS。解决方案是:

  • 不可变封装:对捕获的集合类,用[array copy]或[dict immutableCopy]生成不可变副本;
  • 线程局部存储:对需要修改的对象,用__thread关键字声明线程局部变量,或用NSThread的threadDictionary;
  • 同步访问:在Block执行前,对捕获对象加锁(@synchronized(obj)),但会降低并发性能。

我在渲染引擎中采用的是第一种:所有传入Block的UIImage、NSData都强制copy,确保子线程看到的是稳定快照。

5.3 执行环境的上下文绑定

Block执行时,常需访问self的某些属性(如self.delegate)。但self可能在Block执行前被释放。除了weakSelf,更健壮的做法是在Block创建时,将所需上下文数据序列化:

// 不推荐:捕获self self.processBlock = ^{ [self.delegate processFinished:self.result]; }; // 推荐:捕获必要数据 __weak typeof(self) weakSelf = self; self.processBlock = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf && strongSelf.delegate) { // 仅访问必需字段,避免调用方法 id delegate = strongSelf.delegate; id result = strongSelf.result; [delegate processFinished:result]; } };

5.4 执行结果的异步回传

Block执行结果需回传到主线程更新UI,但直接dispatch_main_async可能因self释放而崩溃。安全模式是:

// 在Block执行完毕后 dispatch_async(dispatch_get_main_queue(), ^{ if (weakSelf && weakSelf.view) { [weakSelf updateUIWithResult:result]; } });

或者,用NSOperationQueue的addOperationWithBlock:,配合queue.cancelAllOperations在VC销毁时取消所有待执行Block。

这套方案在某直播App的美颜滤镜模块中稳定运行两年,日均处理200万次Block调度,零崩溃。核心经验是:不要假设Block的执行环境是安全的,而要在每个环节主动加固——从创建、传递、执行到回传,每一步都做防御性检查。

最后提醒:dispatch_block_t是void(^)(void)的别名,它本身不解决线程安全问题。dispatch_async只是把Block放入队列,真正的安全取决于Block内部逻辑。很多开发者以为用了GCD就万事大吉,结果在Block里直接访问NSMutableArray,照样崩溃。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询