iOS视图控制器 Present 内存溢出排查与解决
2024-12-28 21:13:09
解决Present视图控制器时的内存溢出问题
应用在UINavigationController
栈中存在多个视图控制器(例如:vcA -> vcB -> vcC)的情况下,当从vcC present一个新的视图控制器时,出现内存持续增长,直至应用崩溃(表现为内存溢出错误),这表明在vcC的上下文环境中存在某些资源或引用管理的问题。此现象,在从vcA或vcB present 视图控制器时不会出现。当 VerticalButton
从 xib 文件中删除,问题得到解决。 此案例分析一个在 Present 视图控制器时内存泄漏的常见问题,并提供相应的解决方案。
问题根源分析
此问题通常和循环引用有关。当某个对象拥有其内部另一个对象时,内部对象反过来也持有外部对象的强引用,从而造成内存泄漏。 具体到此场景,VerticalButton
在 xib 加载和更新时可能会持续创建,并由于不正确的引用关系导致无法被释放。updateButtonSize()
方法调用非常频繁。当多个 VerticalButton
同时存在时,或者,如果在 view controller 加载时,设置和更新按钮外观的过程中存在强引用问题,就容易触发 Out of memory
。 视图控制器加载新视图,伴随动画和系统事件可能加剧该问题。
解决方案
1. 排查循环引用
检查 VerticalButton
以及 vcC 的代码,尤其注意是否有 self
被不正确地持有。尤其注意以下几种情况:
- 闭包引用 : 如果你在按钮点击事件的闭包中使用了
self
,必须使用[weak self]
或[unowned self]
捕获 self,避免造成循环引用。 - 代理/委托模式 : 若
VerticalButton
有任何委托,确认该委托不是由自身(VerticalButton
)或其父视图控制器持有的。请将协议委托者改为 weak,避免造成循环引用。
class SomeViewController: UIViewController {
var myButton: VerticalButton!
func configureButtonAction() {
myButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
//错误用法
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// 这会产生循环引用。闭包持有 self。当view controller销毁时无法释放。
print("Button tapped!")
self.dismiss(animated: true)
}
}
//修正
@objc func fixed_buttonTapped() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in // [weak self] 表示,self只是弱引用
guard let strongSelf = self else {
return //避免self变成 nil 时 crash。
}
print("Button tapped!")
strongSelf.dismiss(animated: true)
}
}
// or using unowned, this may be less safe then `weak self` in many cases, especially async code.
@objc func fixed_buttonTapped2() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in
print("Button tapped!")
self.dismiss(animated: true)
}
}
}
操作步骤 :
- 使用代码编辑器检查
VerticalButton
和vcC 代码的闭包、委托及其他持有引用的场景。 - 根据上文代码修正不正确的引用,使用
weak
或者unowned
修饰 self 捕获. - 重新运行应用,观察是否仍然出现内存泄漏。
2. 检查VerticalButton
的尺寸更新机制
VerticalButton
中的updateButtonSize()
方法在 layoutSubviews()
, setTitle(_:for:)
, 和 setImage(_:for:)
中被频繁调用。如果按钮的状态快速更改,可能导致 updateButtonSize()
的重复调用。请注意:
- 布局机制 : 如果你的布局设置是通过
constraints
来实现,确保更新视图约束的方式是高效的,不会引起额外的内存使用。 如果使用frame
,bounds
手动更新 UI 元素布局时, 应当确保逻辑是高效且没有冗余计算。
优化方案
private var shouldUpdateSize = true //标记当前时候是否需要刷新
override func layoutSubviews() {
if shouldUpdateSize { // 避免多次刷新计算
super.layoutSubviews()
updateButtonSize()
shouldUpdateSize = false
DispatchQueue.main.async {
self.shouldUpdateSize = true
}
}
}
override func setTitle(_ title: String?, for state: UIControl.State) {
super.setTitle(title, for: state)
shouldUpdateSize = true; // 标记下一次需要重新更新UI
}
override func setImage(_ image: UIImage?, for state: UIControl.State) {
super.setImage(image, for: state)
shouldUpdateSize = true // 标记下一次需要重新更新UI
}
操作步骤
1. 添加 shouldUpdateSize 标志位,避免 updateButtonSize()
不必要的重复调用。
2. 修改 setTitle
和 setImage
方法,标记下一次布局更新。
3. 重复上述循环引用排查流程,排除其他可能。
3. 使用 Instruments 分析内存使用
Xcode 的 Instruments
工具是检测内存泄漏的利器。 使用 Allocations
模板可以追踪内存的分配和释放,有助于发现无法释放的对象。
操作步骤 :
- 打开 Xcode 并选择 “Product” > “Profile”.
- 选择 “Allocations” 模板.
- 启动你的应用并重现内存泄漏的问题。
- 检查 Allocations 数据列表,关注哪些对象的内存一直在增长,并未得到释放。特别关注自定义的view和Controller。
- 找出内存泄漏的点,并根据泄漏原因, 调整程序代码。
额外的安全建议 :
1. 及时取消操作 : 对于使用 Timer
, DispatchWorkItem
, 和 网络请求的场景,及时停止这些操作,并及时置nil。避免在释放controller时, 未能及时取消计时器或网络请求导致资源未释放
2. 谨慎使用静态变量 :静态变量的声明周期是应用级的。 不必要情况不要滥用静态变量。
总结
此场景了从特定视图控制器 present 新的视图控制器可能造成的内存溢出问题,特别当界面中使用复杂自定义控件 VerticalButton
时,问题更容易被触发。定位和修复这类问题需要仔细检查代码是否存在循环引用,并优化资源管理方式, 并辅助 Xcode Instruments
工具追踪问题根源。合理的架构设计可以避免很多此类问题的发生,请定期复审应用架构并不断调整和改进。