【问题标题】:Intercepting crashes on iOS在 iOS 上拦截崩溃
【发布时间】:2019-02-12 11:12:30
【问题描述】:

说明

我想捕获 iOS 应用程序中发生的所有异常并将它们记录到文件中,并最终将它们发送到应用程序使用的后端服务器。

我一直在阅读有关此主题的内容,并发现了设备发送的信号的用法和处理它们,但我不确定它是否会违反 App Store Review 指南,或者它可能会引入其他问题。

我已将以下内容添加到AppDelegate

NSSetUncaughtExceptionHandler { (exception) in  
    log.error(exception)  
}  

signal(SIGABRT) { s in  
    log.error(Thread.callStackSymbols.prettified())  
    exit(s)  
}  

signal(SIGILL) { s in  
    log.error(Thread.callStackSymbols.prettified())  
    exit(s)  
}  

signal(SIGSEGV) { s in  
    log.error(Thread.callStackSymbols.prettified())  
    exit(s)  
}

问题

  • 这是好方法吗,还有其他方法吗?
  • 是否会因为使用 exit() 而违反 App Store Review 准则
  • kill(getpid(), SIGKILL)代替exit()会更好吗?

资源

【问题讨论】:

  • Crashlytics 做同样的工作。据我所知,这是允许的,您可以使用 exit() 或 abort()。我不会使用 kill(getpid(), SIGKILL)
  • 如果您只想记录错误,那么您根本不需要调用exit()kill(),因为只要从信号处理程序返回就会使用堆栈跟踪终止应用程序。如果您使用的是 Crashlytics(或其他类似的框架),那么肯定有关于何时设置信号处理程序以及可能将处理程序调用链接到先前设置的处理程序等的要求。
  • 一定要使用像 Crashlytics 这样的第三方服务,顺便说一句,它现在是 Firebase 的一部分。
  • @SachinVas killabortexit 之间的主要区别是什么? @trojanfoe我会尝试不调用killexit,看看应用程序是否会正常崩溃,我第一次尝试不返回它实际上开始无限填充日志文件@pckil因为公司严格使用第三方框架,特别是那些处理他们服务器上的数据 Crashlytics 并不是一个真正的选择。需要灵活性。
  • @NajdanTomić 一旦看到信号,您可能需要将自己作为信号处理程序移除......

标签: ios swift logging


【解决方案1】:

这里是前 Crashlytics iOS SDK 维护者。

您上面编写的代码确实存在许多技术问题。

首先,实际上很少有函数被定义为可以在信号处理程序中安全调用。 man sigaction 列出了它们。您编写的代码不是信号安全的,并且会不时发生死锁。这一切都取决于崩溃的线程当时正在做什么。

第二个是您试图在处理程序之后退出程序。您必须记住,信号/异常处理程序是进程范围的资源,并且您可能不是唯一使用它们的人。您必须保存预先存在的处理程序,然后在处理后恢复它们。否则,您可能会对应用程序可能正在使用的其他系统产生负面影响。正如您目前所写的那样,即使是 Apple 自己的崩溃报告器也不会被调用。但是,也许你想要这种行为。

第三,您没有捕获所有线程堆栈。这是崩溃报告的关键信息,但增加了很多复杂性。

第四,信号实际上不是最低级别的错误系统。不要与运行时异常(即NSException)混淆ma​​ch 异常 是用于在iOS 上实现信号的底层机制。它们是一个更强大的系统,但也更复杂。信号有很多陷阱和限制,马赫异常可以绕过。

这些只是我脑海中浮现的问题。崩溃报告是一项棘手的工作。但是,我不想让你认为它是魔法,当然不是。您可以构建一个有效的系统。

我确实想指出的一件事是,崩溃报告者不会就失败给你任何反馈。因此,您可能构建的东西在 25% 的时间内都有效,并且因为您只看到有效的报告,您会认为“嘿,这很好用!”。 Crashlytics 多年来一直在努力找出失败的原因并试图减轻它们的影响。如果您对这一切都感兴趣,您可以查看我所做的关于 Crashlytics 系统的 talk

更新:

