WeTest腾讯质量开发平台 了解和分析 iOS Crash

腾讯WeTest · 2018年09月15日 · 最后由 ola嘿嘿 回复于 2018年09月17日 · 3872 次阅读

作者:孟嵩,腾讯高级开发工程师
商业转载请联系腾讯 WeTest 获得授权,非商业转载请注明出处。
原文链接:http://wetest.qq.com/lab/view/404.html

WeTest 导读

北京时间凌晨一点,苹果一年一度的发布会如期而至。新机型的发布又会让适配相关的同学忙上一阵子啦,并且 iOS Crash 的问题始终伴随着移动开发者。本文将从三个阶段,由浅入深的介绍如何看懂并分析一篇 crash 报告,一起身临其境去读懂它吧。


翻译自苹果官方文档:Understanding and Analyzing Application Crash Reports

孟嵩:这篇万字长文,大概前后翻译了一个月,“写” 了三遍:第一遍是直译,第二遍是把直译改成程序员看着舒服的 “行话”,第三遍是把原文里说的过于抽象或者简单的部分加上我的注解(大家看见所有以孟嵩开头的部分)。

当 app 发生 crash 时会产生 crash report,这对我们定位 crash 的原因非常有帮助。该篇重点介绍了如何符号化、看懂并解析一篇 crash Report。

孟嵩:

开篇给出了这个文档的三个阶段,由浅入深为:

  1. 符号化,把不可读的文档转成可读

  2. 看懂,意思就是知道文档里哪个部分表达的什么

  3. 解析,意思就是能从文档中定位问题,获取解决问题的有价值的信息。

ps:文内展示代码均可左右滑动查看

介绍

当 app 发生 crash 时,系统会生成 crash report 并存储在设备上。crash report 会描述 app 在何种情况之下被系统终止运行,一般情况下描述会包括完整的线程调用堆栈,这对 app 的调试(和问题的定位)是非常有帮助的。所以你应当仔细研读这些 crash report,去了解你的 app 究竟发生的是哪种 crash,并尝试修复它们。

Crash Report,尤其是堆栈信息,在被符号化之前是不可读的。所谓符号化就是把内存地址用可读的函数名和行数来替换。如果你不是从设备直接获取的 crash 日志,而是通过 Xcode 的 Device Window(即通过视图操作而非手动命令行),它们会在几秒之后自动被符号化。当然你也可以把.crash 文件加入到 Xcode 的 Device Window 并自行将它符号化。

Low Memory Report 与其它 crash report 不同,它没有堆栈信息。当由于低内存而发生 crash 时,你必须反思你的内存使用模式和你针对低内存警告的应对方法。本文会提供给你几个内存管理的参考实现,供你参考。

获取 Crash Report 和 Low Memory Report

如何调试已经部署好的 iOS Apps 讨论了如何从一个 iOS 设备直接拿到 crash report 和 low memory report。
App 发布指南里的分析 Crash Reports 讨论了如何查看那些 crash report,这些 report 既包含通过 TestFlight 下载的测试用户处获得,又包含通过 App Store 下载的正式用户处获得。

符号化一篇 Crash report

符号化指的是一种手段,这种手段指的是把堆栈信息(二进制信息)解释成源码里的方法名或者函数名,也就是所谓符号。只有符号化成功后,crash report 才能帮助开发者定位问题。

注意:Low Memory Report 不需要被符号化(因为没有堆栈信息)。 注意:在 MacOS 平台上产生的 crash
report 在生成的时候一般都会被完全符号化过或者半符号化过。因此本节指的符号化针对的是从 iOS、watchOS 乃至 tvOS 中提取出来的 crash
report。整体处理流程上,macOS 的 carsh report 比较类似。

[ crash 上报和符号化过程概述 ]

  1. 编译器在把你的源代码转换成机器码的同时,也会生成一份对应的 Debug 符号表。Debug 符号表其实是一个映射表,它把每一个藏在编译好的 binary 信息中的机器指令映射到生成它们的每一行源代码中。通过 build setting 里的Debug Information Format(DEBUG_INFORMATION_FORMAT),这些 Debug 符号表要么被存储在编译好的 binary 信息中,要么单独存储在 Debug Symbol 文件中 (也就是 dSYM 文件):一般来说,debug 模式构建的 app 会把 Debug 符号表存储在编译好的 binary 信息中,而 release 模式构建的 app 会把 debug 符号表存储在 dSYM 文件中以节省体积。

在每一次的编译中,Debug 符号表和 app 的 binary 信息通过构建时的 UUID 相互关联。每次构建时都会生成新的唯一的能够标识那次构建的 UUID,即便你用同样的源代码,通过同样的编译 setting,UUID 也不会相同。相应的,dSYM 文件也不能用于解析其它(UUID 对应的)binary 信息,即便构建自于同一个源代码。

孟嵩:意思就是说,同一次构建,app+dSYM+UUID 是一套的。如果这几个文件不属于同一次构建,即便是相同的源代码,互相之间在符号化这个事情上也无法互相工作。

  1. 当你为了分发 app 而选择 Archive(存档)时,Xcode 会把 app 的二进制信息和.dYSM 文件存储在你的 home 文件夹下的某个地方。你可以在 Xcode 的 Organizer 里面通过” Archived” 选项找到所有你存档过的 app。 更多存档 app 的细节,请点击官方文档 - 分发你的 App 一文。

注意:想要解析来自于测试、app review 或者客户的 crash report,你需要保留分发出去的那些构建过的 archive 文件。

  1. 如果你是通过 App Store 分发 app 或者是 Test Flight 分发的 beta 版本的 app,你将在上传 archive 到 ITC(iTunes Connect)时看见一个 “是否将 dSYM 一起上传” 的选项。在上传对话框中,请勾选” 在 app 中包含 app 符号表”。上传你的 dYSM 文件对于从 TestFlight 用户和客户以及愿意分享诊断信息的客户那边接收 crash report 是很有必要的。更多详情请参考官方文档 - 分发你的 App 一文。

