可以创建自定义崩溃报告器,但绝对不建议这样做,因为背景中有很多事情很容易被遗忘,并且会引入很多未定义的行为。即使使用第三方框架也可能会很麻烦,但通常这是更好的方法。
我在原始问题中提到的方法将对 Apple 自己的崩溃报告器产生影响,并且由于信号处理不当而引入了未定义的行为。 UNIX 信号并未涵盖所有错误,API 处理与异步信号安全函数一起工作。 Apple 的崩溃报告器使用的 Mach 异常处理是更好的选择,但它更复杂。
在我们开始之前,我想让你看看我闪亮的新 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
崩溃记者。您的部分崩溃不会由以下原因引起
您的代码,但由于框架代码中的问题,以及写得不好
崩溃记者将扰乱苹果崩溃记者并使其
更难诊断这些问题。
此外,在处理真正难以调试的问题时,您
真的需要苹果崩溃中显示的更模糊的信息
报告。如果你破坏了这些信息,你最终会遇到难题
更难。
为避免这些问题,我建议您测试崩溃报告器的
对苹果崩溃记者的影响。基本思路是:
- 创建一个生成一组特定崩溃的程序。
- 运行每次崩溃。
- 验证您的崩溃报告器是否产生了合理的结果。
- 验证 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(open、write、close),因为只有
这些低级 API 被证明是异步信号安全的。你必须
还提前设置了路径,因为标准 API 用于
确定在哪里写入文件(例如NSFileManager)是
不是异步信号安全的。
离线符号
不要尝试从信号处理程序中进行符号化。相当,
在您的崩溃报告中写入足够的信息以支持离线
符号化。具体来说:
- 要符号化的地址
- 对于进程中的每个 Mach-O 图像:
您可以使用以下 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。