那么,如果您发布此代码会发生什么?嗯,有时你会得到有用的报告。有时,您的崩溃处理代码本身会崩溃,这将导致无限循环。有时你的代码会死锁,并有效地挂起你的应用程序。

Apple 已将 exit 设为公共 API(无论好坏),因此您绝对可以使用它。

我建议仅出于学习目的继续走这条路。如果您有真正关心的应用程序,我认为集成现有的开源报告系统并将其指向您控制的后端服务器会更有责任感。没有第三者,也不必担心弊大于利。

【讨论】:

  • 非常感谢您指出技术问题并解释后台发生的情况。我注意到这根本不安全,即使应用程序实际上已经在我的代码中处于崩溃状态,它也更加不稳定和不可预测。仅在应用程序被强制关闭后看到日志文件被填充真的很可怕。这绝对是需要大量研究和充分理解的事情。我也会检查你的谈话。
  • 不客气!请问我你有什么问题。而且,不必害怕!您只是看到带有崩溃报告的正常内容。事情有效,但不能依赖它们在所有情况下都有效。老实说,相对于其他可靠性问题,异步安全并不是那么重要。但是,总体而言,它们是一个重要方面。
  • 感谢鼓励。演讲也很棒,对于像我这样对这个话题不太了解的人来说,这是可以理解的。我想至少记录一些东西来帮助调试崩溃而不会将数据暴露给第三方。由于崩溃报告系统是独立的业务,我预计会遇到这样的挑战。我可以处理NSSetUncaughtExceptionHandler,但我想记录错误而不仅仅是引发异常。总结一下:这是一种有效的方法,但执行起来很糟糕,这样的代码应该通过 Apple Review 流程。我对么?我可以通过 Twitter dm 联系你吗?
  • 您绝对可以联系我。但是,我可能想在这里发表一些我的想法,以防其他人跟进。至于NSSetUncaughtExceptionHandler,我什至不会浪费我的时间。绝大多数崩溃都不是 Obj-C 异常。因此,完成所有工作以捕获、打包和转发报告,然后在服务器端处理它们似乎并不值得。
  • @iosaddicted 它可以看到类似于“atexit”功能的东西。但是,我从未添加过这一点,而且我认为此后不太可能有人添加。
【解决方案2】:

结论

可以创建自定义崩溃报告器,但绝对不建议这样做,因为背景中有很多事情很容易被遗忘,并且会引入很多未定义的行为。即使使用第三方框架也可能会很麻烦,但通常这是更好的方法。

感谢大家提供有关此主题的信息。

问题解答

这是好方法吗,还有其他方法吗?

我在原始问题中提到的方法将对 Apple 自己的崩溃报告器产生影响,并且由于信号处理不当而引入了未定义的行为。 UNIX 信号并未涵盖所有错误,API 处理与异步信号安全函数一起工作。 Apple 的崩溃报告器使用的 Mach 异常处理是更好的选择,但它更复杂。

exit() 的使用会破坏 Apple App Store 的审核吗?

没有。 exit()的使用更多的是与app的正常运行相关。如果应用程序仍然崩溃,调用exit() 不是问题。

使用 kill(getpid(), SIGKILL) 代替 exit() 会更好吗?

爱斯基摩人的引述:

你不能调用exit。这样做有两个问题:

exit 不是异步信号安全的。其实exit可以运行任意代码 通过在 atexit 注册的处理程序。如果你想退出进程, 调用_exit。

无论如何,退出进程是个坏主意,因为它要么 阻止 Apple 崩溃报告器运行或导致其记录 不正确的状态(信号处理程序的状态,而不是 崩溃线程的状态)。

更好的解决方案是取消注册您的信号处理程序(将其设置为 SIG_DFL) 然后返回


其他详细信息(完整上下文)

自从我将这个问题交叉发布到 Apple's official support forum 并从著名的爱斯基摩人那里得到了非常长且描述性的答案,我想与任何决定走与我一样的道路并开始研究这种方法的人分享。

爱斯基摩人的引述

在我们开始之前,我想让你看看我闪亮的新 Implementing Your Own Crash Reporter post。我一直想写这个 一段时间,你的问题给了我一个很好的借口来分配 时间。

