iOS Swift:如何确保子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

