返回

iOS视图控制器 Present 内存溢出排查与解决

IOS

解决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)

            }
        }

}

操作步骤

  1. 使用代码编辑器检查VerticalButton 和vcC 代码的闭包、委托及其他持有引用的场景。
  2. 根据上文代码修正不正确的引用,使用weak 或者 unowned修饰 self 捕获.
  3. 重新运行应用,观察是否仍然出现内存泄漏。

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. 修改 setTitlesetImage 方法,标记下一次布局更新。
3. 重复上述循环引用排查流程,排除其他可能。

3. 使用 Instruments 分析内存使用

Xcode 的 Instruments 工具是检测内存泄漏的利器。 使用 Allocations 模板可以追踪内存的分配和释放,有助于发现无法释放的对象。

操作步骤

  1. 打开 Xcode 并选择 “Product” > “Profile”.
  2. 选择 “Allocations” 模板.
  3. 启动你的应用并重现内存泄漏的问题。
  4. 检查 Allocations 数据列表,关注哪些对象的内存一直在增长,并未得到释放。特别关注自定义的view和Controller。
  5. 找出内存泄漏的点,并根据泄漏原因, 调整程序代码。

额外的安全建议 :
1. 及时取消操作 : 对于使用 Timer, DispatchWorkItem, 和 网络请求的场景,及时停止这些操作,并及时置nil。避免在释放controller时, 未能及时取消计时器或网络请求导致资源未释放
2. 谨慎使用静态变量 :静态变量的声明周期是应用级的。 不必要情况不要滥用静态变量。

总结

此场景了从特定视图控制器 present 新的视图控制器可能造成的内存溢出问题,特别当界面中使用复杂自定义控件 VerticalButton 时,问题更容易被触发。定位和修复这类问题需要仔细检查代码是否存在循环引用,并优化资源管理方式, 并辅助 Xcode Instruments 工具追踪问题根源。合理的架构设计可以避免很多此类问题的发生,请定期复审应用架构并不断调整和改进。