你写道:

我需要捕获所有发生在 iOS 应用程序并将它们记录到文件并最终将它们发送到后端 应用使用的服务器。

我强烈建议不要这样做。我的Implementing Your Own Crash Reporter 帖子解释了为什么这很难。它也有一些 关于如何避免问题的建议,但最终没有办法 实现一个可靠的、二进制的第三方崩溃报告器 兼容,足以调试复杂问题

除此之外,让我们看看你的具体问题:

这是一种好方法吗?

没有。问题是你的极简崩溃记者会破坏 苹果崩溃记者的行为。上述帖子 详细讨论了这个问题。

是否会因为使用 exit() 而违反 App Store Review 准则?

没有。 iOS禁止调用exit都是正常的 您的应用程序的操作。如果您的应用程序仍然崩溃,请调用 exit 不是问题。

但是,调用 exit 会加剧我在 上一点。

使用 kill(getpid(), SIGKILL) 会更好吗?

这不会显着改善情况。

callStackSymbols 没有符号化,有没有符号化的方法 调用StackSymbols?

没有。设备上的符号化非常棘手,应该 避免。同样,我在引用的帖子中对此进行了详细介绍 以上。

分享和享受


由于链接可能会断开,我也会引用帖子。

实现您自己的崩溃报告器

我经常收到有关第三方崩溃报告的问题。这些 通常出现在以下两种情况之一:

  • 人们正在尝试实现自己的崩溃报告器。
  • 人们已经实现了自己的崩溃报告器,并试图根据它生成的报告来调试问题。

这是一个复杂的问题,这篇文章是我试图解开一些 那种复杂性。

如果您对我在这里提出的任何问题有任何后续问题, 请在中开始一个新线程。

重要以下所有内容都是我自己的直接经验。这些都不应该被视为官方的 DTS 政策。如果您有问题 需要一个正式的答案(也许你试图说服你的 老板认为实现自己的崩溃报告是一个非常糟糕的主意:-), 你应该打开一个DTS tech support incident,我们可以 在那里讨论事情。

分享和享受 — Quinn “爱斯基摩人!”苹果开发者关系, 开发者技术支持,核心操作系统/硬件let myEmail = "eskimo" + "1" + "@apple.com"


范围

首先,我只能谈谈这个问题的技术方面。那里 是超出我职权范围的其他方面:

  • 我不为 App Review 工作,只有他们才能就商店允许或不允许的内容给出明确的答案。
  • 制作自己的崩溃报告器会影响隐私。

重要提示如果您实施自己的崩溃报告器,请与律师讨论隐私影响。

本文假设您正在实现自己的崩溃报告器。 很多人使用来自另一个第三方的崩溃报告器。从我的 透视这些是同一件事。如果您使用自定义崩溃 记者,你要为它的行为负责,无论好坏, 无论实际代码来自何处。

注意如果您使用来自其他第三方的崩溃报告器,请运行保留 Apple 崩溃报告中概述的测试以验证 它运行良好。

一般建议

我强烈建议您不要实现自己的崩溃报告器。 实现一个运行良好的基本崩溃报告器非常容易 足以调试简单的问题。创造一个好东西是不可能的 崩溃报告器,可靠、二进制兼容且足够 调试复杂的问题。

“不可能?”,我听到你问,“这对奎因来说是一个非常强烈的词 采用。他通常更加谨慎。”是的,这是真的,我 通常比较谨慎,但在这种情况下,我非常 对这个结论充满信心。

实现自己的崩溃有两个基本问题 记者:

  • 在 iOS(以及其他基于 iOS 的平台、watchOS 和 tvOS)上,您的崩溃报告器必须在崩溃的进程中运行。这意味着它可以 永远不会 100% 可靠。如果进程崩溃,则通过 定义,它处于未定义状态。尝试做真正的工作 这种状态只是在问问题1

  • 为了获得好的结果,您的崩溃报告器必须与系统实现细节密切相关。这些可以从发布更改为 发布,这会使您的崩溃所做的假设无效 记者。这对 Apple 崩溃记者来说不是问题,因为 它随系统一起提供。但是,内置的崩溃报告器 你的产品总是会变脆的。

    我说的是这里来之不易的经验。我在 DTS 工作期间 PowerPC 到 Intel 的过渡,并且看到很多人有定制 事故记者在这个过程中挣扎。