注意:接收自 App Review 的 crash
report 是不会被符号化的,及时你再上传你的 app 到 ITC 时勾选了包含 dSYM 文件。任何来自于 App Review 的 crash
report 都需要在 Xcode 里做符号化。

  1. 当你的 app 发生 crash 时,一个没有被符号化的 crash report 会被创建并存储在设备上。

  2. 用户可以通过调试已部署的 iOS APP 里提到的方法来直接从他们的设备里获得 crash report。如果你通过 AdHoc 或者企业证书分发 app,这是你唯一能从用户获取 crash report 的方法。

  3. 从设备上直接获取的 crash report 是没有被符号化的,你需要通过 Xcode 来符号化。Xcode 会结合 dSYM 文件和你 app 的二进制信息把堆栈里的每一个地址对应到源代码中。处理后的结果就是一个符号化过的 crash report。

  4. 如果用户愿意和 Apple 共享诊断信息,或者用户通过 TestFlight 下载了你的 beta 版本 app,那 crash report 会被上传到 App Store。

  5. App Store 在符号化 crash report 后会把内部所有的 crash reports 做汇总并分组,这种聚合(相似 crash report)的方法叫做 crash 聚类。

  6. 这些符号化后的 crash report 可以在你的 Xcode 的 Crash Organizer 中进行查看。

Bitcode

Bitcode(位编码)是一个编译好的项目的中间表现形式。当你在允许 bitcode 的前提下 Archive 一个 app 时,编译器会在二进制中包含 bitcode 而不是机器码。一旦 binary 信息被上传到 App Store 中,bitcode 会被再次编译成机器码。也许 App Store 会在将来二次编译 bitcode,例如为提高编译器性能而二次编译等。不过这不重要,因为一切对你来说是透明的,也就不需要你来额外付出什么。

[ 图 2 BitCode 编译过程概览 ]

因为你的 binary 信息的最终编译结果是在 App Store 上体现的,因此你的 Mac 将不会包含那些需要对从 App Review 或者用户的设备那里获取到的 Crash report 所必须的符号化用的 dSYM。

孟嵩:这里原文很拗口,大概意思就是需要的东西都在 App Store 云端,之后的操作会自动进行,见下文。

虽然当你 Archive 你的 app 时会创建 dSYM 文件,但它们只能用在 bitcode binary 信息中,并不能用于符号化 crash report。 App Store 允许你从 Xcode 或者 ITC 网站中下载这些随着 bitcode 编译而产生的 dSYM 文件。 为了解析从 App Review 或者给你发送 crash report 的用户的 crash report,你必须要下载这些 dSYM 文件,这样才能符号化 crash report。 如果是从 crash reporting service 那里接收 crash report,符号化会自动完成。

注意:App Store 上编译的 binary 信息和提交的原始文件的 UUID 是不同的。

从 Xcode 下载 dSYM 文件

· 在 Archives organizer,选择你之前提交到 App Store 的 Archive 文件

· 选择 Download dSYM 按钮 Archive

Xcode 会下载 dSYM 文件并且把他们插入到选择的 Archive 中。

从 ITC 网站上下载 dSYM 文件

· 打开 App 详情页面

· 点击 Activity

· 从所有的构建中,选择一个版本

· 点击 下载 dSYM 文件的链接

把” 隐藏的” 符号名还原成原始名

当你把一个带有 bitcode 的 app 上传到 App Store 时,你也许在提交对话框中并没有勾选 “上传你的 app 的符号表信息以便从 Apple 那边接收符号化过的 report” 的选项。 当你选择不发送符号表信息给 Apple 时,Xcode 会在你发送 app 到 ITC 之前用晦涩难懂的符号例如”_hidden#109” 等来替换你的 app 里的 dSYM 文件。Xcode 会创建一个原始符号和” 隐藏” 符号的对照表,并且将其存储在 Archive 的 app 文件中的一个 bcsymbolmap 文件里。每一个 dSYM 文件都会有一个对应的 bcsymbolmap 文件。

在符号化 crash report 之前,你需要把那些从 ITC 中下载下来的 dSYM 文件中的晦涩信息给解析一下。 如果你使用 Xcode 中的下载 dSYM 按钮,这步解析会自动完成。但是,如果你通过 ITC 网站来下载 dSYM 的话,你需要打开 Terminal 并且手动输入下面的命令来做解析(把 example 的 path 信息和 sSYM 信息替换一下)

xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/2017-11-23/MyGreatApp\ 11-23-17\,\ 12.00\ PM.xcarchive/BCSymbolMaps ~/Downloads/dSYMs/3B15C133-88AA-35B0-B8BA-84AF76826CE0.dSYM

针对每一个 dSYMs 文件夹下的 dSYM 文件都运行一次这条命令。

如何判断 Crash report 是否已经符号化

一个 crash report 有可能未符号化,完全符号化,也有可能部分符号化。未符号化的 crash report 不会在堆栈信息中包含方法名或者函数名。相反,你会在加载好的 binary 信息中发现可执行的 16 进制地址信息。在完全符号化的 crash report 里,堆栈中的每一行 16 进制地址信息都会被替换成对应的符号。在部分符号化的 crash report 中,只有一部分堆栈信息被替换成相应的符号信息。

显然,你应当尽力去完全符号化你的 crash report,因为那样你才能够获得 crash report 里最有价值的信息。一个部分符号化的 crash report 也许包含了可以理解 crash 的信息,这取决于 crash 的类型和哪一部分被成功符号化了。一个未符号化的 crash report 用处有限。

[ 相同堆栈信息下的不同程度的符号化 ]

用 Xcode 符号化 iOS 的 Crash report

一般来说,Xcode 会自动尝试符号化它所有的 Crash report。所以你只需要把 crash report 加到 Xcode Organizer 就可以了。

Note:Xcode 只认.crash 后缀的 crash report。如果你收到的 crash report 没有后缀名或者后缀是 txt,在执行下列步骤之前先把它改成.crash。

