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

相关推荐
冻感糕人~1 小时前
大模型面试干货:小白程序员如何准备,轻松拿下高薪Offer?收藏这份独家秘籍!
java·人工智能·学习·ai·面试·职场和发展·大模型学习
文件夹__iOS1 小时前
Swift 5.9 被严重低估的特性:参数包,一次性干掉重复泛型重载
ios·swiftui·swift
薛定猫AI2 小时前
【技术干货】用 AI + Expo 打通 iOS / Android / Web 跨端应用开发:从架构到代码生成实战
android·人工智能·ios
EXnf1SbYK2 小时前
Redis分布式锁进阶第十四篇:分布式锁常见高频面试压轴题 + 线上踩坑标准答案 + 架构师高分收口
面试·职场和发展
Filwaod2 小时前
Java面试现场:从Redis缓存到分布式事务,水货程序员李四的‘表演‘
java·jvm·spring boot·redis·mysql·面试·多线程
张元清2 小时前
React 表单处理:防抖校验、自动保存草稿与受控输入
前端·javascript·面试
MonkeyKing3 小时前
iOS关联对象底层实现与内存管理细节
ios
用户3210442819453 小时前
STL详解
面试
用户3210442819453 小时前
并发编程核心原理
面试