翻译: 如何使用 Xcode 的内存图调试器检测 iOS 内存泄漏并保留周期

Posted 架构师易筋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了翻译: 如何使用 Xcode 的内存图调试器检测 iOS 内存泄漏并保留周期相关的知识,希望对你有一定的参考价值。

在 DoorDash,我们一直在努力通过提高应用程序的稳定性来提高我们的用户体验。这项工作的主要部分是防止、修复和消除我们大型代码库中的任何保留周期和内存泄漏。为了检测和修复这些问题,我们发现 Memory Graph Debugger 快速且易于使用。在我们的 Dasher ios 应用程序上显着提高了我们的无 OOM 会话率之后,我们想分享一些关于避免和修复保留周期的技巧,以及使用 Xcode 的内存图调试器的快速介绍,供不熟悉的人使用。

如果您对查明问题内存的根本原因感兴趣,请查看我们的新博客文章使用 BPF、perf 和 Memcheck 检查 C/C++ 应用程序中的问题内存,以获取有关内存如何工作的详细说明。

1. 什么是保留周期和内存泄漏?

iOS 中的内存泄漏是由于保留周期而无法释放内存中分配的空间量。由于 Swift 使用自动引用计数 (ARC),当两个或多个对象相互持有强引用时,就会发生保留循环。结果,这些对象在内存中相互保留,因为它们的保留计数永远不会减少到 0,这将阻止deinit被调用和内存被释放。

2、我们为什么要关心内存泄漏?

内存泄漏会逐渐增加应用程序中的内存占用,当它达到某个阈值时,操作系统 (iOS) 会触发内存警告。如果未处理该内存警告,您的应用程序将被强制终止,即OOM (内存不足)崩溃。如您所见,如果发生大量泄漏,内存泄漏可能会非常成问题,因为在使用您的应用程序一段时间后,应用程序会崩溃。

此外,内存泄漏可能会给您的应用带来副作用。通常,当观察者应该被释放时,它们被保留在内存中时会发生这种情况。这些泄露的观察者仍然会收听通知,并且在触发时应用程序很容易出现不可预测的行为或崩溃。在下一节中,我们将介绍 Xcode 的内存图调试器,然后在示例应用程序中使用它查找内存泄漏。

3. Xcode 的 Memory Graph Debugger 介绍

要打开,运行您的应用程序(在本例中,我正在运行一个演示应用程序),然后点击可视化调试器和位置模拟器按钮之间的 3 节点按钮。这将获取应用程序当前状态的内存快照。


左侧面板显示了此快照的内存中的对象,然后是其名称旁边的每个类的实例数。

前任: (MainViewController(1))

表示快照时内存中只有一个 MainViewController,下面是该实例在内存中的地址。

如果您在左侧面板上选择一个对象,您将看到将所选对象保存在内存中的引用链。例如,选择0x7f85204227c0underMainViewController将向我们显示如下图表:

  • 该大胆的 线条意味着有一个很强的参考所指向的对象。
  • 浅灰色线条表示它指向的对象存在未知的参考(可能是弱的或强的)。
  • 从左侧面板点击一个实例只会向您显示将所选对象保留在内存中的引用链。但它不会向您显示所选对象所引用的引用。

例如,要验证MainViewController具有强引用的对象中没有保留循环,您需要查看代码库以识别引用的对象,然后单独选择每个对象图以检查是否存在保留循环。

此外,内存图调试器可以自动检测简单的内存泄漏并提示您警告,例如这个紫色!标记。点击它会在左侧面板上显示泄漏的实例。

请注意,Xcode 的自动检测并不总是能捕捉到所有内存泄漏,而且通常您必须自己找到它们。在下一节中,我将解释使用内存图调试器进行调试的方法。

4. 使用 Memory Graph Debugger 的方法

捕获内存泄漏的一种有用方法是通过一些核心流程运行应用程序,并为第一次和后续迭代拍摄内存快照。

  1. 运行核心流程/功能并离开它,然后重复几次并拍摄应用程序的内存快照。查看内存中的对象以及每个对象存在的每个实例的数量。
  2. 检查保留周期/内存泄漏的这些迹象:
    • 在左侧面板中,您是否在列表中看到任何不应该存在或应该被释放的对象/类/视图等?
    • 是否有越来越多的类的相同实例保存在内存中?例如:在经过流程 4 次以上迭代后MainViewController (1)变为MainViewController (5)?
    • 查看左侧面板上的调试导航器,您是否注意到内存增加了?尽管恢复到原始状态,应用程序现在是否比以前消耗了更多的兆字节 (MB)
  3. 如果您发现了一个不应再存在于内存中的实例,那么您就发现了一个对象的泄漏实例。
  4. 点击泄漏的实例并使用对象图来追踪将其保留在内存中的对象。
  5. 当您追踪将对象链保留在内存中的父节点是什么时,您可能需要继续浏览对象图。
  6. 一旦您相信找到了父节点,请查看该对象的代码并找出循环强引用的来源并修复它。

在下一节中,我将通过我个人见过的导致保留循环的常见代码用例示例。要继续,请下载这个名为LeakyApp 的示例项目

5. 用一个例子修复内存泄漏