· 把 iOS 设备连接到你的 Mac

· 从 Window 菜单栏选择 Devices

· 在 Devices 左侧,选择一个设备

· 点击右边在 “Device Information“ 下面的 ” View Device Logs” 按钮

· 把你的 Crash report 拖拽到左侧 panel 中

· Xcode 会自动符号化 Crash report 并且显示结果

为了符号化一个 Crash report,Xcode 需要去定位如下信息:

· 崩溃的 app 的 binary 信息以及 dSYM 文件

· 所有 app 关联的自定义 framework 的 binary 信息以及 dSYM 文件。如果是从 app 构建出来的 framework,它们的 dYSM 会随着 app 的 dSYM 文件一起拷贝到 archive 中。如果是第三方的 framework,你需要去找作者要 dYSM 文件。

· 发生 crash 时 app 所依赖的 OS 的符号表信息。这些符号表包含了特定 OS 版本

(例如 iOS9.3.3)上的 framework 所需调试信息。 OS 符号表的架构具有独特性——一个 64 位的 iOS 设备不会包含 armv7 的符号表。Xcode 将要自动拷贝你连接到的特定版本的 Mac 的符号表。

在上述任何一处,如果没有 Xcode,你将无法符号化一个 crash report,或者只能部分符号化一个 crash report。

用 atos 符号化 Crash report

atos 命令可以把地址里的数字替换成等价的符号。如果调试符号信息是完备的,则 atos 的输出信息将会包含文件名和对应的资源行数。atos 命令可以被用来单独符号化那些未符号化或者部分符号化过的 crash report(中的堆栈信息里的地址)。

想要使用 atos 符号化 crash report 可以按如下方式操作:

  1. 找到你想要符号化的那一行,记下第二列的 binary 信息名,以及第三列的地址。

  2. 从 crash report 底部的 binary 信息名列表中找到那个名字,记下来架构名和加载的地址。

孟嵩:例如在下图里,我们想符号化的部分就是 0x00000001000effdc,binary 信息名是 The
Elements,底部能找到对应的名字的架构名称是 arm64,加载地址是 0x1000e4000。

[ 在 Crash report 里提取出使用 atos 所需要的信息 ]

  1. 定位二进制对应的 dSYM 文件。你可以用 Splotlight,结合 UUID,来寻找匹配的 dSYM 文件。(请查看相关章节。)dSYM 是一个 bundle,包含通过编译器在 build 时编译出来的 DWARF 调试信息(nimo: DWARF 的可能的解释是,Debugging With Attributed Record Formats,是一种调试文件结构标准,结构相当的复杂)。你在使用 atos 时必须提供这个文件的路径,而不是 dSYM 的 bundle 路径。

  2. 有了上述信息之后,你就可以把堆栈里的地址通过 atos 命令来符号化了。你可以符号化多条地址,通过空格来进行区分。

atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>

清单 1 使用 atos 命令的样例,以及结果输出

$ atos -arch arm64 -o TheElements.app.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
-[AtomicElementViewController myTransitionDidStop:finished:context:]

利用符号化排查问题

如果 Xcode 没有完全符号化一个 crash report,很可能是你的 Mac 丢失了 app binary 信息对应的 dSYM 文件,或者是丢了一个或多个 app 关联的 framework 的 dSYM 文件,也有可能在发生 Crash 时 OS 层面的 app 的设备符号表丢失了。下列步骤显示了如何使用 Spotlight 来判断那些可以符号化对应堆栈地址信息的 dSYM 文件是否在你的 Mac 上。

[ 定位一个二进制镜像 ]

  1. 在 Xcode 无法符号化的堆栈里找一行,注意第二列的 binary 信息的名字。

  2. 在 crash report 的底部中的二进制信息列表里找到那个名字。这个列表包含了每一个 crash 事故现场存在于进程里的二进制信息的 UUID。

孟嵩:本例中需要关注的 binary 信息的名字是 The
Element,在底部列表中对应的二进制信息的 UUID 是 77b672e2b9f53b0f95adbc4f68cb80d6

列表 2 你可以用 grep 命令来快速找到二进制信息的列表信息

$ grep --after-context=1000 "Binary Images:" <Path to Crash Report> | grep <Binary Name>
  1. 把二进制信息的 UUID 按照 8-4-4-4-12 格式 (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) 转换成 32 个字符组成的字符串。注意所有字母必须大写。

  2. 用 mdfind 命令,结合” com_apple_xcode_dsym_uuids == ”(包含引号)来查找 UUID 信息。

列表 3 使用 mdfind 命令来通过给定 UUID 查找 dSYM 文件。

$ mdfind "com_apple_xcode_dsym_uuids == <UUID>"
  1. 如果 spotlight 找到了 UUID 对应的 dSYM 文件,mdfind 会把 dSYM 文件和可能包含的归档文件的路径打印出来。如果一个 UUID 对应的 dSYM 文件没有找到,mdfind 会直接退出。

如果 spotlight 找到了二进制对应的 dSYM 文件,但是 Xcode 没有能结合二进制信息成功把地址符号化,那你应该上报一枚 bug 并且把 crash report 和对应的 dSYM 文件一起附到 bug report 中。作为权宜之策,你可以手动用 atos 来对地址进行符号化。

如果 spotlight 没有找到二进制信息对应的 dSYM 文件,确保你还有 app 发生 crash 的那个版本的 Xcode 归档文件,并且这个文件存在于 spotlight 可以找到的某个地方。如果你的 app 是支持 bitcode 方式构建的,确保你已经从 App Store 下载了最终编译版本的 dSYM 文件。

如果你觉得你已经有了二进制信息对应的正确的 dSYM 文件,那你可以用 dwarfdump 命令来打印对应的匹配 UUID。你也可以用用 dwarfdump 命令来打印二进制的 UUID。

xcrun dwarfdump --uuid

注意:你必须保存你最开始上传到 App Store 的发生 crash 的 app 的归档文件。dSYM 文件和 app 二进制文件是一一对应,且每次构建都不相同。即便通过相同的源码和配置,再执行一次构建,生成的 dSYM 文件也无法和之前的 crash report 做符号化匹配。

