[译]《iOS Crash Dump Analysis》- 运行时崩溃

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新
原书为 iOS Crash Dump Analysis Book,已得作者授权,欢迎 star

在本章中,我们将展示那些运行时检测到问题并导致应用崩溃的示例。

在崩溃报告中,我们可以通过异常类型 EXC_BREAKPOINT (SIGTRAP) 来区分这些崩溃。

我们考虑两个例子。 第一个例子,说明运行时如何处理强制展开nil可选项的情况。
我们的第二个例子说明运行时如何处理释放正在等待的信号量。

解包 Nil 可选类型

Swift 编程语言是朝着编写默认安全代码迈出的重要一步。

Swift 的核心概念是明确地处理可选性。在类型声明中,尾随的 ? 表示该值可以不存在,用 nil 表示。这些类型需要显式展开来访问它们存储的值。

当一个值在对象初始化时不可用,但在对象生命周期的后期,然后尾随' !'用于保存值的类型。这意味着可以在代码中处理该值,而不需要显式解包。它被称为可选的隐式解包可选。
注意,从 Swift 4.2 开始,在实现级别,它是可选的,带有注释,表明可以使用它而无需显式解包。

我们使用 icdab_wrap 示例程序来演示由于错误使用可选控件而导致的崩溃。

iOS UIKit Outlets

使用故事板来声明用户界面,并将UIViewsUIViewController相关联,这是一个标准范例。

当用户界面更新时,比如启动我们的应用程序时,或者在场景之间执行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场景时,由于我们的 planetImageOutletnil,因此尝试存储下载的图像时会崩溃。

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 

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编写一些测试用例。未主动更新的代码往往仅在以新方式使用时才会引起错误。有时,具有遗留代码知识的员工已离开项目,因此知识较少的员工进行更新可能会带来风险。

如果可以随时间替换旧代码,那就太好了。 通常,需要业务证明。 一种策略是将旧模块分解成较小的部分。 如果能战略性地做到这一点,那么可以采用现代编码实践对较小的部分之一进行重新加工。当增强了此类模块以解决新客户需求时,它将成为双赢。

感谢你阅读本文!

你可能感兴趣的:([译]《iOS Crash Dump Analysis》- 运行时崩溃)