不过,这篇文章仍然存在,因为很多人忽略了我的将军 建议,因此后续部分包含有关特定的建议 技术问题。

警告请勿将以下任何内容解释为鼓励实施您自己的崩溃报告器。我强烈建议不要这样做。 但是,如果您忽略我的建议,那么您至少应该尝试 将风险降至最低,这就是本文档其余部分的内容。

1 在 macOS 上,您的崩溃报告器可能会用完 过程,就像苹果崩溃记者一样。然而,这呈现 它自己的问题:当进程用完时,您无法访问各种 崩溃进程的临界状态位,而不是紧密 绑定到不被视为 API 的实现细节。

保留 Apple 崩溃报告

您必须确保您的崩溃报告者不会扰乱 Apple 崩溃记者。您的部分崩溃不会由以下原因引起 您的代码,但由于框架代码中的问题,以及写得不好 崩溃记者将扰乱苹果崩溃记者并使其 更难诊断这些问题。

此外,在处理真正难以调试的问题时,您 真的需要苹果崩溃中显示的更模糊的信息 报告。如果你破坏了这些信息,你最终会遇到难题 更难。

为避免这些问题,我建议您测试崩溃报告器的 对苹果崩溃记者的影响。基本思路是:

  1. 创建一个生成一组特定崩溃的程序。
  2. 运行每次崩溃。
  3. 验证您的崩溃报告器是否产生了合理的结果。
  4. 验证 Apple 崩溃报告器是否也产生了合理的结果。

关于第 1 步,您的测试套件应包括:

  • 您的代码引发了未处理的语言异常
  • 操作系统抛出未处理的语言异常(越界访问 NSArray 是一种简单的方法)
  • 内存访问异常
  • 非法指令异常
  • 断点异常

确保在主线程和 辅助线程。

关于第 4 步,检查生成的 Apple 崩溃报告 包括正确的值:

  • 异常信息
  • 崩溃的线程
  • 那个线程的状态
  • 任何特定于应用程序的信息,尤其是最后一个异常回溯

信号

许多第三方崩溃报告器使用 UNIX 信号来捕捉崩溃。 这是一个耻辱,因为使用 Mach 异常处理,机制 苹果崩溃记者使用的,通常是更好的选择。 然而,有两个理由支持 UNIX 信号而不是 Mach 异常处理:

  • 在基于 iOS 的平台上,您的崩溃报告器必须在进程内运行,并且执行进程内 Mach 异常处理是不可行的。
  • 人们对 UNIX 信号更加熟悉。 Mach 异常处理和一般的 Mach 消息传递非常晦涩难懂。

如果您为崩溃报告器使用 UNIX 信号,请注意 API 有一些巨大的缺陷。首先,您的信号处理程序 只能使用异步信号安全函数1。你可以找到一份清单 sigaction man page 中的这些功能 2.

警告此列表不包括malloc。这意味着崩溃报告器的信号处理程序不能使用 Objective-C 或 Swift,因为 无法限制这些语言运行时的分配方式 记忆。这意味着你被 C 或 C++ 困住了,但即便如此你 必须小心遵守此约束。

特工:比你知道的还要糟糕。

许多崩溃报告使用backtrace 之类的函数(参见其man page) 从他们的信号处理程序中获取回溯。有两个问题 用这个:

  • backtrace 不是异步信号安全函数。
  • backtrace 使用了一种无法很好地处理交叉信号处理程序堆栈帧 [3] 的简单算法。

后一个例子尤其令人担忧,因为它隐藏了 触发信号的堆栈帧的标识。

如果你要回溯一个信号,你必须使用 crashed 线程的状态(可通过处理程序uapparameter 访问)开始 你的回溯。

如果您的崩溃报告者想要记录 崩溃的线程,这是获取它的地方。