如果你不在存有这个归档文件,你应该重新提交一次有归档的新版本,以确保再发生 crash 的时候你可以符号化 crash report。

分析 Crash report

这段将会讨论一篇标准 crash report 的各章节的含义。

每一篇 crash report 都有一个 header。

列表 4 一篇 crash report 的 header 部分

Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C

CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc

Hardware Model: iPad6,8Process: TheElements [303]Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElementsIdentifier: com.example.apple-samplecode.TheElementsVersion: 1.12

Code Type: ARM-64 (Native)Role: Foreground

Parent Process: launchd [1]Coalition: com.example.apple-samplecode.TheElements [402]



Date/Time: 2016-08-22 10:43:07.5806 -0700

Launch Time: 2016-08-22 10:43:01.0293 -0700

OS Version: iPhone OS 10.0 (14A5345a)

Report Version: 104

大部分字段的含义是不言自明的,但是有一些值得特别指出:

· Incident Identifier: 一个 crash report 的唯一 ID。两个 report 不会使用同一个 Incident Identifier。

· CrashReporter Key: 一个匿名的设备相关 ID。同一个设备的两篇 crash report 会有相同的 CrashReporter Key。

· Beta Identifier:一个整合了发生 crash app 的设备和供应商信息的 ID。来自同一个供应商和设备的两篇 report 会包含相同的 ID 值。这个字段只有当 app 通过 TestFlight 分发的时候出现,并且出现在应该出现 Crash Reporter Key Field 的地方。

· Process:发生 Crash 时的进程名。这个和 app 信息属性列表里的 CFBundleExecutable Key 中的值可以匹配上。

· Version:发生 crash 的版本号。这个值可以关联到发生 crash 的 app 的 CFBundleVersion 和 CFBundleVersionString 上。

· Code Type:发生 crash 的上下文所在架构环境。有 ARM-64,ARM,X86-64 和 X86.

· Role:在发生 crash 时进程的的 task_role。

孟嵩:task_role 的定义如下:

enum task_role {

   TASK_RENICED = -1,

  TASK_UNSPECIFIED = 0,

  TASK_FOREGROUND_APPLICATION,

  TASK_BACKGROUND_APPLICATION,

  TASK_CONTROL_APPLICATION,

  TASK_GRAPHICS_SERVER,

  TASK_THROTTLE_APPLICATION,

  TASK_NONUI_APPLICATION,

  TASK_DEFAULT_APPLICATION

};

· OS Version: OS version,包含发生 crash 时的所属 app 的编译码。

异常信息

遇到 Objective-C/C++ 时不要懵(即便有些会导致 Crash)。这章列出了 Mach 异常类型和相应的能提供 crash 的蛛丝马迹的一些字段信息。当然,不是所有字段都会出现在每一篇 crash report 里。

列表 5 由于 uncaught Objective-C exception 而导致的进程被停止的 crash report 的摘录

Exception Type: EXC_CRASH (SIGABRT)

Exception Codes: 0x0000000000000000, 0x0000000000000000

Exception Note: EXC_CORPSE_NOTIFY

Triggered by Thread: 0

列表 6 由于反向引用了一个 NULL 指针而造成进程被终止的 crash report 的摘录

Exception Type: EXC_BAD_ACCESS (SIGSEGV)

Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000

Termination Signal: Segmentation fault: 11

Termination Reason: Namespace SIGNAL, Code 0xb

Terminating Process: exc handler [0]

Triggered by Thread: 0

可能出现在这一章节的某些字段解读如下。

· Exception Codes: 和异常是有关的处理器指定信息,这些信息会被编码成一个或者多个 64 位二进制数字。一般来说,这个字段不应该存在,因为 crash report 生成时会把 exception code 转化成可读的信息并在其它字段进行体现。

· Exception Subtype:可读的 exception code 的名称。

· Exception Message:从 exception code 中解析出来的附加的可读信息。

· Exception Note:不特指某一种异常的额外信息。如果这个字段包含” SIMULATED”(不是 Crash),则进程并没有发生 crash,而是在系统层面被 kill 掉了,比如看门狗机制。

孟嵩:为了防止一个应用占用过多的系统资源,苹果工程师门设计了一个 “看门狗” 的机制。“看门狗” 会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗” 就会强制终结这个应用的进程。
开发者们在 crashlog 里面,会看到诸如 0x8badf00d 这样的错误代码 (看起来很像 bad food,看门狗吃到了坏的食物,不嗨森)。
看门狗触发条件如下:

[ 看门狗触发时机 ]

· Termination Reason:当进程被终止时的原因及信息。关键的信息模块,不论是进程内还是进程外,当遇到一个致命错误(fatal error,例如 bad code signature,缺失依赖库,不恰当的访问私有敏感信息等)。MacOS Sierra,iOS 10, watch OS3 和 tvOS 10 已经采用新的架构去记录这些错误信息,所以这些系统之下的 crash report 会在 Termination Reason 这个字段里描述 error message 信息。

· Triggered by Thread:指出异常是在哪个线程发生的

接下来的章节会解释常见的异常类型:

Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]

进程试图去访问无效的内存空间,或者尝试访问的方法是不被允许的(例如给只读的内存空间做写操作)。在 Exception Subtype 字段中如果出现 kern_return_t 的话,说明内存地址空间被不正确的访问了。

这里有几个调试 bad memory crash 的小贴士:

· 如果 objc_msgSend 或者 objc_release 出现在 crash 的线程的附近,则进程有可能尝试去给一个被释放的对象发送消息。你应当用 Zombie instrument 方式来运行 profile,来更好地了解发生 crash 的原因。

· 如果 gpus_ReturnNotPermittedKillClient 在近 crash 的线程附近,则进程有可能是尝试在后台通过 OpenGL ES 或者 Metal 来做渲染。可以参见 QA1766: How to fix OpenGL ES application crashes when moving to the background

