MVVM+Coordinator架构下SplashscreenViewController未销毁问题求助
我太懂这种找不到内存泄漏根源的头疼了——启动页控制器没法销毁,不仅占内存,还可能引发后续的逻辑问题。先从你给出的代码片段入手,再给你几个高频踩坑点和排查方法:
1. 先检查ViewModel的引用关系
你代码里定义了var viewModel: SplashscreenViewModelType!,这是VC对ViewModel的强引用。如果你的ViewModel反过来也强引用了当前VC(比如ViewModel里写了var viewController: SplashscreenViewController?),那直接就形成了循环引用:VC → ViewModel → VC,两者都没法被释放。
解决思路:ViewModel对VC的引用必须是弱引用,改成weak var viewController: SplashscreenViewController?,或者用闭包回调的方式(闭包里要捕获weak self)。
2. 动画/闭包的self捕获是重灾区
启动页通常会有动画逻辑,如果你用了UIView动画、Completion闭包或者其他异步任务,闭包里直接写self很容易触发循环引用。比如类似这样的代码:
UIView.animate(withDuration: animationDuration) { self.view.transform = CGAffineTransform(scaleX: self.animationEndScale, y: self.animationEndScale) } completion: { finished in self.viewModel.notifyAnimationComplete() }
这里闭包会强捕获self,而如果闭包被系统动画队列持有,同时VC又持有ViewModel,ViewModel再持有VC的话,就会形成闭环。
解决思路:闭包里一定要用[weak self](或者[unowned self],但weak更安全):
UIView.animate(withDuration: animationDuration) { [weak self] in guard let self = self else { return } self.view.transform = CGAffineTransform(scaleX: self.animationEndScale, y: self.animationEndScale) } completion: { [weak self] finished in self?.viewModel.notifyAnimationComplete() }
3. 其他可能的强引用来源
- 定时器(Timer):如果VC创建了Timer,并且把target设为self,Timer会被RunLoop强引用,同时VC又持有Timer的话,就会循环。一定要在
deinit里调用timer.invalidate()。 - 通知中心观察者:虽然iOS 9+系统会自动移除通知观察者,但如果是自定义的通知中心,或者注册时用了block方式且捕获了self,还是可能有问题。建议在
deinit里主动移除观察者:deinit { NotificationCenter.default.removeObserver(self) } - 闭包属性:如果VC有自己的闭包属性,且闭包里捕获了self,同时这个闭包被外部对象持有,也会导致循环。比如:
同样要改成var onDismiss: (() -> Void)? // 外部赋值时如果捕获了self,就有风险 onDismiss = { self.navigationController?.popViewController(animated: true) }[weak self]。
快速定位问题的工具
别光靠Malloc栈,用Xcode的Memory Graph Debugger更直接:
- 运行App,进入启动页后跳转到其他页面
- 点击Debug工具栏里的内存图按钮(📊)
- 在内存图里找到
SplashscreenViewController的实例,查看它的「Retained By」引用链,就能清楚看到是谁还在持有它的强引用。
另外,给VC加个deinit方法验证:
deinit { print("✅ SplashscreenViewController 已销毁") }
如果控制台没打这个日志,说明确实存在强引用没释放。
内容的提问来源于stack exchange,提问作者Konrad Siemczyk

