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"); };

    1. 栈Block(_NSConcreteStackBlock)
    • 存储位置:栈区,生命周期随作用域结束而销毁(栈空间自动回收);

    • 触发条件:捕获了局部变量(未被copy);

    • 注意:ARC下栈Block会被编译器自动优化为堆Block(如赋值给strong变量),MRC下需手动copy,否则作用域结束后Block失效,访问会崩溃;

    • 示例:int a = 10; void (^stackBlock)(void) = ^{ NSLog(@"%d", a); };

    1. 堆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为堆Block;MRC下需手动copy,否则为栈Block,作用域结束后失效。

三、Block 循环引用(面试重中之重,必考难点)

3.1 循环引用的核心原因(必背,面试直接答)

核心逻辑:两个或多个对象互相持有强引用,导致引用计数无法降至0,对象无法被系统销毁,造成内存泄漏;Block的循环引用,本质是「Block强引用捕获的对象,同时该对象又强引用Block」,形成闭环。

关键前提:ARC下,Block捕获OC对象(如self)时,默认是强引用捕获;若该对象(如控制器)又用strong指针持有Block,就会形成循环引用(MRC下需手动retain才会形成,面试重点考察ARC场景)。

3.2 循环引用的常见场景(必背,结合代码示例)

面试需能说出场景+代码示例+核心闭环,以下3个场景为高频考点,务必牢记:

场景1:Block作为控制器的属性(最常见)

复制代码
// 控制器内部声明Block属性(strong修饰,默认)
@property (nonatomic, strong) void (^myBlock)(void);

// 赋值时,Block内部访问self(隐式捕获self,强引用)
self.myBlock = ^{
    // 捕获self,强引用
    NSLog(@"访问控制器属性:%@", self.name);
};
// 闭环:self(强引用)→ myBlock(Block),myBlock(强引用)→ self,形成循环引用

场景2:Block与定时器(易忽略)

复制代码
// 控制器内部创建定时器,Block内部访问self
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    // Block强引用self,定时器强引用Block,self强引用定时器
    [self doSomething];
}];
// 闭环:self → timer → Block → self,循环引用(定时器会被RunLoop持有,加剧内存泄漏)

场景3:Block嵌套(多层闭环)

复制代码
self.myBlock = ^{
    // 内层Block捕获self,强引用
    void (^innerBlock)(void) = ^{
        NSLog(@"%@", self);
    };
    // 外层Block强引用内层Block,self强引用外层Block,形成多层闭环
    innerBlock();
};

3.3 循环引用的判断方法(面试延伸,实操考点)

  • 代码层面:判断是否存在「对象强引用Block,Block强引用该对象」的闭环,重点关注Block内部是否访问self、实例变量,以及Block的持有方式(strong/copy);

  • 工具层面:使用Xcode的Instruments工具(Leaks模板)检测,运行程序后,操作触发Block,若控制器销毁后(pop/dismiss),内存中仍存在该控制器实例,即为循环引用导致的内存泄漏。

四、Block 循环引用的解决方案(面试必背,结合实操)

核心思路:打破强引用闭环,即让Block对捕获的对象(如self)变为弱引用,或让对象对Block变为弱引用,常用4种方案,按面试高频度排序,需掌握每种方案的适用场景、代码示例及注意事项:

方案1:__weak 弱引用(最常用,ARC专属)

  • 核心原理:在Block外部,用__weak修饰要捕获的对象(如self),Block内部捕获该弱引用指针,弱引用不增加引用计数,对象销毁时,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逻辑"即可。

五、面试高频问答(直接应答,无需修改)

  • 问题1:Block的底层结构是什么?本质是什么?

    • 应答:Block底层是C语言结构体,核心包含isa指针、Flags标志位、FuncPtr函数指针、捕获的外部变量;本质是OC对象(继承自NSObject),核心作用是封装代码块,捕获外部变量实现延迟执行。
  • 问题2:Block的变量捕获规则是什么?(重点答对象和self)

    • 应答:局部基本类型值捕获、局部OC对象指针捕获(ARC下强引用)、__block变量指针捕获(可修改)、全局变量不捕获;实例变量会隐式捕获self,Block内部访问实例变量本质是访问self。
  • 问题3:Block有哪几种类型?区别是什么?

    • 应答:3种类型------全局Block(全局区,不捕获局部变量)、栈Block(栈区,捕获局部变量未copy)、堆Block(堆区,栈Block被copy);区别在于存储位置、生命周期,以及是否需要手动管理内存。
  • 问题4:Block为什么会产生循环引用?常见场景有哪些?

    • 应答:原因是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自动优化为堆Block;

  3. 循环引用:核心是"Block强引用对象,对象强引用Block",常见于控制器属性、定时器场景;

  4. 解决方案:__weak是首选,配合__strong处理耗时操作,其他方案按需使用,重点区分__weak与__unsafe_unretained;

  5. 面试关键:能说出底层结构、捕获规则、循环引用场景+原因+解决方案,结合代码示例应答更加分。

相关推荐
考研保研资料分享6 小时前
金融专硕保研经验:清华五道口预推免、北大汇丰夏令营与面试复盘
面试·金融
事圆则缓6 小时前
Kotlin 入门与面试:从空安全、扩展函数到协程
安全·面试·kotlin
tink7 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架9 小时前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程10 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程10 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
可乐鸡翅yeah_10 小时前
hls.js 缓冲区参数 maxBufferLength、maxBufferSize 通俗讲解与业务调参
开发语言·javascript·ios·音视频·safari·m3u8