点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新
原书为 iOS Crash Dump Analysis Book,已得作者授权,欢迎 star
在本章中,我们将展示那些运行时检测到问题并导致应用崩溃的示例。
在崩溃报告中,我们可以通过异常类型 EXC_BREAKPOINT (SIGTRAP)
来区分这些崩溃。
我们考虑两个例子。 第一个例子,说明运行时如何处理强制展开nil可选项的情况。
我们的第二个例子说明运行时如何处理释放正在等待的信号量。
解包 Nil 可选类型
Swift 编程语言是朝着编写默认安全代码迈出的重要一步。
Swift 的核心概念是明确地处理可选性。在类型声明中,尾随的 ?
表示该值可以不存在,用 nil
表示。这些类型需要显式展开来访问它们存储的值。
当一个值在对象初始化时不可用,但在对象生命周期的后期,然后尾随' !'用于保存值的类型。这意味着可以在代码中处理该值,而不需要显式解包。它被称为可选的隐式解包可选。
注意,从 Swift 4.2 开始,在实现级别,它是可选的,带有注释,表明可以使用它而无需显式解包。
我们使用 icdab_wrap
示例程序来演示由于错误使用可选控件而导致的崩溃。
iOS UIKit Outlets
使用故事板来声明用户界面,并将UIViews
与UIViewController
相关联,这是一个标准范例。
当用户界面更新时,比如启动我们的应用程序时,或者在场景之间执行segue时,故事板实例支持 UIViewController
并在我们的 UIViewController
对象中将字段设置为已创建的 UIViews
。
当我们将故事板链接到控制器代码时,会自动生成一个字段声明,例如:
@IBOutlet weak var planetImageOutlet: UIImageView!
所有权规则
如果我们没有显式创建对象,并且没有将所有权传递给我们,那我们不应缩短所传递对象的生命周期。
在我们的icdab_wrap
示例中,我们有一个父页面,我们可以进入一个具有大冥王星图像的子页面。
该图像是从 Internet 下载的。 当离开该页面并访问原始页面时,代码尝试通过释放与图像关联的内存来减少内存。
对于这种图像清理策略是是否有用可取,存在另一种争论。应该使用一个分析工具来告诉我们什么时候应该尽量减少内存占用。
我们的代码存在一个 bug:
override func viewWillDisappear(_ animated: Bool) {
planetImageOutlet = nil
// BUG; should be planetImageOutlet.image = nil
}
与其将图像视图的图像设置为 nil,不如将图像视图本身设置为 nil
。
这意味着当我们重新访问Pluto场景时,由于我们的 planetImageOutlet
为nil
,因此尝试存储下载的图像时会崩溃。
func imageDownloaded(_ image: UIImage) {
self.planetImageOutlet.image = image
}
该代码将崩溃,因为它隐式解包了已设置为 nil
的可选类型。
解包 nil 可选类型的崩溃报告
当我们从 swift 运行时强制解包可选类型 nil 中得到崩溃时,我们看到:
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001011f7ff8
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [0]
Triggered by Thread: 0
注意这是一个异常类型, EXC_BREAKPOINT (SIGTRAP)
。
我们看到运行时环境由于遇到问题而引发了断点异常。这是通过查看堆栈顶部的swift核心库来识别的。
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libswiftCore.dylib
0x00000001011f7ff8 0x101050000 + 1736696
1 libswiftCore.dylib
0x00000001011f7ff8 0x101050000 + 1736696
2 libswiftCore.dylib
0x00000001010982b8 0x101050000 + 295608
3 icdab_wrap
0x0000000100d3d404
PlanetViewController.imageDownloaded(_:)
+ 37892 (PlanetViewController.swift:45)
存在未初始化指针的另一个细微提示是机器寄存器的特殊值是 0xbaddc0dedeadbead
。
这是由编译器设置的,以指示未初始化的指针:
Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x0000000100ecc100 x1: 0x00000001c005b9f0
x2: 0x0000000000000008
x3: 0x0000000183a4906c
x4: 0x0000000000000080 x5: 0x0000000000000020
x6: 0x0048000004210103
x7: 0x00000000000010ff
x8: 0x00000001c00577f0 x9: 0x0000000000000000
x10: 0x0000000000000002
x11: 0xbaddc0dedeadbead
x12: 0x0000000000000001 x13: 0x0000000000000002
x14: 0x0000000000000000
x15: 0x000a65756c617620
x16: 0x0000000183b9b8cc x17: 0x0000000000000000
x18: 0x0000000000000000
x19: 0x0000000000000000
x20: 0x0000000000000002 x21: 0x0000000000000039
x22: 0x0000000100d3f3d0
x23: 0x0000000000000002
x24: 0x000000000000000b x25: 0x0000000100d3f40a
x26: 0x0000000000000014
x27: 0x0000000000000000
x28: 0x0000000002ffffff fp: 0x000000016f0ca8e0
lr: 0x00000001011f7ff8
sp: 0x000000016f0ca8a0 pc: 0x00000001011f7ff8
cpsr: 0x60000000
释放正在使用的信号量
libdispatch
库支持识别运行时问题。
出现此类问题时,应用程序崩溃并显示异常类型, EXC_BREAKPOINT (SIGTRAP)
我们使用 icdab_sema
示例程序来演示 libdispatch
检测到的由于错误使用信号量而导致的崩溃。
libdispatch
库是用于管理并发的操作系统库(称为Grand Central Dispatch或GCD)。该库可从Apple处以开源形式获得。
该库抽象了操作系统如何提供对多核CPU资源的访问的详细信息。 在崩溃期间,它会向崩溃报告提供其他信息。 这意味着,如果我们愿意,我们可以找到检测到运行时问题的代码。
释放信号量的崩溃示例
icdab_sema
示例程序在启动时发生崩溃。
崩溃报告如下(为便于演示,将其截断):
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001814076b8
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [0]
Triggered by Thread: 0
Application Specific Information:
BUG IN CLIENT OF LIBDISPATCH:
Semaphore object deallocated while in use
Abort Cause 1
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libdispatch.dylib 0x00000001814076b8
_dispatch_semaphore_dispose$VARIANT$mp + 76
1 libdispatch.dylib 0x00000001814067f0
_dispatch_dispose$VARIANT$mp + 80
2 icdab_sema_ios 0x00000001006ea98c
use_sema + 27020 (main.m:18)
3 icdab_sema_ios 0x00000001006ea9bc
main + 27068 (main.m:22)
4 libdyld.dylib 0x0000000181469fc0
start + 4
Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x00000001c409df10 x1: 0x000000016f71ba4f
x2: 0xffffffffffffffe0 x3: 0x00000001c409df20
x4: 0x00000001c409df80 x5: 0x0000000000000044
x6: 0x000000018525c984 x7: 0x0000000000000400
x8: 0x0000000000000001 x9: 0x0000000000000000
x10: 0x000000018140766c x11: 0x000000000001dc01
x12: 0x000000000001db00 x13: 0x0000000000000001
x14: 0x0000000000000000 x15: 0x0001dc010001dcc0
x16: 0x000000018140766c x17: 0x0000000181404b58
x18: 0x0000000000000000 x19: 0x00000001b38f4c80
x20: 0x0000000000000000 x21: 0x0000000000000000
x22: 0x00000001c409df10 x23: 0x0000000000000000
x24: 0x0000000000000000 x25: 0x0000000000000000
x26: 0x0000000000000000 x27: 0x0000000000000000
x28: 0x000000016f71bb18 fp: 0x000000016f71ba70
lr: 0x00000001814067f0
sp: 0x000000016f71ba40 pc: 0x00000001814076b8
cpsr: 0x80000000
错误的信号量代码
重现信号量问题的代码基于使用手动引用计数(MRC)的Xcode项目。 这是一个旧设置,但在与遗留代码库集成时可能会遇到。 在项目级别,将选项 “Objective-C Automatic Reference Counting” 设置为“NO”。 然后,我们可以直接调用 dispatch_release
API。
代码如下:
#import <Foundation/Foundation.h>
void use_sema() {
dispatch_semaphore_t aSemaphore =
dispatch_semaphore_create(1);
dispatch_semaphore_wait(aSemaphore,
DISPATCH_TIME_FOREVER);
// dispatch_semaphore_signal(aSemaphore);
dispatch_release(aSemaphore);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
use_sema();
}
return 0;
}
使用特定于应用程序的崩溃报告信息
在我们的示例中,崩溃报告的 Application Specific Information
部分直接说明了问题。
BUG IN CLIENT OF LIBDISPATCH:
Semaphore object deallocated while in use
我们只需要给信号量发信号就可以避免这个问题。
如果我们有一个更不寻常的问题,或者想更深入地了解它,我们可以查找该库的源代码,并在代码中找到相关诊断消息。
以下是相关的库代码:
void
_dispatch_semaphore_dispose(dispatch_object_t dou,
DISPATCH_UNUSED bool *allow_free)
{
dispatch_semaphore_t dsema = dou._dsema;
if (dsema->dsema_value < dsema->dsema_orig) {
DISPATCH_CLIENT_CRASH(
dsema->dsema_orig - dsema->dsema_value,
"Semaphore object deallocated
while in use"
);
}
_dispatch_sema4_dispose(&dsema->dsema_sema,
_DSEMA4_POLICY_FIFO);
}
在这里,我们可以通过 DISPATCH_CLIENT_CRASH
宏查看导致崩溃的库。
经验教训
在现代应用程序代码中,应避免使用手动引用计数。
当通过运行时库发生崩溃时,我们需要返回API规范以了解我们如何违反导致崩溃的API合同。崩溃报告中特定于应用程序的信息应该有助于我们在重新阅读API文档、研究工作样例代码和查看运行时库源代码的详细级别(如果可用)时集中注意力。
如果已从旧代码库中继承了MRC代码,则应使用设计模式来包装基于MRC的代码,并向其中提供干净的API。然后,该程序的其余部分可以使用自动引用计数(ARC)。这将包含问题,并允许新代码从ARC中受益。 也可以将特定文件标记为MRC。需要为文件设置编译器标志选项 -fno-objc-arc
。可以在 Xcode 的 Build Phases-> Compile Sources区域中找到它。
如果遗留代码不需要增强,则最好将其保留下来,而仅用 Facade API 对其进行包装。 然后,我们可以为该API编写一些测试用例。未主动更新的代码往往仅在以新方式使用时才会引起错误。有时,具有遗留代码知识的员工已离开项目,因此知识较少的员工进行更新可能会带来风险。
如果可以随时间替换旧代码,那就太好了。 通常,需要业务证明。 一种策略是将旧模块分解成较小的部分。 如果能战略性地做到这一点,那么可以采用现代编码实践对较小的部分之一进行重新加工。当增强了此类模块以解决新客户需求时,它将成为双赢。
感谢你阅读本文! 🚀