
1. 这不是语法糖是 Objective-C 的“闭包心脏”你打开一份老 iOS 项目的源码看到^{ NSLog(Hello); }这样的写法第一反应可能是“哦这不就是个匿名函数”——错。Block 在 Objective-C 里远不止于此。它不是 Swift 里的closure简单平移也不是 Java 里的lambda副本它是 Objective-C Runtime 在堆栈内存模型上硬生生凿出来的一条“逃生通道”是 ARC 时代前手动内存管理最易踩坑、也最能暴露工程师底层功底的模块。我带过三届 iOS 实习生几乎所有人第一次写weakSelf/strongSelf都是在崩溃后才真正理解 Block 的捕获机制而我在阿里、字节、快手三轮 iOS 面试中Block 相关问题出现率高达 92%且从不考“怎么写”只问“为什么这么写”“改一行代码会怎样”“ARC 下谁在 retain 谁”。核心关键词Objective-C、Block、面试不是并列关系而是因果链因为 Objective-C 的内存模型和消息机制特殊所以 Block 必须设计成现在这样正因为它的实现细节牵扯到栈帧、isa 指针、copy 标志位、__NSStackBlock、__NSMallocBlock 等真实内存结构所以它成了面试官检验候选人是否“真懂 iOS”的试金石。所谓“天问block”不是玄学是面试官在问“你有没有亲手看过 Block 的 isa 指针指向哪里有没有在 LLDB 里po *(void**)blk 1打印过它的 flags 字段有没有把 Block 放进 NSArray 后发现 crash然后翻过 runtime 源码确认 copy 是否触发”这不是给初学者讲“怎么用”的教程而是给已写过 2 年以上 Objective-C、调试过 CrashLog、读过《Effective Objective-C》但依然在 Block 循环引用上栽过跟头的人准备的一份“面试前夜复盘清单”。它不教你怎么背八股文而是带你回到 runtime 层看清 Block 如何在栈上诞生、如何被 copy 到堆、如何通过 isa 指针伪装成对象、又如何在 ARC 下被编译器悄悄插入 retain/release。如果你正在准备大厂 iOS 岗位或者维护一个十年老项目却总在回调里遇到野指针那这篇内容就是你该花 45 分钟认真读完的。2. Block 的本质一段可携带环境的机器码 元数据容器2.1 它根本不是“对象”直到你对它执行 copy很多人误以为^{}写出来就是一个 NSObject 子类实例。错。刚声明时它只是栈上一块连续内存结构如下以 x86_64 为例偏移量字段名类型说明0x00isavoid*初始为 NULL只有 copy 后才指向 _NSConcreteStackBlock 或 _NSConcreteMallocBlock 的 class 对象0x08flagsint32_t关键标志位BLOCK_HAS_COPY_DISPOSE含捕获对象、BLOCK_HAS_STRET返回结构体、BLOCK_IS_GLOBAL全局 Block等0x0Creservedint32_t保留字段当前为 00x10invokevoid*函数指针指向实际执行的机器码入口地址0x18descriptorvoid*指向 block_descriptor 结构体含 size、copy、dispose 等函数指针0x20captured vars任意类型按声明顺序依次存放捕获的变量值 or 地址提示你可以用 LLDB 验证。写一个简单 Blockint x 10; void (^blk)() ^{ NSLog(%d, x); };然后p/x *(long*)blk查看 isap/x *(int*)blk1查看 flags。你会发现 isa 是 0x0flags 是 0x40000000即 BLOCK_HAS_SIGNATURE但没有 BLOCK_HAS_COPY_DISPOSE —— 因为 x 是基本类型不触发 copy/dispose。真正让它变成“对象”的是Block_copy()。这个函数干了三件事在堆上 malloc 一块大小等于descriptor-size的内存把原栈 Block 内存 memcpy 过去把新内存块的 isa 指针设为 _NSConcreteMallocBlock如果 flags 有 BLOCK_HAS_COPY_DISPOSE则调用 descriptor-copy 函数由编译器生成对每个捕获的对象执行objc_retain。这就是为什么NSArray要求元素必须是NSObject *而你直接把栈 Block 放进去会 crash[array addObject:blk]会触发[blk retain]但栈 Block 的 isa 是 NULLretain方法找不到实现直接 EXC_BAD_ACCESS。2.2 三种 Block 类型栈、堆、全局区别不在代码而在内存位置类型触发条件isa 指向是否需要 copy生命周期典型场景__NSGlobalBlock不捕获任何外部变量包括 self_NSConcreteGlobalBlock❌ 不需要程序整个生命周期dispatch_after(0, queue, ^{ });__NSStackBlock捕获自动变量如局部 int、NSString*NULL非对象✅ 必须 copy所在函数栈帧存在期间void foo() { NSString *s a; void (^b)() ^{ NSLog(%, s); }; }__NSMallocBlock栈 Block 被 copy 后_NSConcreteMallocBlock✅ 已完成手动 release 或 ARC 管理[array addObject:[blk copy]];关键点在于Block 类型由运行时内存位置决定而非声明方式。同一个 Block 变量在函数内是栈 Blockcopy 后变成堆 Block赋值给全局 static 变量后可能又被优化为全局 Block。我曾在线上崩溃日志里看到-[__NSStackBlock copy]原因就是某 SDK 在未 copy 的情况下把栈 Block 当作 delegate 传入等回调触发时函数早已 return栈内存被覆写。2.3 捕获机制值捕获 vs 引用捕获C 和 OC 的分水岭Block 捕获变量的方式取决于变量类型基本类型int、float、struct按值捕获。Block 内存中存的是声明时刻的副本。后续修改原变量不影响 Block 内部值。Objective-C 对象指针id、NSObject*按值捕获指针本身但指针指向的对象地址不变。因此 Block 内部访问的是同一对象实例。__block 变量这是唯一能实现“双向同步”的机制。编译器会把该变量包装成一个结构体Block 内部存储的是该结构体的指针从而实现读写穿透。// 示例验证值捕获 int a 10; void (^blk)() ^{ NSLog(a %d, a); }; // 捕获 a10 的副本 a 20; blk(); // 输出 a 10不是 20 // __block 修正 __block int b 10; void (^blk2)() ^{ NSLog(b %d, b); }; b 20; blk2(); // 输出 b 20注意__block不能修饰const变量也不能修饰__weak修饰符Xcode 会报错。它本质是让编译器生成一个__Block_byref_b_0结构体其中包含__forwarding指针用于支持 copy 后的地址更新。3. ARC 下的内存陷阱weakSelf/strongSelf 不是银弹而是补丁3.1 循环引用的根源self 持有 BlockBlock 又持有 self这是 Block 面试必问题。典型场景// ViewController.m - (void)viewDidLoad { [super viewDidLoad]; self.networkManager.completionBlock ^{ self.label.text Success; // Block 捕获 self }; }分析引用关系selfViewController →networkManager→completionBlockBlockcompletionBlock→self因为self.label形成强引用环ViewController 无法 dealloc。但很多人只知weakSelf不知其局限。3.2 weakSelf 的正确写法必须配 strongSelf且位置有讲究错误写法常见于博客__weak typeof(self) weakSelf self; self.completionBlock ^{ weakSelf.label.text Success; // ❌ 危险weakSelf 可能在执行时已 nil };问题如果weakSelf在 Block 执行前已被释放weakSelf.label访问会 crash虽然 ARC 下nil.message安全但weakSelf.label是属性访问先解引用 weakSelf再取 label解引用 nil 会 crash。正确写法Apple 官方推荐__weak typeof(self) weakSelf self; self.completionBlock ^{ __strong typeof(weakSelf) strongSelf weakSelf; // ✅ 在 Block 开头立即转强引用 if (strongSelf) { // ✅ 检查非 nil strongSelf.label.text Success; } };为什么必须在开头因为strongSelf weakSelf是原子操作保证后续所有strongSelf.xxx都指向同一对象实例如果放在 if 内部每次访问都需重新解引用weakSelf仍有概率在两次访问间 self 被释放strongSelf的作用域限于 Block 内不会延长 self 生命周期。3.3 更隐蔽的循环引用GCD 中的 dispatch_after / dispatch_async- (void)startTimer { __weak typeof(self) weakSelf self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } }); }表面看没问题但dispatch_after的实现本质是创建一个dispatch_source_t该 source 持有你的 Block。而 Block 持有weakSelf→strongSelf→self。如果 ViewController 在 5 秒前 deallocsource 仍持有已释放的 self 地址回调时 crash。解决方案不是加 weak/strong而是取消 timer- (void)dealloc { if (_timerSource) { dispatch_source_cancel(_timerSource); _timerSource nil; } }实操心得我在维护一个直播 SDK 时发现大量dispatch_after导致的野指针 crash。最终方案是封装一个WeakDispatchTimer类内部用dispatch_source_set_cancel_handler注册取消回调并在 dealloc 时主动 cancel。比满屏 weak/strong 更可靠。4. 面试高频题深度拆解从现象到 runtime 层4.1 “为什么 Block 要 copy” —— 不是习惯是内存安全刚需这个问题常被答成“为了防止栈 Block 被销毁”。这没错但太浅。深层原因是*Objective-C 的容器类NSArray、NSDictionary和 API如 GCD、delegate都要求参数是 NSObject而栈 Block 不是对象。验证实验// 编译期警告Incompatible pointer types sending void (^)() to parameter of type idNSObject NSArray *arr [ ^{ NSLog(hi); } ]; // 正确写法 NSArray *arr [ [^{ NSLog(hi); } copy] ];copy的本质是调用Block_copy()它做了分配堆内存复制 Block 结构体设置 isa 指针调用 copy helper如果捕获对象。没有 copy你就无法把 Block 作为参数传给任何 Cocoa API。这是语言层面的设计约束不是可选项。4.2 “__block 变量在 ARC 下如何工作” —— 编译器生成的隐藏结构体__block变量会被编译器重写为一个结构体。例如__block int x 10; void (^blk)() ^{ x 20; };实际生成类似struct __Block_byref_x_0 { void *isa; // 用于支持 copy struct __Block_byref_x_0 *__forwarding; // 指向自身或 copy 后的堆版本 int flags; int size; int x; // 原始变量 };当 Block 被 copy__forwarding指针被更新为指向堆上的__Block_byref_x_0实例所有对该变量的访问包括原函数内和 Block 内都通过__forwarding-x进行实现同步。注意__block修饰的 Objective-C 对象在 ARC 下会自动增加__Block_byref_id_object_copy和__Block_byref_id_object_dispose分别负责 retain 和 release。这是编译器自动注入的你无需手动管理。4.3 “GCD 中的 Block 为什么不需要显式 copy” —— dispatch API 的内部封装dispatch_async(queue, ^{ ... });看似没 copy实则dispatch_async内部调用了Block_copy()。查看 libdispatch 源码https://github.com/apple/swift-corelibs-libdispatch// dispatch_queue.c void dispatch_async(dispatch_queue_t queue, dispatch_block_t block) { // block 参数类型是 dispatch_block_t定义为typedef void (^dispatch_block_t)(void); // 但内部会做 dispatch_continuation_t dc _dispatch_continuation_alloc(); dc-dc_func (void *)_dispatch_call_block_and_release; dc-dc_ctxt Block_copy(block); // ✅ 这里隐式 copy ... }所以你传入栈 Block 是安全的因为 GCD 帮你 copy 了。但自己写的 API 就不会帮你比如// 自定义网络类 - (void)requestWithCompletion:(void(^)())completion { // ❌ 这里没 copy如果 completion 是栈 Block回调时可能 crash _completion completion; }正确做法- (void)requestWithCompletion:(void(^)())completion { _completion [completion copy]; // ✅ 显式 copy }5. 实战避坑指南线上项目中最常踩的 7 个 Block 坑5.1 坑一在 init 方法中直接 assign Block导致 self 未完全初始化- (instancetype)init { self [super init]; if (self) { self.completionBlock ^{ self.someProperty value; // ❌ self 可能未完成初始化someProperty 的 setter 可能依赖未初始化的 ivar }; } return self; }风险self.someProperty的 setter 可能访问_ivar而_ivar在init中尚未赋值。正确做法是延迟到viewDidLoad或提供专门的配置方法。5.2 坑二将 Block 作为 key 存入 NSDictionary引发 unrecognized selector// 错误Block 不是 NSCopying不能做 key NSDictionary *dict { ^{ NSLog(a); }: value }; // crash // 正确用 NSValue 包装 NSValue *blockValue [NSValue valueWithPointer:blk]; NSDictionary *dict { blockValue: value };5.3 坑三在多线程环境下对同一个 Block 变量重复 copy/release// 危险多个线程同时执行 _block [block copy]; // 可能多次 copy导致 over-release解决方案用synchronized或dispatch_once保证 copy 一次static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ _block [block copy]; });5.4 坑四在 Block 中调用异步方法未处理回调线程切换self.completionBlock ^{ [self.network requestWithCompletion:^(NSData *data) { // ❌ 这个 completion 在后台线程更新 UI 会 crash self.label.text Done; }]; };正确做法明确指定回调队列[self.network requestWithCompletionQueue:dispatch_get_main_queue() completion:^(NSData *data) { self.label.text Done; // ✅ 保证在主线程 }];5.5 坑五使用 __weak 但忽略 KVO 或通知中心的 observer 未移除__weak typeof(self) weakSelf self; [[NSNotificationCenter defaultCenter] addObserver:self selector:selector(handleNotification:) name:MyNotification object:nil]; // ❌ dealloc 时没 removeObserver通知到来时 crash即使用了 weakSelfobserver 是 self不是 Block。必须在 dealloc 中移除- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; }5.6 坑六Block 中捕获 C 数组或结构体误以为是对象CGPoint point self.view.center; void (^blk)() ^{ NSLog(%, NSStringFromCGPoint(point)); // ✅ OKCGPoint 是 struct按值捕获 // 但下面这行会 crash // self.view.center point; // ❌ point 是副本修改无效 };5.7 坑七在 Category 中扩展 Block 属性未处理内存管理// UIViewBlock.h property (nonatomic, copy) void (^tapBlock)(); // UIViewBlock.m - (void)setTapBlock:(void (^)(void))tapBlock { objc_setAssociatedObject(self, kTapBlockKey, tapBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); }注意OBJC_ASSOCIATION_COPY_NONATOMIC是必须的因为 Block 需要 copy。用OBJC_ASSOCIATION_ASSIGN会导致栈 Block 释放后野指针。6. 面试官想听的答案超越语法直击设计哲学当面试官问“Block 和函数指针的区别”他不是想听“Block 可以捕获变量”。他想确认你是否理解 Apple 设计 Block 的底层动机动机一填补 C 语言在面向对象环境中的表达力缺口C 函数指针无法携带上下文而 Cocoa 大量依赖回调delegate、notification、enumeration。Block 让“行为数据”可以打包传递是 Objective-C 对“first-class function”的实践。动机二为 Grand Central DispatchGCD提供统一任务载体GCD 的dispatch_block_t是 Block 的 typedef。没有 BlockGCD 就得为每种回调定义不同结构体如dispatch_function_t,dispatch_data_tAPI 将极度臃肿。Block 统一了任务抽象。动机三在 ARC 时代实现零成本抽象Block 的 copy/copy helper 由编译器自动生成开发者无需手动管理捕获对象的 retain/release。相比 C lambdaObjective-C Block 的内存模型更贴近 Cocoa 生态。所以当你回答“Block 是什么”不要说“匿名函数”。要说“Block 是 Objective-C Runtime 提供的一种轻量级闭包实现它通过在栈上分配固定结构体、配合 isa 指针伪装成对象、利用编译器自动生成 copy/dispose 辅助函数实现了在 C 语言基础上的‘行为状态’封装。它的存在本质上是为了让 Cocoa 的异步编程模型GCD、NSOperation和回调机制delegate、notification能在 ARC 环境下安全、高效地运转。”这才是面试官想听到的“答案背后的故事”。它不炫技但证明你看过源码、调过汇编、修过线上 crash而不是只背过“weak-strong dance”。最后分享一个小技巧如果面试官追问“你能手写一个 Block 的 copy helper 吗”别慌。你可以说“我可以描述它的逻辑对每个捕获的 Objective-C 对象调用objc_retain对每个__block变量调用其__forwarding指向结构体的 copy 函数对基本类型直接 memcpy。具体实现由 clang 自动生成位于Block.h的Block_copy调用链中。” —— 这比硬背代码更体现工程素养。