You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS Swift:如何确保子ViewController触发deinit释放内存

如何确保切换UITabBar时子ViewController被释放(解决deinit不调用的问题)

我看了你遇到的问题——切换TabBar项时移除子VC但始终看不到deinit日志,这说明子VC还被某个强引用攥着,没被真正回收。结合你给出的代码,我来拆解问题根源并给出可行的解决方案:

问题核心:不必要的强引用未被解除

你用private lazy var lotteryViewController: LotteryViewController来持有子VC实例,这个懒加载属性是强引用类型。只要父ViewController存在,这个属性就会一直牢牢持有子VC的引用——哪怕你调用了remove(asChildViewController:)方法把它从父VC的子控制器列表和视图层级中移除,父VC的这个lazy var还是会攥着子VC不放,导致ARC无法回收它,自然不会触发deinit。

解决方案:动态管理子VC的引用关系

我们需要调整子VC的持有逻辑,让只有当前显示的子VC被强引用,切换时彻底解除对旧子VC的所有强引用,具体步骤如下:

1. 替换持有方式:用可选属性跟踪当前子VC

在父ViewController中,删掉原来各个子VC的lazy var属性,换成一个可选类型的属性来记录当前显示的子VC:

private var currentChildViewController: UIViewController?

2. 动态创建&切换子VC

当切换TabBar项时,先移除当前显示的子VC,再动态创建新的子VC并添加,同时把新子VC赋值给currentChildViewController。比如切换到LotteryViewController的逻辑可以改成这样:

func switchToLotteryTab() {
    // 先移除当前显示的子VC
    if let currentVC = currentChildViewController {
        remove(asChildViewController: currentVC)
    }
    
    // 动态创建新的子VC(不要用lazy var,避免永久持有)
    let storyboard = UIStoryboard(name: "TrophyRoom", bundle: Bundle.main)
    let lotteryVC = storyboard.instantiateViewController(withIdentifier: "LotteryViewController") as! LotteryViewController
    
    // 添加为子VC
    add(asChildViewController: lotteryVC)
    // 只持有当前显示的子VC
    currentChildViewController = lotteryVC
}

3. 排查子VC内部的强引用循环

除了父VC的持有问题,还要确保子VC内部没有触发循环引用:

  • 如果子VC里用到了闭包(比如网络请求、定时器回调),一定要用[weak self]或[unowned self]打破循环:
    // 错误示例(会导致循环引用)
    someButton.addAction {
        self.doSomething()
    }
    
    // 正确示例
    someButton.addAction { [weak self] in
        self?.doSomething()
    }
    
  • 关于NotificationCenter:你在deinit里调用NotificationCenter.default.removeObserver(self)的方式,只适用于用addObserver(_:selector:name:object:)注册的观察者;如果是用闭包方式(addObserver(forName:object:queue:using:))注册的,需要保存返回的token并在deinit里移除:
    private var notificationToken: NSObjectProtocol?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        notificationToken = NotificationCenter.default.addObserver(forName: SomeNotificationName, object: nil, queue: .main) { [weak self] notification in
            // 处理通知逻辑
        }
    }
    
    deinit {
        if let token = notificationToken {
            NotificationCenter.default.removeObserver(token)
        }
        print("\nDeinit: \(self.description)\n")
    }
    

4. 确认移除方法的正确性

你的remove(asChildViewController:)方法完全符合Apple官方规范:先调用willMove(toParentViewController: nil),再移除视图,最后调用removeFromParentViewController(),这个逻辑可以直接保留。

验证方法

修改完成后切换TabBar,你应该能看到子VC的deinit日志正常打印了。如果还是没触发,建议用Xcode的Memory Graph Debugger(调试栏里的内存图标)查看子VC的引用链,定位还有哪些对象在持有它。


内容的提问来源于stack exchange,提问作者Andre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 06:52:27