【问题标题】:iOS app crashing in debug mode, working in release modeiOS应用程序在调试模式下崩溃,在发布模式下工作
【发布时间】:2019-09-11 07:57:50
【问题描述】:

有几个问题与此完全相反,我不明白在发布模式下运行我的应用程序的工作原理/原因,但在调试模式下崩溃并出现 EXC_BAD_ACCESS 错误。

崩溃的方法是递归的,非常!!重大的;只要没有太多递归,它在调试(iPhone XS 上少于 ~1000,模拟器上无限制)和发布模式(无限制?)中都可以正常工作。

我不知道从哪里开始找出如何调试调试模式,我想知道是否由于堆栈跟踪或其他未知因素捆绑了某种递归软限制?甚至可以归结为电缆,因为我能够在模拟器中成功运行而没有问题?

我应该注意到 Xcode 在看似随机的地方报告崩溃,例如我知道已实例化且有效的属性 getter;以防万一。

我打算将它重构为更小的块,但我想我会在这里发布,以防有​​人对可能导致此问题的原因有任何想法。

见: https://gist.github.com/ThomasHaz/3aa89cc9b7bda6d98618449c9d6ea1e1

【问题讨论】:

  • 鉴于它在 iOS 设备上的崩溃比在模拟器上更快,我首先怀疑正在使用多少内存(因为模拟器可以利用计算机的所有内存,而不是有限的设备的内存)。您是否观察过运行时的消耗?在回答您的问题时,不,我不怀疑调试构建中有任何人为限制,也没有连接问题。我怀疑调试构建的额外安全检查和/或未优化的构建将占用更多内存。因此,请监控应用程序的内存量表或使用 Instruments 的“分配”工具。
  • 顺便说一下,如果您为您的方案(僵尸、任何 malloc 功能等)打开了任何诊断选项,请确保在完成后将其关闭那个特殊的诊断。它们中的许多会增加严重的内存开销,只会加剧内存问题。做您需要的诊断,但完成后将其关闭。
  • @Rob,没有内存消耗问题,峰值使用大约 130MB,发布版本峰值使用大约 100MB
  • 顺便说一句,如果你简单地将它重构为更小的方法(这通常是一个好主意),它实际上可能会使这里的特定问题变得更糟,更快地耗尽堆栈内存。任何方法调用都会向堆栈推送更多内容(除非优化器将其内联)。

标签: ios xcode debugging


【解决方案1】:

您的堆栈内存不足。

考虑这个非常简单的递归函数,将 1 和 n 之间的整数相加:

func sum(to n: Int) -> Int {
    guard n > 0 else { return 0 }
    return n + sum(to: n - 1)
}

您会发现,例如,如果您尝试将 1 到 100,000 之间的数字相加,则应用在发布版本和调试版本中都会崩溃,但只会在调试版本中更快地崩溃。我怀疑在调试版本中只有更多的诊断信息被推送到堆栈上,导致它更快地耗尽堆栈中的空间。在上述的发布版本中,堆栈指针每次递归调用都会提前 0x20 字节,而调试版本每次都会提前 0x80 字节。如果你在递归函数中做任何实质性的事情,这些增量可能会更大,并且在更少的递归调用时可能会发生崩溃。但是我的设备(iPhone Xs Max)和我的模拟器(Thread.current.stackSize)上的堆栈大小是 524,288 字节,这对应于堆栈指针前进的数量和我能够递归调用的最大数量达到。如果您的设备比模拟器更早崩溃,则可能是您的设备内存较少,因此分配了较小的stackSize

归根结底,如果您想享受快速性能但又不想招致巨大调用堆栈的内存开销,您可能希望将您的算法重构为非递归算法。顺便说一句,上述的非递归再现比递归再现快一个数量级。

或者,您可以异步调度递归调用,这样可以消除堆栈大小问题,但会引入 GCD 开销。上述的异步再现比简单的递归再现慢了两到三个数量级,显然,比迭代再现慢了另一个数量级。

诚然,我的简单 sum 方法是如此微不足道,以至于递归调用的开销开始占整个计算时间的很大一部分,并且鉴于您的例程似乎更复杂,我怀疑差异会不那么鲜明。尽管如此,如果您想避免用完堆栈空间,我只是建议采用非递归再现。


建议您观看以下 WWDC 视频:


值得注意的是,深度递归例程并不总是需要消耗大量堆栈。值得注意的是,有时我们可以使用tail-recursion,我们的递归调用是最后一次调用。例如。我上面的 sn-p 没有使用尾调用,因为它将n 添加到递归调用返回的值中。但是我们可以重构它以传递运行总计,从而确保递归调用是真正的“尾调用”:

func sum(to n: Int, previousTotal: Int = 0) -> Int {
    guard n > 0 else { return previousTotal }
    return sum(to: n - 1, previousTotal: previousTotal + n)
}

发布构建足够聪明,可以优化这种尾递归(通过称为“尾调用优化”,TCO,也称为“尾调用消除”的过程),缓解递归调用的堆栈增长。 WWDC 2015 Profiling in Depth 在另一个主题上,时间分析器,准确地展示了优化尾调用时发生的情况。

最终效果是,如果您的递归例程使用尾调用,则发布构建可以使用尾调用消除来缓解堆栈内存问题,但调试(非优化)构建不会这样做。

【讨论】:

  • 这绝对是有道理的。我猜内存调试器只显示整个应用程序的内存占用,所以当我看到如此相对较低的数字时,我想不出它怎么可能是内存问题。我认为我在软限制方面是正确的。 :)
  • @ThomasHaz - 仅供参考,我在上面添加了一些 WWDC 链接。我还添加了关于尾递归的讨论。
【解决方案2】:

EXEC_BAD_ACCESS 通常意味着您正在尝试访问不在内存中或可能未正确初始化的对象。

检查您的代码,如果您在以某种方式删除 Dictionary 变量后访问它?您的变量是否正确初始化?您可能已经声明了变量,但没有对其进行初始化和访问。

可能有很多原因,没有看到任何代码就不能说太多。

尝试打开 NSZombieOjects - 这可能会为您提供更多调试信息。参考这里How to enable NSZombie in Xcode?

如果您想知道错误发生的确切位置和时间,您可以使用仪器检查内存泄漏。这可能会有所帮助http://www.raywenderlich.com/2696/instruments-tutorial-for-ios-how-to-debug-memory-leaks

【讨论】:

  • 感谢您的快速回复。泄漏不会抛出任何东西,不幸的是 NSZombieObjects 也不会。它在模拟器和我的 iPhone 上的发布模式下工作正常,只是在我的 iPhone 上不是调试模式。
  • 不,我已经在要点中发布了我的庞大功能。
猜你喜欢
  • 2018-03-22
  • 1970-01-01
  • 1970-01-01
  • 2018-07-01
  • 2019-12-05
  • 1970-01-01
  • 2012-08-20
  • 2015-11-25
  • 2018-07-31
相关资源
最近更新 更多