iOS Block 底层深度解析:结构、变量捕获、copy逻辑与循环引用本质 在iOS开发中Block是Objective-C以下简称OC的核心特性之一也是面试高频考点——从日常的UI回调、网络请求回调到GCD异步任务Block无处不在。但很多开发者对Block的认知仅停留在“匿名函数”的表层不清楚其底层结构、变量捕获的规则、copy的底层逻辑更难精准定位循环引用的本质导致开发中频繁出现内存泄漏、崩溃等问题。本文将从底层原理出发结合objc4-818.2源码适配iOS 13逐一对Block的底层结构、变量捕获规则、copy逻辑、循环引用本质四大核心点进行拆解每个知识点都搭配可直接在Xcode中运行的实战示例全程无冗余、重点突出既适合新手入门也适合开发者查漏补缺、深化理解。前置说明本文聚焦OC中的BlockSwift中的Block闭包有自身的底层实现暂不展开所有示例均基于64位架构32位已淘汰涉及的源码均做简化处理保留核心逻辑便于理解文中涉及的内存地址、运行结果可直接复制代码到Xcode中验证。一、底层结构Block本质是什么源码拆解很多人误以为Block是“函数”但从底层来看Block的本质是一个OC对象——它继承自NSObject有自己的isa指针内部封装了“函数实现地址”和“捕获的变量”是一个“带状态的函数对象”。1. Block底层结构体objc4源码简化版在objc4源码中Block的核心结构体是struct __block_impl和struct __XXX_block_impl_0XXX为Block所在的函数名编译器自动生成简化后如下// Block的基础结构体所有Block都包含该结构体 struct __block_impl { void *isa; // isa指针标识Block的类型如__NSGlobalBlock__、__NSStackBlock__、__NSMallocBlock__ int Flags; // 标志位存储Block的状态如是否可copy、是否已copy等 int Reserved; // 保留字段用于后续扩展 void *FuncPtr; // 函数指针指向Block的具体实现代码 }; // 自定义Block的结构体编译器自动生成命名格式为__函数名_block_impl_0 struct __main_block_impl_0 { struct __block_impl impl; // 内嵌基础结构体包含isa、函数指针等核心信息 struct __main_block_desc_0* Desc; // 描述信息结构体存储Block的大小、copy/destroy函数等 // 这里存储Block捕获的变量捕获的变量会作为结构体成员存在 int a; // 示例捕获的auto变量a __weak id weakSelf; // 示例捕获的weak指针weakSelf }; // Block描述信息结构体存储Block的元数据 struct __main_block_desc_0 { size_t reserved; // 保留字段 size_t Block_size; // Block的内存大小 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); // copy函数指针 void (*dispose)(struct __main_block_impl_0*); // 释放函数指针 };2. Block的三种类型关键区分根据Block的存储位置和isa指针指向Block分为3种类型不同类型的copy逻辑、生命周期完全不同这也是后续copy逻辑和内存管理的核心基础Block类型存储位置isa指向核心特点全局Block__NSGlobalBlock__全局数据区_NSGlobalBlock不捕获任何变量生命周期与程序一致无需copy栈Block__NSStackBlock__栈内存_NSStackBlock捕获auto变量生命周期随栈帧销毁而销毁需copy到堆堆Block__NSMallocBlock__堆内存_NSMallocBlock由栈Block copy而来生命周期由引用计数管理需手动管理内存3. 实战示例1区分三种Block类型通过代码打印Block的类型直观理解三种Block的区别可直接复制运行#import UIKit/UIKit.h #import objc/runtime.h int main(int argc, char * argv[]) { NSString * appDelegateClassName; autoreleasepool { // 1. 全局Block不捕获任何变量 void (^globalBlock)(void) ^{ NSLog(全局Block不捕获任何变量); }; NSLog(全局Block类型%, object_getClass(globalBlock)); // 2. 栈Block捕获auto变量未copy int a 10; void (^stackBlock)(void) ^{ NSLog(栈Block捕获auto变量a %d, a); }; NSLog(栈Block类型%, object_getClass(stackBlock)); // 3. 堆Block将栈Block copy到堆 void (^mallocBlock)(void) [stackBlock copy]; NSLog(堆Block类型%, object_getClass(mallocBlock)); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }运行结果全局Block类型__NSGlobalBlock__ 栈Block类型__NSStackBlock__ 堆Block类型__NSMallocBlock__补充说明ARC环境下某些场景如将Block赋值给strong指针会自动触发copy将栈Block转为堆Block后续会详细讲解。二、变量捕获Block如何“记住”外部变量核心规则Block的核心特性之一是“捕获外部变量”即Block内部可以访问外部的变量但并非所有变量都会被捕获捕获规则由变量的存储类型决定auto、static、全局变量不同存储类型的变量捕获方式和生命周期完全不同。核心原则Block只捕获“会被销毁的变量”全局变量、static变量不会被销毁因此Block不捕获其值而是直接访问其地址auto变量会随栈帧销毁因此Block会捕获其值形成副本。1. 变量捕获的3种场景附示例场景1捕获auto变量局部变量默认autoauto变量是最常见的局部变量如int a 10生命周期随函数栈帧销毁而销毁。Block捕获auto变量时会复制变量的值到Block结构体中Block内部访问的是副本而非原变量因此修改原变量不会影响Block内部的副本反之亦然。#import UIKit/UIKit.h int main(int argc, char * argv[]) { NSString * appDelegateClassName; autoreleasepool { int a 10; // auto变量默认可省略auto关键字 void (^block)(void) ^{ // 访问的是捕获的副本不是原变量a NSLog(Block内部a %d, a); }; a 20; // 修改原变量a的值 block(); // 执行Block打印的是捕获时的副本10 } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }运行结果Block内部a 10原理Block捕获auto变量a时会在其结构体__main_block_impl_0中添加一个int类型的成员变量a将原变量a的值10复制到该成员变量中后续Block内部访问的都是这个副本。场景2捕获static变量静态局部变量static变量存储在全局数据区生命周期与程序一致不会随栈帧销毁。Block捕获static变量时不复制值而是捕获变量的地址因此修改原变量会影响Block内部的访问结果反之亦然。#import UIKit/UIKit.h int main(int argc, char * argv[]) { NSString * appDelegateClassName; autoreleasepool { static int a 10; // static变量 void (^block)(void) ^{ // 访问的是static变量的地址不是副本 NSLog(Block内部a %d, a); a 30; // 通过地址修改原变量的值 }; a 20; // 修改原变量a的值 block(); // 执行Block打印20 NSLog(Block执行后外部a %d, a); // 打印30 } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }运行结果Block内部a 20 Block执行后外部a 30场景3访问全局变量/全局静态变量全局变量、全局静态变量定义在函数外部存储在全局数据区生命周期与程序一致Block不捕获这类变量直接通过地址访问因此修改全局变量会直接影响Block内部的访问结果。#import UIKit/UIKit.h int globalA 10; // 全局变量 static int staticGlobalA 20; // 全局静态变量 int main(int argc, char * argv[]) { NSString * appDelegateClassName; autoreleasepool { void (^block)(void) ^{ // 直接访问全局变量地址不捕获 NSLog(全局变量globalA %d, globalA); NSLog(全局静态变量staticGlobalA %d, staticGlobalA); }; globalA 100; staticGlobalA 200; block(); // 打印修改后的值 } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }运行结果全局变量globalA 100 全局静态变量staticGlobalA 2002. 特殊情况__block修饰符修改auto变量默认情况下Block内部不能修改捕获的auto变量因为访问的是副本修改副本无意义若想在Block内部修改auto变量需给变量添加__block修饰符。原理__block修饰的auto变量会被包装成一个__Block_byref_XXX_0结构体编译器自动生成Block捕获的是该结构体的地址而非变量本身因此可以通过结构体修改原变量的值。#import UIKit/UIKit.h int main(int argc, char * argv[]) { NSString * appDelegateClassName; autoreleasepool { __block int a 10; // __block修饰的auto变量 void (^block)(void) ^{ a 20; // 可以修改原变量的值 NSLog(Block内部修改后a %d, a); }; block(); NSLog(Block执行后外部a %d, a); // 打印20 } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }运行结果Block内部修改后a 20 Block执行后外部a 20三、copy逻辑Block为什么要copy底层流程结合前面的Block类型可知栈Block的生命周期随栈帧销毁而销毁比如函数执行完毕栈帧释放栈Block也会被销毁若在栈Block销毁后再执行它会导致野指针崩溃。因此当我们需要在栈帧销毁后仍能使用Block时必须将其copy到堆内存转为堆Block——这就是Block copy的核心目的。1. Block copy的底层流程核心不同类型的Blockcopy行为不同底层流程可总结为3句话重点全局Block__NSGlobalBlock__copy后还是自身因为它本身存储在全局数据区无需复制栈Block__NSStackBlock__copy后会生成一个新的堆Block__NSMallocBlock__将栈Block的内容函数指针、捕获的变量复制到堆中同时修改isa指针指向堆Block__NSMallocBlock__copy后引用计数1本质是retain操作不会生成新的Block。2. ARC环境下的自动copy关键细节在ARC环境下编译器会自动对Block进行copy操作避免栈Block销毁后崩溃常见的自动copy场景将Block赋值给strong指针如property (strong, nonatomic) void (^block)(void);将Block作为函数返回值返回将Block传入GCD函数如dispatch_async、dispatch_after将Block赋值给NSArray、NSDictionary等容器。3. 实战示例2验证Block的copy逻辑通过代码打印Block的地址和类型验证不同类型Block的copy行为#import UIKit/UIKit.h #import objc/runtime.h int main(int argc, char * argv[]) { NSString * appDelegateClassName; autoreleasepool { // 1. 全局Block copy void (^globalBlock)(void) ^{ NSLog(全局Block); }; void (^globalBlockCopy)(void) [globalBlock copy]; NSLog(全局Block原地址%pcopy后地址%p, globalBlock, globalBlockCopy); NSLog(全局Block原类型%copy后类型%, object_getClass(globalBlock), object_getClass(globalBlockCopy)); NSLog(------------------------); // 2. 栈Block copy int a 10; void (^stackBlock)(void) ^{ NSLog(栈Blocka %d, a); }; void (^stackBlockCopy)(void) [stackBlock copy]; NSLog(栈Block原地址%pcopy后地址%p, stackBlock, stackBlockCopy); NSLog(栈Block原类型%copy后类型%, object_getClass(stackBlock), object_getClass(stackBlockCopy)); NSLog(------------------------); // 3. 堆Block copy栈Block copy后得到 void (^mallocBlockCopy)(void) [stackBlockCopy copy]; NSLog(堆Block原地址%pcopy后地址%p, stackBlockCopy, mallocBlockCopy); NSLog(堆Block原类型%copy后类型%, object_getClass(stackBlockCopy), object_getClass(mallocBlockCopy)); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); }运行结果关键部分全局Block原地址0x1000081c0copy后地址0x1000081c0 全局Block原类型__NSGlobalBlock__copy后类型__NSGlobalBlock__ ------------------------ 栈Block原地址0x7ff7bfeff3a0copy后地址0x6000000100008000 栈Block原类型__NSStackBlock__copy后类型__NSMallocBlock__ ------------------------ 堆Block原地址0x6000000100008000copy后地址0x6000000100008000 堆Block原类型__NSMallocBlock__copy后类型__NSMallocBlock__结论全局Block copy后地址、类型不变栈Block copy后地址变化类型转为堆Block堆Block copy后地址、类型不变仅引用计数1。四、循环引用本质是什么避坑实战循环引用是Block开发中最常见的问题也是面试重点——很多开发者只知道“用weakSelf避免循环引用”却不清楚循环引用的本质导致遇到复杂场景时仍会踩坑。核心结论Block循环引用的本质是“相互强引用”——对象强引用BlockBlock同时强引用该对象形成闭环导致两者都无法被系统释放进而造成内存泄漏。1. 循环引用的典型场景示例最常见的场景ViewController中定义一个strong修饰的Block属性Block内部访问self强引用self形成“self → Block → self”的闭环。#import UIKit/UIKit.h interface ViewController : UIViewController // strong修饰的Block属性self强引用Block property (strong, nonatomic) void (^myBlock)(void); end implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Block内部访问selfBlock强引用self self.myBlock ^{ NSLog(Block内部访问self%, self); }; } // 析构函数验证是否释放 - (void)dealloc { NSLog(ViewController dealloc); } end问题当ViewController被pop或dismiss后dealloc方法不会被调用——因为self强引用myBlockmyBlock强引用self两者形成循环引用无法被释放造成内存泄漏。2. 循环引用的本质拆解结合底层结构结合前面的Block底层结构我们可以拆解循环引用的本质selfViewController对象有一个strong属性myBlock因此self会强引用myBlockBlock的引用计数1Block内部访问self会捕获self因为self是auto变量Block会复制self的强引用到其结构体中因此Block会强引用selfself的引用计数1此时self和Block相互强引用引用计数都无法降为0系统无法释放它们形成内存泄漏。3. 避坑方案3种常用方式附示例方案1使用__weak修饰self最常用在Block外部定义一个__weak修饰的weakSelfBlock内部访问weakSelf弱引用打破“相互强引用”的闭环——weakSelf不会增加self的引用计数当self被释放时weakSelf会自动置为nil。- (void)viewDidLoad { [super viewDidLoad]; // 定义weakSelf弱引用self __weak typeof(self) weakSelf self; self.myBlock ^{ // Block内部访问weakSelf弱引用不增加self的引用计数 NSLog(Block内部访问weakSelf%, weakSelf); }; }补充若Block内部有异步操作如网络请求、延迟执行需在Block内部再用__strong修饰weakSelf避免self在异步操作执行前被释放即“weak-strong dance”- (void)viewDidLoad { [super viewDidLoad]; __weak typeof(self) weakSelf self; self.myBlock ^{ // 异步操作前强引用weakSelf避免self被释放 __strong typeof(weakSelf) strongSelf weakSelf; if (!strongSelf) return; // 防止self已释放 // 执行异步操作 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(异步操作执行%, strongSelf); }); }; }方案2使用__block修饰selfARC环境下需手动置nil用__block修饰self此时Block捕获的是__Block_byref结构体的地址在Block执行完毕后手动将self置为nil打破循环引用——适用于需要在Block内部修改self的场景。- (void)viewDidLoad { [super viewDidLoad]; // __block修饰selfARC环境下__block修饰的对象会被强引用 __block typeof(self) blockSelf self; self.myBlock ^{ NSLog(Block内部访问blockSelf%, blockSelf); // Block执行完毕后手动置nil打破循环引用 blockSelf nil; }; // 必须执行Block否则blockSelf不会置nil仍会内存泄漏 self.myBlock(); }方案3使用第三方参数传递self不推荐仅作了解将self作为Block的参数传递Block内部通过参数访问self不捕获self从而避免循环引用——缺点是破坏Block的简洁性仅适用于简单场景。// 定义带参数的Block属性 property (strong, nonatomic) void (^myBlock)(ViewController *); - (void)viewDidLoad { [super viewDidLoad]; self.myBlock ^(ViewController *vc) { // 通过参数访问self不捕获self NSLog(Block内部访问self%, vc); }; // 调用Block时传递self self.myBlock(self); }4. 实战示例3排查循环引用验证是否释放通过dealloc方法的打印验证循环引用是否被解决#import UIKit/UIKit.h interface ViewController : UIViewController property (strong, nonatomic) void (^myBlock)(void); end implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 方案1weakSelf weak-strong dance __weak typeof(self) weakSelf self; self.myBlock ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (!strongSelf) return; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(异步操作执行%, strongSelf); }); }; // 执行Block self.myBlock(); } - (void)dealloc { NSLog(ViewController dealloc); // 能打印说明无循环引用已释放 } end运行结果当ViewController被pop后会打印“ViewController dealloc”说明循环引用已解决对象正常释放。五、总结Block核心要点面试必记结合前面的底层解析和实战示例Block的核心要点可总结为4句话覆盖所有高频考点本质Block是OC对象底层是包含isa指针、函数指针、捕获变量的结构体继承自NSObject变量捕获auto变量捕获值副本static变量捕获地址全局变量不捕获__block修饰auto变量可实现内部修改copy逻辑栈Block copy到堆全局Block copy不变堆Block copy仅retainARC环境下多种场景自动copy循环引用本质是相互强引用self→Block→self常用__weakweak-strong dance解决需注意异步场景的安全问题。