在扩展定义的递归函数中访问self时触发EXC_BAD_ACCESS错误
解决Swift协议扩展中递归访问self导致的EXC_BAD_ACCESS错误
嘿,这种问题我之前做主题化控制器框架的时候也踩过坑,简直是debug噩梦!先给你拆解下核心原因,再分享几个实用的解决办法。
首先咱们先还原下你提到的简化代码(应该差不多是这个样子):
protocol ThemeableViewController: UIViewController { func initialise(with theme: Theme) } extension ThemeableViewController { func cascadeInitialise(with theme: Theme) { initialise(with: theme) // 递归遍历子控制器应用主题 for child in self.children { if let themeableChild = child as? ThemeableViewController { themeableChild.cascadeInitialise(with: theme) } } } }
为什么会触发EXC_BAD_ACCESS?主要有两个常见诱因:
- 类型擦除导致的self访问异常:协议扩展里的方法,编译器对
self的类型推断有时候会因为协议存在出现偏差,尤其是递归调用时,可能把具体的UIViewController类型擦除成协议类型,导致访问错误的内存地址。 - 生命周期冲突导致的野指针:如果递归调用的时机不对(比如控制器已经被pop出栈、即将被释放的时候),此时访问
self就会指向已经被释放的内存,直接触发崩溃。
接下来给你三个靠谱的解决办法,按优先级排序:
办法一:改用迭代遍历替代递归
递归虽然写起来简洁,但在UIViewController这种有复杂生命周期的对象上很容易出问题。换成迭代的队列方式,能彻底避免递归带来的栈深度和内存管理隐患:
extension ThemeableViewController { func cascadeInitialise(with theme: Theme) { // 用队列管理需要应用主题的控制器 var themeQueue: [ThemeableViewController] = [self] while !themeQueue.isEmpty { let currentVC = themeQueue.removeFirst() // 确保控制器视图已加载,避免访问未初始化的UI元素 guard currentVC.isViewLoaded else { continue } currentVC.initialise(with: theme) // 将符合条件的子控制器加入队列 currentVC.children.compactMap { $0 as? ThemeableViewController }.forEach { themeQueue.append($0) } } } }
这种方式不仅解决了递归的问题,还能更清晰地控制每个控制器的处理顺序,debug起来也更方便。
办法二:添加生命周期检查,避免访问无效self
如果坚持要用递归,一定要在访问self和子控制器前做有效性检查,确保不会访问已经释放的对象:
extension ThemeableViewController { func cascadeInitialise(with theme: Theme) { // 先检查当前控制器是否还处于活跃视图层级中 guard view.window != nil else { return } initialise(with: theme) children.forEach { child in // 同样检查子控制器的有效性 guard let themeableChild = child as? ThemeableViewController, child.view.window != nil else { return } themeableChild.cascadeInitialise(with: theme) } } }
这里用view.window != nil判断控制器是否还处于活跃状态,避免访问已经被销毁的控制器实例。
办法三:明确Self类型约束,避免类型擦除
有时候编译器在协议扩展里对self的类型处理不够精准,咱们可以通过强制类型转换让编译器识别具体类型。比如把协议标记为@objc(UIViewController本来就是NSObject子类,所以没问题),同时用Self做类型转换:
@objc protocol ThemeableViewController: UIViewController { func initialise(with theme: Theme) } extension ThemeableViewController { func cascadeInitialise(with theme: Theme) { initialise(with: theme) children.forEach { child in if let themeableChild = child as? Self { themeableChild.cascadeInitialise(with: theme) } } } }
这里用Self代替协议类型做转换,确保编译器能正确识别具体的控制器类型,避免类型擦除带来的内存访问错误。
总结一下,最稳妥的还是办法一,用迭代替代递归,从根源上避免递归带来的各种隐患。如果一定要用递归,记得加生命周期检查。
内容的提问来源于stack exchange,提问作者Andrew Bennet
相关产品推荐
相关产品推荐

