iOS 开发 Block 底层结构、循环引用及解决方案
核心说明聚焦面试高频提问全程直击考点无冗余表述覆盖Block底层结构、类型分类、变量捕获、循环引用核心难点、解决方案及面试延伸点兼顾理论深度与实操应答性可直接用于面试背诵。一、Block 核心定义面试开篇必答面试必记Block 是携带上下文的匿名函数本质是 OC 中的一个对象继承自NSObject底层基于C语言结构体实现核心作用是“封装一段代码块”可在合适的时机调用同时能捕获外部变量实现代码的灵活复用与延迟执行是iOS开发中回调、异步操作如GCD的核心载体。二、Block 底层结构核心必记面试高频Block 底层本质是一个C语言结构体struct编译器会自动将Block代码转换为该结构体的实例核心包含4个关键部分结合底层源码简化记忆面试无需写完整源码掌握核心结构即可2.1 底层结构体核心结构必背isa指针Block的本质是对象因此结构体第一个成员是isa指针用于指向Block的类型全局/栈/堆Block与OC对象的isa指针作用一致Flags标志位4字节用于存储Block的状态信息如是否被copy、是否有销毁辅助函数、是否捕获了弱引用等面试常考“是否被copy”标识FuncPtr函数指针指向Block封装的代码块即Block内部的执行逻辑是Block能执行代码的核心调用Block本质就是调用该函数指针捕获的外部变量结构体后续成员为Block捕获的外部变量值或指针捕获规则由变量类型决定是Block循环引用的核心诱因。面试延伸Block结构体底层源码简化无需背诵理解即可struct __Block_impl { void *isa; // 指向Block类型 int Flags; // 标志位 int Reserved; // 预留字段用于扩展 void (*FuncPtr)(void *); // 指向Block执行代码的函数指针 }; // 包含捕获变量的Block结构体示例 struct __XXXBlock_impl_0 { struct __Block_impl impl; // 捕获的外部变量如self、局部变量等 id weakSelf; // 示例捕获的weakSelf指针 };2.2 Block 变量捕获规则面试必问核心重点Block会自动捕获其作用域内的外部变量捕获方式由变量类型决定直接影响Block的内存管理和循环引用务必牢记以下规则结合表格记忆面试直接应答变量类型捕获方式是否可在Block内修改核心说明局部基本类型int、float等值捕获编译时拷贝值到Block结构体不可修改只读捕获的是变量的副本Block内修改不影响外部变量局部OC对象指针捕获捕获对象地址带所有权语义不可修改指针指向可修改对象内部属性ARC下默认强引用捕获MRC下默认弱引用捕获是循环引用的主要场景__block修饰的局部变量指针捕获包装为__Block_byref结构体可修改底层将变量包装成结构体捕获结构体指针实现Block内外变量同步修改__block不改变引用计数ARC下静态局部变量指针捕获捕获变量地址可修改变量存储在全局区生命周期长Block捕获其地址修改会影响外部变量全局变量/静态全局变量不捕获直接访问可修改变量存储在全局区作用域全局可见Block无需捕获直接访问实例变量self-xxx捕获self隐式捕获self是当前对象的指针通过self访问可修改Block不会直接捕获实例变量而是捕获self再通过self访问实例变量是循环引用的高频场景2.3 Block 三种类型面试高频结合内存分区Block的类型由isa指针指向决定不同类型的Block存储位置、生命周期不同直接影响内存管理面试常考类型区别及转换场景1. 全局Block_NSConcreteGlobalBlock存储位置程序全局区__TEXT,__const段生命周期与程序一致触发条件不捕获任何外部变量或仅捕获全局/静态变量示例void (^globalBlock)(void) ^{ NSLog(全局Block); };2. 栈Block_NSConcreteStackBlock存储位置栈区生命周期随作用域结束而销毁栈空间自动回收触发条件捕获了局部变量未被copy注意ARC下栈Block会被编译器自动优化为堆Block如赋值给strong变量MRC下需手动copy否则作用域结束后Block失效访问会崩溃示例int a 10; void (^stackBlock)(void) ^{ NSLog(%d, a); };3. 堆Block_NSConcreteMallocBlock存储位置堆区生命周期由引用计数管理ARC自动管理MRC需手动release触发条件栈Block被copy手动copy、赋值给copy修饰的属性、作为返回值等面试延伸栈Block copy到堆的核心操作——拷贝Block结构体、拷贝捕获的变量__block变量会拷贝到堆并调整forwarding指针、更新isa指针为堆Block类型。补充考点ARC与MRC下Block的copy差异——ARC下Block赋值给strong变量、作为返回值、作为GCD参数时编译器自动copy为堆BlockMRC下需手动copy否则为栈Block作用域结束后失效。三、Block 循环引用面试重中之重必考难点3.1 循环引用的核心原因必背面试直接答核心逻辑两个或多个对象互相持有强引用导致引用计数无法降至0对象无法被系统销毁造成内存泄漏Block的循环引用本质是「Block强引用捕获的对象同时该对象又强引用Block」形成闭环。关键前提ARC下Block捕获OC对象如self时默认是强引用捕获若该对象如控制器又用strong指针持有Block就会形成循环引用MRC下需手动retain才会形成面试重点考察ARC场景。3.2 循环引用的常见场景必背结合代码示例面试需能说出场景代码示例核心闭环以下3个场景为高频考点务必牢记场景1Block作为控制器的属性最常见// 控制器内部声明Block属性strong修饰默认 property (nonatomic, strong) void (^myBlock)(void); // 赋值时Block内部访问self隐式捕获self强引用 self.myBlock ^{ // 捕获self强引用 NSLog(访问控制器属性%, self.name); }; // 闭环self强引用→ myBlockBlockmyBlock强引用→ self形成循环引用场景2Block与定时器易忽略// 控制器内部创建定时器Block内部访问self self.timer [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { // Block强引用self定时器强引用Blockself强引用定时器 [self doSomething]; }]; // 闭环self → timer → Block → self循环引用定时器会被RunLoop持有加剧内存泄漏场景3Block嵌套多层闭环self.myBlock ^{ // 内层Block捕获self强引用 void (^innerBlock)(void) ^{ NSLog(%, self); }; // 外层Block强引用内层Blockself强引用外层Block形成多层闭环 innerBlock(); };3.3 循环引用的判断方法面试延伸实操考点代码层面判断是否存在「对象强引用BlockBlock强引用该对象」的闭环重点关注Block内部是否访问self、实例变量以及Block的持有方式strong/copy工具层面使用Xcode的Instruments工具Leaks模板检测运行程序后操作触发Block若控制器销毁后pop/dismiss内存中仍存在该控制器实例即为循环引用导致的内存泄漏。四、Block 循环引用的解决方案面试必背结合实操核心思路打破强引用闭环即让Block对捕获的对象如self变为弱引用或让对象对Block变为弱引用常用4种方案按面试高频度排序需掌握每种方案的适用场景、代码示例及注意事项方案1__weak 弱引用最常用ARC专属核心原理在Block外部用__weak修饰要捕获的对象如selfBlock内部捕获该弱引用指针弱引用不增加引用计数对象销毁时weak指针自动置为nil打破闭环代码示例对应场景1// 外部用__weak修饰self __weak typeof(self) weakSelf self; self.myBlock ^{ // 内部用weakSelf访问弱引用不形成闭环 NSLog(访问控制器属性%, weakSelf.name); };注意事项面试高频延伸weakSelf在Block内部可能被销毁如控制器提前pop若需在Block内部执行耗时操作需搭配__strong使用避免weakSelf被提前释放搭配__strong示例__weak typeof(self) weakSelf self; self.myBlock ^{ // 强引用weakSelf确保Block执行期间self不被销毁 __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; // 耗时操作 } };方案2__unsafe_unretained 弱引用兼容低版本慎用核心原理与__weak类似修饰对象后Block捕获弱引用指针不增加引用计数区别对象销毁后__unsafe_unretained修饰的指针不会自动置为nil会变成野指针访问时会崩溃__weak会自动置为nil更安全适用场景兼容iOS 5及以下版本目前几乎不用面试仅需了解重点说清与__weak的区别代码示例__unsafe_unretained typeof(self) weakSelf self; self.myBlock ^{ NSLog(%, weakSelf.name); // 风险weakSelf可能为野指针 };方案3__block 修饰MRC常用ARC慎用核心原理MRC__block修饰对象指针Block捕获该指针时默认是弱引用不会增加引用计数打破闭环核心原理ARC__block修饰对象指针Block捕获时会强引用该对象需在Block内部手动将指针置为nil打破闭环实操复杂不如__weak常用代码示例ARC场景__block typeof(self) blockSelf self; self.myBlock ^{ NSLog(%, blockSelf.name); // 手动置nil打破闭环必须在Block执行后置nil否则无效 blockSelf nil; };面试延伸ARC下不推荐用__block解决循环引用原因是需手动置nil易遗漏且不如__weak安全MRC下是常用方案之一。方案4打破对象对Block的强引用主动释放核心原理让持有Block的对象如self在合适的时机如控制器销毁时将Block置为nil打破“对象强引用Block”的环节适用场景Block仅在特定场景使用如点击事件使用完毕后主动释放代码示例控制器场景// 控制器销毁时将Block置为nil- (void)dealloc {self.myBlock nil; // 打破self对Block的强引用闭环解除}注意事项若Block内部仍强引用self仅置nil Block无法完全打破闭环需配合__weak使用双重保障。补充方案使用第三方框架面试延伸如RAC框架的weakify和strongify宏本质是对__weak和__strong的封装简化代码面试无需写源码只需说明“通过框架宏简化弱引用操作底层仍是__weak__strong逻辑”即可。五、面试高频问答直接应答无需修改问题1Block的底层结构是什么本质是什么应答Block底层是C语言结构体核心包含isa指针、Flags标志位、FuncPtr函数指针、捕获的外部变量本质是OC对象继承自NSObject核心作用是封装代码块捕获外部变量实现延迟执行。问题2Block的变量捕获规则是什么重点答对象和self应答局部基本类型值捕获、局部OC对象指针捕获ARC下强引用、__block变量指针捕获可修改、全局变量不捕获实例变量会隐式捕获selfBlock内部访问实例变量本质是访问self。问题3Block有哪几种类型区别是什么应答3种类型——全局Block全局区不捕获局部变量、栈Block栈区捕获局部变量未copy、堆Block堆区栈Block被copy区别在于存储位置、生命周期以及是否需要手动管理内存。问题4Block为什么会产生循环引用常见场景有哪些应答原因是ARC下Block默认强引用捕获的对象如self若该对象又强引用Block形成强引用闭环引用计数无法为0对象无法销毁常见场景Block作为控制器属性、Block与定时器、Block嵌套。问题5如何解决Block的循环引用每种方案的适用场景是什么应答核心是打破强引用闭环4种方案1. __weak修饰最常用ARC专属安全需配合__strong处理耗时操作2. __unsafe_unretained兼容低版本有野指针风险慎用3. __block修饰MRC常用ARC需手动置nil复杂4. 主动置nil Block配合其他方案双重保障。问题6__weak和__unsafe_unretained的区别是什么应答二者均为弱引用不增加引用计数区别是对象销毁后__weak指针自动置为nil不会崩溃__unsafe_unretained指针不置为nil会变成野指针访问时崩溃__weak更安全。六、面试总结核心提炼快速背诵1. 底层核心Block是OC对象底层为C语言结构体核心含isa、Flags、FuncPtr、捕获的变量变量捕获规则决定内存管理2. 类型重点3种Block全局/栈/堆区别在存储位置和生命周期ARC下栈Block自动优化为堆Block3. 循环引用核心是“Block强引用对象对象强引用Block”常见于控制器属性、定时器场景4. 解决方案__weak是首选配合__strong处理耗时操作其他方案按需使用重点区分__weak与__unsafe_unretained5. 面试关键能说出底层结构、捕获规则、循环引用场景原因解决方案结合代码示例应答更加分。