最后是如何退出信号处理程序的问题。 您不得调用 exit。这样做有两个问题:

  • exit 不是异步信号安全的。事实上,exit 可以通过在atexit 注册的处理程序运行任意代码。如果你想退出 处理,请致电_exit
  • 无论如何,退出进程都是一个坏主意,因为它会阻止 Apple 崩溃报告器运行或导致它记录 不正确的状态(信号处理程序的状态,而不是 崩溃线程的状态)。

更好的解决方案是取消注册您的信号处理程序(将其设置为 SIG_DFL) 然后返回。这将导致崩溃的进程 继续执行,再次崩溃,并通过 苹果崩溃记者。

1 虽然崩溃报告者捕捉到的常见信号不是 技术上异步信号(SIGABRT 除外),您仍然需要处理 它们作为异步信号,因为它们可以在任何时间出现在任何线程上 时间。

2 将此列表扩展到其他例程是合理的 在系统调用中实现为薄垫片。例如,我没有 对从信号处理程序调用 vm_read(见下文)感到不安。

[3] 交叉信号处理程序堆栈帧由 当内核在线程上运行信号处理程序时。由于没有 API 来了解这些框架的结构,没有办法 单独回溯这些框架之一。我很高兴去 进入细节,但它真的与这个讨论无关。如果 你有兴趣,开始一个新线程,我们可以在那里聊天。

阅读记忆

信号处理程序必须非常小心它所触及的内存, 因为该内存的内容可能已被 触发信号的崩溃。我的一般规则是 信号处理程序可以安全访问:

  • 它的代码
  • 它的堆栈
  • 它的论点
  • 不可变的全局状态

最后一点,我用 immutable 来表示 immutable after 启动。我认为设置一些全局状态是合理的 该过程在安装信号处理程序之前开始,然后 在您的信号处理程序中依赖它。

在安装信号处理程序后更改任何全局状态是 危险,如果你需要这样做,你必须小心确保 即使发生崩溃,您的信号处理程序也会看到一致的状态 可能会在您的更改进行到一半时发生。

请注意,您不能使用互斥锁来保护这个全局状态,因为 互斥锁不是异步信号安全的(即使它们是你 如果互斥锁被崩溃的线程持有,则死锁)。你 应该能够为此使用原子操作,但原子 众所周知,操作很难正确使用(如果我有一美元 每次我向开发人员指出他们正在使用原子 操作不正确,我会得到非常糟糕的报酬(-:但这仍然是 很多开发者!)。

如果您的信号处理程序读取其他内存,则必须注意避免 在阅读时崩溃。没有用于此的 BSD 级 API 1,所以我推荐你使用vm_read

1 执行此操作的传统 UNIX 方法是安装一个 信号处理程序以捕获由读取触发的任何内存异常, 但现在我们正在讨论信号处理程序中的信号处理和 太傻了。

写入文件

如果您想从信号处理程序中编写崩溃报告,您可以 必须使用低级 UNIX API(openwriteclose),因为只有 这些低级 API 被证明是异步信号安全的。你必须 还提前设置了路径,因为标准 API 用于 确定在哪里写入文件(例如NSFileManager)是 不是异步信号安全的。

离线符号

不要尝试从信号处理程序中进行符号化。相当, 在您的崩溃报告中写入足够的信息以支持离线 符号化。具体来说:

  • 要符号化的地址
  • 对于进程中的每个 Mach-O 图像:
    • 图片路径
    • 图像 UUID
    • 图片加载地址

您可以使用以下 API 获取大部分 Mach-O 图像信息 <mach-o/dyld.h>1。但是请注意,这些 API 不是 异步信号安全。您需要提前获取这些信息,并且 缓存它以供您的信号处理程序记录。

这很复杂,因为 Mach-O 图像列表可以 在处理加载和卸载代码时发生变化。这需要你 与您的信号处理程序共享可变状态,这正是我所做的 建议不要在阅读记忆中。

注意您可以使用_dyld_register_func_for_add_image了解图片加载和卸载 和_dyld_register_func_for_remove_image

1 我相信你需要解析 Mach-O 加载命令来获取 图片 UUID。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 2022-12-20
    • 2015-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多