Swift调用子视图控制器偶发致命错误排查求助
偶发子视图控制器崩溃问题排查:已释放的unowned引用错误
问题概述
地图页面长按打开子视图控制器TripOverviewVC时,偶发崩溃,控制台错误:
Fatal error: Attempted to read an unowned reference but object 0x283f3cf80 was already deallocated
崩溃触发概率极低,需多次长按复现,无法通过断点调试。添加日志后发现:正常场景下CHECK 1-16顺序输出,崩溃时卡在CHECK 14与CHECK 15之间;子VC的viewWillAppear(输出CHECK 10)可正常触发,但viewDidAppear(CHECK 15)从未执行。
相关代码
父VC调用子VC代码
let slideVC = TripOverviewVC(customer: myCustomer, route: myRoute, panelController: panelController!, annotationManager: annotationManager!, mapview: mapView!) slideVC.view.roundCorners([.topLeft, .topRight], radius: 22) slideVC.view.layer.zPosition = 15 print("CHECK 8") add(slideVC) print("CHECK 13") let height = view.frame.height let width = view.frame.width slideVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height) print("CHECK 14")
父VC的add子VC实现
func add(_ child: UIViewController) { addChild(child) print("CHECK 9") view.addSubview(child.view) print("CHECK 11") child.didMove(toParent: self) print("CHECK 12") }
子VC的viewDidAppear实现
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("CHECK 15") UIView.animate(withDuration: 0.3) { [weak self] in let frame = self?.view.frame let yComponent = UIScreen.main.bounds.height - self!.view.frame.height*0.5 self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height) } tmpOrigin = self.view.frame.origin print("CHECK 16") }
问题分析
核心触发点:unowned引用访问已释放对象
崩溃提示明确指向unowned修饰的对象已被回收,大概率是TripOverviewVC内部用unowned声明了某些属性(比如初始化时传入的panelController、annotationManager、mapView),且该对象的生命周期短于子VC,导致在流程中被访问时已被系统释放。生命周期异常:viewDidAppear未触发
子VC的viewWillAppear触发但viewDidAppear未执行,说明视图未完成完整的显示流程,可能是子VC在布局阶段被意外释放,或者视图层级操作打断了生命周期回调链。代码隐性隐患
- 初始化子VC时使用强制解包(
panelController!等),虽不是当前偶发崩溃的直接原因,但存在nil崩溃风险; viewDidAppear的动画闭包中,使用[weak self]后又强制解包self!,若self被释放会触发崩溃;- 父VC未显式持有子VC,仅通过
addChild关联,内存紧张时可能被系统提前回收。
解决方案
- 排查并修复unowned引用
检查TripOverviewVC中所有unowned修饰的属性,将非必要的unowned改为weak(需改为可选类型),或确保被引用对象的生命周期长于子VC:
// 原可能的unowned声明 unowned var panelController: PanelController // 改为weak weak var panelController: PanelController?
- 修复强制解包问题
修改viewDidAppear中的动画闭包,避免强制解包:
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("CHECK 15") UIView.animate(withDuration: 0.3) { [weak self] in guard let self = self else { return } let frame = self.view.frame let yComponent = UIScreen.main.bounds.height - frame.height*0.5 self.view.frame = CGRect(x: 0, y: yComponent, width: frame.width, height: frame.height) } tmpOrigin = view.frame.origin print("CHECK 16") }
- 确保父VC强持有子VC
在父VC中添加属性存储子VC,避免被提前释放:
// 父VC中声明 private var currentSlideVC: TripOverviewVC? // 调用时赋值 currentSlideVC = TripOverviewVC(customer: myCustomer, route: myRoute, panelController: panelController!, annotationManager: annotationManager!, mapview: mapView!) let slideVC = currentSlideVC! // 后续操作不变...
- 调整子VC添加流程
将frame设置逻辑移至子VC内部(如viewWillLayoutSubviews),避免父VC直接操作子VC视图导致生命周期异常:
// TripOverviewVC的viewWillLayoutSubviews override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() let screenBounds = UIScreen.main.bounds view.frame = CGRect(x: 0, y: screenBounds.maxY, width: screenBounds.width, height: screenBounds.height) }
内容的提问来源于stack exchange,提问作者Jan L
相关产品推荐
相关产品推荐