· 通过在运行你的 app 时勾选 Address Sanitizer。address sanitizer 会在编译期间在内存访问时添加额外的操作,当你的 app 运行,Xcode 会在内存可能发生 crash 的时候给出提示信息。

Abnormal Exit [EXC_CRASH // SIGABRT]

进程异常退出。这种异常最常见的原因在于 uncaught Objective-C/C++ exception 并且调用了 abort()。

扩展 App(nimo:App Extensions,例如输入法) 如果花了太多时间做初始化的话就会以这种异常退出(看门狗机制)。如果扩展程序由于在启动时挂起进而被 kill 掉,那 report 中的 Exception Subtype 字段会写 LAUNCH_HANG。因为扩展 App 没有 main 函数,所以任何情况下的在 static constructors 和 +load 方法里的初始化时间都会体现在你的扩展或者依赖库中。因此你应当尽可能的推迟这些逻辑。

Trace Trap [EXC_BREAKPOINT // SIGTRAP]

和 Abnormal Exit 类似,这种异常是由于在特殊的节点加入 debugger 调试节点的原因。你可以在你自己的代码里通过使用__builtin_trap() 函数来触发这个异常。如果没有 debugger 存在,则线程会被终止并生成一个 crash report。

底层库(例如 libdispatch)会在遇到 fatal 错误的时候陷入这个困局。关于错误的相关信息会在 crash report 的章节或者是设备的的打印信息里找到。
Swift 代码会在运行时的时候遇到下述问题时抛出这种异常:

· 一个 non-optional 的类型被赋予一个 nil 值

· 一个失败的强制转换

遇到这种错误,查下堆栈信息并想清楚是在哪里遇到了未知情况 (unexpected condition)。额外信息也可能会在设备的控制台的日志里出现。你应当尽量修改你的代码,去优雅的处理这种运行时错误。例如,处理一个 optional 的值,通过可选绑定 (Optional binding) 而不是强制解包来获得其值。

孟嵩:可选绑定,就是类似如下语句的使用

``` if let actualValue = maybeHasValue(){

foo(actualValue)

} ```

Illegal Instruction [EXC_BAD_INSTRUCTION // SIGILL]

当尝试去执行一个非法或者未定义的指令时会触发该异常。有可能是因为线程在一个配置错误的函数指针的误导下尝试 jump 到一个无效地址。
在 Intel 处理器上,ud2 操作码会导致一个 EXC_BAD_INSTRUCTIONY 异常,但是这个通常用来做调试用途。在 Intel 处理器上,Swift 会在运行时碰到未知情况时被停止。
详情参考 Trace Trap。

Quit [SIGQUIT]

这个异常是由于其它进程拥有高优先级且可以管理本进程(因此被高优先级进程 Kill 掉)所导致。SIGQUIT 不代表进程发生 Crash 了,但是它确实反映了某种不合理的行为。

iOS 中,如果占用了太长时间,键盘扩展程序会随着宿主 app 被干掉。因此,这种情况的异常下不太可能会在 Crash report 中出现合理可读的异常代码。大概率是因为一些其它代码在启动时占用了太长时间但是在总时间限制前(看门狗的时间限制,见上文中的表格)成功结束了,但是执行逻辑在 extension 退出的时候被错误的执行了。你应该运行 Profile,仔细分析一下 extension 的各部分消耗时间,把耗时较多的逻辑放到 background 或者推迟(推迟到 extension 加载完毕)。

Killed[SIGKILL]

进程收到系统指令被干掉。请自行查看 Termination Reason 来定位线程被干掉的原因。

Termination Reason 字段会包含一个命名空间和代码。以下代码只针对 watchOS:

· 代码 0xc51bad01 表示 watchOS 在后台任务占用了过多的 cpu 时间而导致 watch app 被干掉。想要解决这个问题,优化后台任务,提高 CPU 执行效率,或者减少后台的任务运行数量。

· 代码 0xc51bad02 表示在后台的规定时间内没有完成指定的后台任务而导致 watch app 被干掉。想要解决这个问题,需要当 app 在后台运行时减少 app 的处理任务。

· 代码 0xc51bad03 表示 watch app 没有在规定时间内完成后台任务,且系统一直非常忙以至于 app 无法获取足够的 CPU 时间来完成后台任务。虽然一个 app 可以通过减少自身在后台的运行任务来避免这个问题,但是 0xc51bad03 这个错误把矛头指向了过高的系统负载,而非 app 本身有什么问题。

Guarded Resource Violation [EXC_GUARD]

进程违规访问了一个被保护的资源。系统库会把特定的文件描述符标记为被被保护,因此任何对这些文件描述符的常规操作都会抛出 EXC_GUARD 异常(nimo: 当系统想操作这些文件描述符时,它们会用特殊的被授权过的私有 API)。所以遇到诸如私自关闭掉系统打开的文件描述符之类的操作时您可以快速察觉。例如,如果一个 app 关闭掉了曾经支持 Core Data 存储的 SQLite 文件的文件描述符,你会发现 Core Data 过一会儿神秘 crash。guard exception 会在不久之后注意到并且让他们更容易被 debug。

更新版本的 iOS crash report 会在 Exception Subtype 和 Exception Message 字段里包含关于 EXC_GUARD 异常的可读详细信息。在 macOS 或者是更老版本的 iOS 的 crash report 中,这条信息会被加密成第一个 Exception Code 并以位信息进行呈现,它可以被这么解读:

· [63:61] - Guard Type:被保护的资源的类型。0x2 值表示资源是一个文件描述符。

· [60:32] - Flavor:在何种情况之下出现的问题。

如果第一个 (1<<0) bit 被设值,则进程尝试在一个被保护的文件描述符上调用 close()

如果第二个 (1<<1) bit 被设值,则进程尝试在被保护的文件描述符上用 F_DUPFD 或 F_DUPFD_CLOEXEC 调用 dup(), dup2(), 或 fcntl() 命令。

如果第三个 (1<<2) bit 被设值,则进程尝试通过 socket 发送一个被保护的文件描述符。

如果第五个 (1<<4) bit 被设值,则进程尝试写一个被保护的文件描述符。

· [31:0] - File Descriptor:进程尝试修改被保护的文件描述符。

Resource Limit [EXC_RESOURCE]

进程的资源超过限定阈值。这条推送是 OS 发出的,表示进程占有了太多的资源。准确的资源列在了 Exception Subtype 的字段里。如果 Exception Note 字段包含了 NON-FATAL CONDITION(非严重错误),则即便是生成了 crash report,进程也不会被 kill 掉。

· 如果 EXCEPTION SUBTYPE 里出现 MEMORY 则暗示了进程占用已经超过系统限制。如果之后出现由于系统占用过多进程被 Kill,可能和这有关。

· 如果 EXCEPTION SUBTYPE 里出现 WAKEUP 则暗示线程每秒被进程唤醒太多次了,进而导致 CPU 被频繁唤醒并且造成电量损耗。
通常,这种事发生在进程间通信(通过 peformSelector:onThread:或者 dispatch_async),而且会远比预想的发生的更频繁。因为发生这种异常的通信被触发的如此频繁,所以很多后台进程会出现彼此高度雷同的堆栈信息——恰恰暗示了它们是从哪儿来的。

Other Exception Types

有些 crash report 可能会出现无名的 Exception Type,这时候在这个字段上会出现纯 16 进制值(例如 00000020)。如果你收到这样的 crash report,直接去 Exception Code 查看更多信息。

· 如果 Exception Code 是 0xbaaaaaad 则说明此条 logs 是系统堆栈快照,并非 crash report。可以通过同时按(手机)侧边按钮和音量键来记录堆栈快照。通常情况下,这些 logs 是用户无意中生成的,并非表示错误。

· 如果 Exception Code 是 0xbad22222 表示一个 VoIP 应用因为频繁暂停被 iOS 系统终止掉。

· 如果 Exception Code 是 0x8badf00d(读起来像 badfood)则说明一个应用因为触发了看门狗机制被 iOS 系统终止掉,有可能是应用花了太长时间启动,终止,或者是响应系统事件。一种常见原因是在主线程上做网络同步逻辑。不论 Thread0 上(也就是主线程)想做什么(重要的事),都应该转移到后台线程,或者换一种方式触发,这样它才不会阻塞主线程。

· 如果 Exception Code 是 0xc00010ff 则说明 app 因为环境过热(的事件)被 iOS 系统干掉了。这个也许是和发生 crash 的特定设备有关,或者是和它所在的环境有关。如果想知道更多高效运行 app 的 tips,请参考 WWDC 的文章: iOS Performance and Power Optimization with Instruments。

· 如果 Exception Code 是 0xdead10cc(读起来像 deadlock) 则说明一个应用被系统终止掉,原因是在应用挂起时拿到了文件锁或者 sqlite 数据库所长期不释放直到被冻结。如果你的 app 在挂起时拿到了文件锁或者 sqlite 数据库锁,它必须请求额外的后台执行时间 (request additional background execution time ) 并在被挂起前完成解锁操作。

· 如果 Exception Code 是 0x2bad45ec 则说明 app 因为违规操作(安全违规)被 iOS 系统终止。终止描述会写:“进程被查到在安全模式进行非安全操作”,暗示 app 尝试在禁止屏幕绘制的时候绘制屏幕,例如当屏幕锁定时。用户可能会忽略这种异常,尤其当屏幕是关闭的或者当这种终止发生时正好锁屏。

Note:通过 App Switcher(就是双击 home 键出现的那个界面) 并不会生成 crash
report。一旦 app 进入挂起状态,被 iOS 在任何时间终止掉都是合理的,因此这时候不会生成 crash report。

额外的诊断信息

本章节包含终止相关的额外诊断信息,包括:

· 应用的具体信息:在进程被终止前捕捉到的框架错误信息

· 内核信息:关于代码签名问题的细节

· Dyld(动态链接库)错误信息:被动态链接器提交的错误信息

从 macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10 开始,大部分这种信息都在 Exception Information 的 Termination Reason 字段下了。
你应当阅读本章节来更好的明白当进程被终止的时候发生了什么。

表 7:一段因为找不到链接库而导致进程被终止的 crash report 的摘录

Dyld Error Message:

Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework

 Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements  
 Reason: no suitable image found.

表 8:一段因为没能快速加载初始 view controller 而导致进程被终止的 crash report 的摘录

Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU

堆栈信息

一个 crash report 最有意思的部分一定是每个线程在被终止时的堆栈信息。这些信息和你在 debug 时看到的很类似。

列表 9:一个完全符号化的 crash report 的堆栈部分摘录

Thread 0 name: Dispatch queue: com.apple.main-thread

Thread 0 Crashed:

0   TheElements                 0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)

1   UIKit                     0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312

2   UIKit                     0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160

3   QuartzCore                  0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260

4   libdispatch.dylib             0x000000018dd6d1c0 _dispatch_client_callout + 16

5   libdispatch.dylib             0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000

6   CoreFoundation                0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12

7   CoreFoundation                0x000000018ee8fb18 __CFRunLoopRun + 1660

8   CoreFoundation                0x000000018edbe048 CFRunLoopRunSpecific + 444

9   GraphicsServices               0x000000019083f198 GSEventRunModal + 180

10  UIKit                      0x0000000194d21bd0 -[UIApplication _run] + 684

11  UIKit                      0x0000000194d1c908 UIApplicationMain + 208

12  TheElements                  0x00000001000653c0 main (main.m:55)

13  libdyld.dylib                 0x000000018dda05b8 start + 4



Thread 1:

0   libsystem_kernel.dylib           0x000000018deb2a88 __workq_kernreturn + 8

1   libsystem_pthread.dylib           0x000000018df75188 _pthread_wqthread + 968

2   libsystem_pthread.dylib           0x000000018df74db4 start_wqthread + 4


...

第一行列出了当前的线程号,以及当前的执行队列的 id。其余各行列出来每一个堆栈中堆栈片段信息,从左到右分别是:

· 堆栈片段号。堆栈的展示顺序会和调用顺序一致,片段 0 是在程序被终止时执行的函数。片段 1 是调用片段 0 的函数,以此类推。

· 在堆栈片段中驻留的执行函数的名称

· 片段 0 代表机器指令在被终止的生活所在的地址。其它片段表示如果片段 0 执行完成之后下一个执行的片段地址

· 在一个符号化的 crash report 中,代表在堆栈片段中的函数名称

异常

Objective-C 中的异常通常用来表明在运行时发生的代码错误,例如越界访问数组,或者更改 immutable 的对象,没有实现 protocol 中必须实现的方法,或者给接收者无法识别的对象发送信息。

Note:给之前已经释放的对象发送消息会引发 NSInvalidArgumentException 异常进而 crash,而非内存访问违规。这会在新的变量正好占据了之前释放变量所在内存时。如果你的 app 因为 NSInvalidArgumentException 发生 crash(在堆栈信息中查看 [NSObject(NSObject)
doesNotRecognizeSelector:]),考虑通过 Zombies instrument
来 profiling 你的应用,来排除刚才提到的内存管理问题。

如果异常没有被捕捉到,他会被一个叫 uncaught exception 方法所拦截。默认的 uncaught exception 的日志会显示到设备的控制台,之后会终止进程。异常堆栈信息会在生成的 crash report 的上一个异常堆栈(Last Exception Backtrace)下,就像列表 10 所写。异常消息会被 crash report 忽略。如果你收到了一个带有上一个异常堆栈(Last Exception Backtrace)的 crash report,你应当去获取原始设备并获取其控制台日志信息,来更好的了解发生 crash 的原因。

List10:发生了上一个异常堆栈(Last Exception Backtrace)的未符号化 crash report 摘录

Last Exception Backtrace:

(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8)

一个只包含 16 进制信息的有 Last Exception Backtrace 信息的 crash 日志必须被符号化,以获取有价值的堆栈信息,就像列表 11 所写。

列表 11:一个包含 Last Exception Backtrace 信息的符号化的 crash report。这个异常出现在加载 app 的 storyboard 时,需要响应的 IBOutlet 的对应元素丢失了。

Last Exception Backtrace:

0   CoreFoundation                    0x18eee41c0 __exceptionPreprocess + 124

1   libobjc.A.dylib                   0x18d91c55c objc_exception_throw + 56

2   CoreFoundation                    0x18eee3e88 -[NSException raise] + 12

3   Foundation                       0x18f8ea1a0 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 272

4   UIKit                          0x195013fe4 -[UIViewController setValue:forKey:] + 104

5   UIKit                          0x1951acf20 -[UIRuntimeOutletConnection connect] + 124

6   CoreFoundation                    0x18ee03dc4 -[NSArray makeObjectsPerformSelector:] + 232

7   UIKit                          0x1951ab8f4 -[UINib instantiateWithOwner:options:] + 1756

8   UIKit                        0x195458128 -[UIStoryboard instantiateViewControllerWithIdentifier:] + 196

9   UIKit                          0x19545fa20 -[UIStoryboardSegueTemplate instantiateOrFindDestinationViewControllerWithSender:] + 92

10  UIKit                           0x19545fc7c -[UIStoryboardSegueTemplate _perform:] + 56

11  UIKit                           0x19545ff70 -[UIStoryboardSegueTemplate perform:] + 160

12  UIKit                           0x194de4594 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1352

13  UIKit                           0x194e94e8c -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 268

14  UIKit                           0x194f47d8c _runAfterCACommitDeferredBlocks + 292

15  UIKit                           0x194f39b40 _cleanUpAfterCAFlushAndRunDeferredBlocks + 560

16  UIKit                           0x194ca92ac _afterCACommitHandler + 168

17  CoreFoundation                    0x18ee917dc __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32

18  CoreFoundation                    0x18ee8f40c __CFRunLoopDoObservers + 372

19  CoreFoundation                    0x18ee8f89c __CFRunLoopRun + 1024

20  CoreFoundation                    0x18edbe048 CFRunLoopRunSpecific + 444

21  GraphicsServices                   0x19083f198 GSEventRunModal + 180

22  UIKit                           0x194d21bd0 -[UIApplication _run] + 684

23  UIKit                           0x194d1c908 UIApplicationMain + 208

24  TheElements                       0x1000ad45c main (main.m:55)

如果你发现本应该被捕捉的异常并没有被捕捉到,请确定您没有在 building 应用或者 library 时添加了-no_compact_unwind 标签。

64 位 IOS 用了 zero-cost 的异常实现机制。在 zero-cost 系统里,每一个函数都有一个额外的数据,它会描述如果一个异常在跨函数范围内实现,该如何展开相应的堆栈信息。如果一个异常发生在多个堆栈但是没有可展开的数据,那么异常处理函数自然无法跟踪并记录。也许在堆栈很上层的地方有异常处理函数,但是如果那里没有一个片段的可展开信息,没办法从发生异常的地方到那里。指定了-no_compact_unwind 标签表明你那些代码没有可展开信息,所以你不能跨越函数抛出异常(也就是说无法通过别的函数捕捉当前函数的异常)。

Thread State(线程状态)

这章列出了 crash 线程的线程状态。这里列出了注册过的值。在你读一个 crash report 的时候,了解线程状态并非必须,但是如果你想更好地了解 crash 的细节,这也许会起一些帮助。

列表 12:ARM64 设备的 crash report 的一段 Thread State 摘录

Thread 0 crashed with ARM Thread State (64-bit):



x0: 0x0000000000000000   x1: 0x000000019ff776c8   x2: 0x0000000000000000   x3: 0x000000019ff776c8    

x4: 0x0000000000000000   x5: 0x0000000000000001   x6: 0x0000000000000000   x7: 0x00000000000000d0    

x8: 0x0000000100023920   x9: 0x0000000000000000  x10: 0x000000019ff7dff0  x11: 0x0000000c0000000f   

x12: 0x000000013e63b4d0  x13: 0x000001a19ff75009  x14: 0x0000000000000000  x15: 0x0000000000000000   

x16: 0x0000000187b3f1b9  x17: 0x0000000181ed488c  x18: 0x0000000000000000  x19: 0x000000013e544780   

x20: 0x000000013fa49560  x21: 0x0000000000000001  x22: 0x000000013fc05f90  x23: 0x000000010001e069   

x24: 0x0000000000000000  x25: 0x000000019ff776c8  x26: 0xee009ec07c8c24c7  x27: 0x0000000000000020   

x28: 0x0000000000000000  fp: 0x000000016fdf29e0   lr: 0x0000000100017cf8    

sp: 0x000000016fdf2980   pc: 0x0000000100017d14 cpsr: 0x60000000

Binary Images

这一章列出了在进程被终止时加载在进程中的二进制文件(binary images)。

列表 13:一段 crash report 的完整二进制文件摘录

Binary Images:


0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements


...

每一行都包含了一个二进制文件的以下细节信息:

· 在进程内的二进制文件的地址空间

· 一段二进制的名称或者 bundle id(仅针对 macOS)。一个 MacOS 的 crash report,如果二进制时 OS 的一部分,会在前面加上 a。

·(仅针对 macOS)二进制的短版本 (short version) 和 bundle 版本,通过破折号来分割。

·(仅针对 iOS)二进制文件的架构名。一个二进制可能包含多个分片,每一个架构它都支持。其中只有一个可以被加载到进程中。

· 一个可以唯一标示二进制文件的 id,即 UUID。这个值会随每一次构建而发生变化,并且它会用来定位需要符号化时的 dSYM 文件。

· 磁盘上二进制文件的 path。

读懂低内存 report(Low Memory Reports)

当系统检测到内存不足时,iOS 系统里的虚拟内存系统会协同各应用来做内存释放。每个运行着的应用都会接收到系统发来低内存推送(Low-memory notification),要求释放内存空间,从而达到减少整体内存消耗的目的。如果内存压力依然存在,系统可能会终止后台进程以减轻内存压力。如果(整体)内存释放够了,你的应用将可以继续运行;不然,你的应用会被 iOS 终止,因为可供你的应用运行的内存不够,这时候会生成一个低内存 report(Low-Memory Report)并存储在你的设备中。

低内存 report 的格式和其它 crash report 略有不同,它没有应用的堆栈信息。一个低内存 report 的 Header 会和 crash report 的 header 有些类似。紧接着 Header 的时各个字段的系统级别的内存统计信息。记录下页大小(Page Size)字段。每一个进程的内存占用大小是根据内存的页的数量来 report 的。
一个低内存 report 最重要的部分是进程表格。这个表格列出了所有的运行进程,包括系统在生成低内存 report 时的守护进程。如果一个进程被” 遗弃” 了,会在 [原因] 一列附上具体的原因。一个进程可能被遗弃的原因有:

· [per-process-limit]:进程占用超过了它的最大内存值。每一个进程在常驻内存上的限制是早已经由系统为每个应用分配好了的。超过这个限制会导致进程被系统干掉。

注意:扩展程序 (nimo: Extension app,
例如输入法等) 的最大内存值更少。一些技术,例如地图视图和 SpriteKit,占用非常多的基础内存,因此不适合用在扩展程序里。

·

· [vm-pageshortage]/[vm-thrashing]/[vm]:由于系统内存压力被干掉。

· [vnode-limit]: 打开太多文件了。

注意:系统会尽量避免在 vnodes 已经枯竭的时候干掉高频 app。因此你的应用如果在后台,即便并没有占用什么 vnode,而有可能被杀掉。

· [highwater]:一个系统守护进程超过过了它的内存占用高水位(就是已经很危险了)。

· [jettisoned]:进程因为其它不可描述的原因被杀掉。

如果你没有在你的应用或者扩展程序里看到原因,那 crash 的原因就不是低内存压力。仔细查看一下.crash 文件(在之前章节里有写)。

当你发现一个低内存 crash,与其去担心哪一部分的代码出现问题,还不如仔细审视一下自己的内存使用习惯和针对低内存告警(low-memory warning)的处理措施。Locating Memory Issues in Your App 列出了如何使用 Leaks Instrument 工具来检查内存泄漏,和如何使用 Allocations Instrument 的 Mark Heap 功能来避免内存浪费。 Memory Usage Performance Guidelines 讨论了如何处理接受到低内存告警的问题,以及如何高效使用内存。当然,也推荐你去看下 2010 年的 WWDC 中的 Advanced Memory Analysis with Instruments 那一章节。

重要:Leaks 和 Allocation 工具不能检测所有的内存使用情况。你需要和 VM
Tracker 工具一起运行 (包含在 Allocation 工具里) 来查看你的内存运行。默认 VM Tracker 是不可用的。如果想通过 VM
Tracker 来 profile 你的应用,点击 instrument 工具,选中” Automatic
Snapshotting” 标签或者手动点击” Snapshot Now” 按钮。

相关文档

如果想查看如何使用 Zombies 模板工具来修复内存释放的 crash,可以查看 Eradicating Zombies with the Zombies Trace Template 。

如果想查看应用归档的信息,请参考 App Distribution Guide 。
如果想了解关于 crash logs 的解读,请参考 Understanding Crash Reports on iPhone OS WWDC 2010 Session 。


此次苹果新发布的 6.1 英寸 iPhone XR、5.8 英寸 iPhone XS、6.5 英寸 iPhone XS Max,WeTest 将会第一时间收入机房,关注 WeTest 官方报道,获取最新机型上线时间。

点击:http://wetest.qq.com/product/cloudphone 更多 “云真机” 产品等你来用

如果使用当中有任何疑问,欢迎联系腾讯 WeTest 企业 QQ:2852350015。

共收到 1 条回复 时间 点赞
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册