下载相同的 Xcode 项目后,运行该应用程序。我们将通过一个使用内存图调试器的示例。

  1. 应用程序运行后,您将看到三个按钮。我们将通过一个例子,所以点击“Leaky Controller”
  2. 这将显示ObservableViewController一个带有导航栏的空视图。
  3. 点击返回导航项。
  4. 重复几次。
  5. 现在拍摄内存快照。

拍摄内存快照后,您将看到如下内容:

由于我们多次重复这个流程,一旦我们返回主屏幕MainViewController,如果没有内存泄漏,observable 视图控制器应该已经被释放。但是,我们ObservableViewController (25)在左侧面板中看到,这意味着我们有 25 个该视图控制器的实例仍在内存中!另请注意,Xcode 未将此识别为内存泄漏!

现在,点击ObservableViewController (25)。您将看到对象图,它看起来类似于:


如您所见,它显示Swift closure context, 保留ObservableViewController在内存中。这个闭包由 保留在内存中__NSObserver。现在让我们转到代码并修复此泄漏。

现在我们转到文件ObservableViewController.swift. 乍看之下,我们有一个很常见的用例:
https://gist.github.com/chauvincent/33cf83b0894d9bb12d38166c15dd84a5

我们正在注册一个观察者viewDidLoad和删除自己作为一个观察者deinit。但是,这里有一个棘手的代码用法:
https://gist.github.com/chauvincent/b191414d54ba4cbb04614b1f85ac2e24

我们将函数作为闭包传递!默认情况下,这样做会self强烈捕获。您可以参考对象图来证明情况确实如此。

NotificationCenter似乎保持对闭包的强引用,并且handleNotification函数保持对 的强引用self,将 thisUIViewController和它持有的对象保持在内存中!

我们可以通过不将函数作为闭包传递并添加weak self到捕获列表来简单地解决此问题:

https://gist.github.com/chauvincent/a35a8f08c7dd4fc183ab2bd5b2ba5e6d

现在重建应用程序并重新运行该流程几次,并通过获取内存快照来验证对象现在是否已被释放。

ObservableViewController退出流程后,您应该会看到类似这样的内容在列表中无处可见!

内存泄漏已修复!🎉随意测试 LeakyApp 存储库中的其他示例,并通读评论。我在每个文件中都包含了注释,解释了每个保留周期/内存泄漏的原因。

6. 避免保留循环的其他提示

  1. 请记住,默认情况下使用函数作为闭包会保持强引用。如果必须将函数作为闭包传递并导致保留循环,则可以进行扩展或运算符重载以中断强引用。我不会讨论这个话题,但是网上有很多资源可以解决这个问题。
  2. 通过闭包使用具有动作处理程序的视图时,请注意不要在其自己的闭包中引用视图!如果你这样做了,你必须使用捕获列表来保持对该视图的弱引用,以及视图具有强引用的闭包。
    例如,我们可能有一些像这样的可重用视图:

https://gist.github.com/chauvincent/b2da3c76b0b811c947487ef3bf171d5a

在调用者中,我们有一些这样的演示代码:

https://gist.github.com/chauvincent/c049136b236c8b358d81ad16168a0243

这是一个保留循环,因为someModalVC'sactionHandler捕获了对someModalVC. 同时someModalVC强烈提到actionHandler

要解决此问题:

https://gist.github.com/chauvincent/fe868818e9be6f61cf3bc032539ff3a8
我们需要确保引用someModalVCweak通过更新捕获列表 [weak someModalVC] in 来打破保留循环。

  1. 当你在你的对象上声明属性并且你有一个协议类型的变量时,一定要添加一个类约束并weak根据需要声明它!这是因为如果您不添加类约束,编译器会默认给您一个错误。虽然众所周知,delegate委托模式应该是weak,但请记住,此规则仍然适用于其他抽象和设计模式,或您声明的任何协议变量。

例如,这里我们使用了一个干净的 swift 模式:

https://gist.github.com/chauvincent/8882082ea1280c722955b4803ca6854b

https://gist.github.com/chauvincent/15f52e6908a70ea36d099a16d2d660e2

在这里,我们需要OrdersListPresenter的view属性必须是弱引用,否则我们将有来自View-> Interacter-> Presenter->的强循环引用View。但是,当将该属性更新为时,weak var view: OrdersListDisplayLogic我们将收到编译器错误。


在将协议类型变量声明为弱变量时,此编译器错误可能看起来令人沮丧!但是在这种情况下,您必须通过向协议添加类约束来解决此问题!

https://gist.github.com/chauvincent/bbc2c2fc42df62bad61a9d4c49b0290e

总的来说,我发现使用 Xcode Memory Graph Debugger 是一种快速简便的查找和修复保留周期和内存泄漏的方法!我希望您发现这些信息有用,并在您开发时定期记住这些提示!谢谢!

参考

https://doordash.engineering/2019/05/22/ios-memory-leaks-and-retain-cycle-detection-using-xcodes-memory-graph-debugger/

以上是关于翻译: 如何使用 Xcode 的内存图调试器检测 iOS 内存泄漏并保留周期的主要内容,如果未能解决你的问题,请参考以下文章

面向开发的内存调试神器,如何使用ASAN检测内存泄漏堆栈溢出等问题

转使用Xcode和Instruments调试解决iOS内存泄露

内存检测工具Valgrind

如何检测 iPhone 上的内存泄漏?

如何检测大对象堆是不是导致内存不足异常

xcode5中调试时,CPU与Memory是代表啥