程序调试 (一) —— App Crash的调试和解决示例(一)

版本记录

版本号 时间
V1.0 2020.01.17 星期五

前言

程序总会有bug,如果有好的调试技巧和方法,那么就是事半功倍,这个专题专门和大家分享下和调试相关的技巧。希望可以帮助到大家。

开始

首先看下主要内容:

在本教程中,您将了解导致应用崩溃的原因以及如何解决该问题。

接着看下写作环境:

Swift 5, iOS 13, Xcode 11

应用程序崩溃是开发周期的自然组成部分。 面临的挑战是要了解崩溃背后的真正原因并应用正确的修补程序,而不仅仅是隐藏崩溃。

在本教程中,您将查看一些崩溃示例,对其进行调查,了解它们为什么会发生,最后,一劳永逸地修复它们。

在开始之前,了解有关Swift的一些详细信息非常有价值,这样您就可以进一步了解遇到的错误:

  • Swift使用静态类型(static typing),这意味着编译器在编译时就知道值的类型。
  • 它确保您在使用变量之前先对其进行初始化。
  • 它还会通知您可能的nil值,并确保您知道如何在代码中使用它们。

在修复项目时,您将对这些要点有更多的了解。 现在,该忙起来了。

打开入门项目。 您会发现一个名为CrashGallery的项目。

程序调试 (一) —— App Crash的调试和解决示例(一)_第1张图片

该项目显示了一些导致应用崩溃的常见情况。它是专门为演示这些方案并帮助您了解它们而设计的。

gallery展示了三种展品,展示了不同的崩溃场景:

  • 1) Force Unwrapping:显示某些不正确使用nil值的情况。
  • 2) Weak References:从storyboard中说明用户界面中的引用链,以及如何意外断开引用链并使应用崩溃。
  • 3) Invalid Table Updates:显示与UITableView共同的逻辑差异的示例,它将使您的应用程序崩溃。

您将研究所有这些崩溃情况,以了解如何找到它们以及如何进行修复。但是在开始查看崩溃及其原因之前,请花一点时间回顾一下三个重要工具,以帮助您在崩溃发生时进行跟踪。


Tools to Help You Fix and Resolve Crashes

查明崩溃的原因可能很棘手。幸运的是,有一些有用的工具可以使这项工作变得更加容易。本教程的第一步是了解最重要的三个。

1. Breakpoints

您将介绍的第一个便捷工具是断点,它使您的应用在指定的行上暂停执行,因此您可以调查该点对象的状态。

要在任何行上创建断点,只需在源文件中单击要停止执行的行号即可。

程序调试 (一) —— App Crash的调试和解决示例(一)_第2张图片

但是,如果您不确定应该看哪行怎么办?

每当从Xcode运行的应用程序崩溃时,调试器就会向您显示崩溃的行。但是有时候,这行你并不知道在哪里。对于这种情况,还有一种方便的断点:异常断点(exception breakpoint)

发生崩溃时,异常断点会自动停止应用程序,并向您显示导致该行的行。现在,这并不总是您需要解决的问题。崩溃可能是由于之前几行的错误所致,但是该行在应用中显示“嘿……我无法继续进行”。

要添加异常断点,请打开Debug navigator,然后单击导航器左下角的+。从结果菜单中选择Exception Breakpoint…。单击结果对话框外的任意位置以设置断点。

注意:异常断点是由Objective-C运行时中发生的错误触发的,这在大多数情况下是UIKit内部的错误。大多数Swift崩溃都会使调试器停止在您要查找的实际行上。

程序调试 (一) —— App Crash的调试和解决示例(一)_第3张图片

2. Console Log

控制台日志位于Xcode窗口的底部。 该应用运行时,它将显示大量有用的日志。 每当您的应用崩溃时,您都会发现一条日志消息,其中包含有关崩溃性质的信息,无论是索引超出范围的异常,nil引用还是其他。

该日志还包含有关警告的信息,因此即使您的应用程序没有崩溃也要引起注意。 它可能会突出显示可以帮助您改善应用程序的内容。

应用未运行时,此窗口将完全为空。 当您运行应用程序时,它将开始显示日志。

程序调试 (一) —— App Crash的调试和解决示例(一)_第4张图片

3. Variables View

用于调查崩溃的第三个有价值的工具是Variables View。 与控制台日志类似,当应用程序未运行时,它将完全为空;但是,当应用程序执行时,它将保持为空。

当您暂停执行时,该视图将仅显示当前作用域中变量的值,这与断点并存。

程序调试 (一) —— App Crash的调试和解决示例(一)_第5张图片

控制台日志还显示变量的值,但是Variables View更直观,并向您显示所有变量,而不仅仅是一个。 在许多情况下,它很有用,因此最好对两者都熟悉。

程序调试 (一) —— App Crash的调试和解决示例(一)_第6张图片
Console Log printing the value of a variable that is also present in the Variables View.
程序调试 (一) —— App Crash的调试和解决示例(一)_第7张图片
Variables View can show more than just text information. It can show the visual content of a UI element.

现在,您已经知道修复该损坏的应用程序,构建和运行入门应用程序所需的工具,并了解第一个展览。


The Infamous nil

Swift引入了可选参数(optionals),这意味着对象或表达式可能具有值,也可能没有值。 您不能假设自己将永远拥有值。 这是您的应用崩溃的最常见原因。

在第一个exhibit中,您会看到其中的一些情况,但是最好先了解Xcode所提供的功能,以帮助您确定崩溃的位置,发生的情况以及原因。 相当多的侦探工作。


Exhibit A: Dark Force – Force Unwrapping

构建并运行该应用程序,然后在图库屏幕中打开标题为Force Unwrapping的第一项。

程序调试 (一) —— App Crash的调试和解决示例(一)_第8张图片

此屏幕的任务是计算顶部写的数字总和。 顶部的文字视图包含电视节目Lost中输入的数字,并用逗号分隔。

当您点击Calculate按钮时,数字总和将显示在屏幕上。 试一试。

程序调试 (一) —— App Crash的调试和解决示例(一)_第9张图片

太好了,它可以按您的预期工作。 现在,对其进行处理,并在数字序列的末尾添加,two

程序调试 (一) —— App Crash的调试和解决示例(一)_第10张图片

点击Calculate看一下发生了什么...,程序崩溃了

程序调试 (一) —— App Crash的调试和解决示例(一)_第11张图片

崩溃位于第49行的ForceUnwrappingViewController.swift中。看看Xcode向您显示的内容–触发崩溃的行上有一个红色突出显示。

控制台日志包含有关崩溃的信息,并且Variables View显示了calculateSum(items :)范围内的itemfinalValue的值。

item的值为“ two”,因此当您将其转换为Int时,它会失败,并给出nil值。 强制解包 操作符导致了崩溃。

1. Proving Your Case

不要把上述猜当成事实; 质疑它,并确保真正是导致崩溃的原因。 修复崩溃问题后,您并不想反复尝试。 您希望110%确定要解决的问题。

要测试您的理论,请在控制台日志中键入以下命令:

po Int(item)

在表达式之前输入的po命令代表打印对象(print object),这是一个LLDB命令,用于打印对象的描述。 您也可以使用p,但控制台中的结果看起来会略有不同。

控制台输出将为nil

程序调试 (一) —— App Crash的调试和解决示例(一)_第12张图片

所以Int(item)nil,执行po Int(item)时! 您会获得一些其他信息。

程序调试 (一) —— App Crash的调试和解决示例(一)_第13张图片

此结果与崩溃上记录的错误相同,因此您对崩溃的来源绝对正确。

可是等等! 其他值如何工作?

在导致崩溃的同一行上添加一个断点,然后重新启动应用程序。 在计算总和之前,请记住先写,two

程序调试 (一) —— App Crash的调试和解决示例(一)_第14张图片

断点上item的值为4,并且Int(item)的结果给出一个值而不是nil

程序调试 (一) —— App Crash的调试和解决示例(一)_第15张图片

2. Finding the Right Solution

Int(_ :)item的值为4时有效,但在其值为two时无效。 换句话说,当值是带有数字的字符串而不是带有字母的字符串时,即使它们构成数字的名称,它也可以工作。

要解决此崩溃,请在calculateSum(items :)中替换以下代码行:

finalValue += Int(item)!

使用下面的代码

if let intValue = Int(item) {
  finalValue += intValue
}

上面的代码在使用Int(item)之前检查其结果是否为nil,以防崩溃。

通过单击蓝色箭头禁用断点,它将变为半透明的蓝色。 在数字之后的文本字段中构建并运行并添加所需的任何类型的文本。

程序调试 (一) —— App Crash的调试和解决示例(一)_第16张图片

它不再崩溃,但是否已完全修复? 下面不添加数字,请删除最后一个,然后重试。

程序调试 (一) —— App Crash的调试和解决示例(一)_第17张图片

该应用程序在58行的ForceUnwrappingViewController.swift中再次崩溃。

程序调试 (一) —— App Crash的调试和解决示例(一)_第18张图片

日志相关信息如下所示:

Could not cast value of type 'Swift.String' (0x7fff879c3f88) to 'Swift.Int' (0x7fff879c1e48).

崩溃行强制将结果强制转换为Int,而您提供的值是String。 这意味着valueToShownil,当您强制对其进行拆包时,该应用程序崩溃,类似于上面已修复的崩溃。

仅当总数大于100时,calculateSum(items :)才会显示总和。否则,消息应为Sum is too low

这是一个简单的解决方法。 用以下代码块替换showResult(result :)中的代码:

if let intValue = result as? Int {
  sumLabel.text = "\(intValue)"
} else if let stringValue = result as? String {
  sumLabel.text = stringValue
}

在这里,您检查是否可以将result强制转换为Int,然后创建其值的字符串并将其添加到label中。 如果可以将其强制转换为字符串,则按原样使用该值。

构建并运行。 当总数低于100时,您会看到错误消息Sum is too low

程序调试 (一) —— App Crash的调试和解决示例(一)_第19张图片

Exhibit B: Weak Grip — Weak References

您要解决的第二个崩溃涉及一种不寻常的显示和隐藏视图的方法。

程序调试 (一) —— App Crash的调试和解决示例(一)_第20张图片

Weak References屏幕是一个包含两个步骤的简单表单,其中,只有第一个问题的答案为yes时,第二步才处于激活状态。

注意:除了此应用程序中显示的方法以外,还有很多方法可以实现相同的结果。 目的是显示导致崩溃的方案,而不是设计出能正常工作的表单。

当您关闭开关时,第二个问题消失了,但是当您再次将其打开时…发生了崩溃。

程序调试 (一) —— App Crash的调试和解决示例(一)_第21张图片

该应用程序在WeakReferencesViewController.swift37行中崩溃了。

WeakReferencesViewController具有三个项目:

  • 1) 到stackViewIBOutlet
  • 2) 第二个QuestionViewIBOutlet
  • 3) IBActionswitchValueChanged(_ :),在其中您更改开关的值以删除secondQuestionView或将其重新添加到stackView的底部。

有两种方法可以弄清Xcode为什么显示nil:从Variables View中浏览值,或检查从控制台日志崩溃行中找到的两个变量的值。

程序调试 (一) —— App Crash的调试和解决示例(一)_第22张图片

从调试器的输出中可以看出,secondQuestionView的值为nil,但是为什么呢? 在switchValueChanged(_ :)的第一行上添加一个断点,然后重新启动应用程序以开始调查。

构建并运行。

程序调试 (一) —— App Crash的调试和解决示例(一)_第23张图片

当您关闭开关时,secondQuestionView不会为nil。但是,当视图消失后再次打开时,它已经为nil

1. Understanding the Crash

这样做的原因是由于UIKit中的引用链(reference chain)。每个视图都强引用(strong reference)其中显示的子视图。只要secondQuestionView在屏幕上的视图层次结构中,就会有对其的强引用。

因此,当您从第二个ViewView的superview中删除了第二个QuestionView时,您就打破了连接。并查看secondQuestionViewIBOutlet定义,您会发现它被标记为weak。因此,它从内存中释放,并且其引用更改为nil,因为没有人持有它以防止这样做。

一旦从secondQuestionView声明中删除了weak关键字,崩溃将消失。您可以对stackView进行相同的操作,以防万一,但是由于从不从父视图中移除stackView,因此它对崩溃没有影响。

删除weak关键字,然后构建并运行以再次尝试该场景。

程序调试 (一) —— App Crash的调试和解决示例(一)_第24张图片

您会看到该表格现在可以正常工作了。 该视图出现并根据需要消失。


Exhibit C: Unexpected Updates — Invalid Table Updates

第三次崩溃与之前的崩溃略有不同。 更多的是数据不匹配。

在图库屏幕上打开第三个项目,称为Invalid Table Updates,现在开始研究下。

程序调试 (一) —— App Crash的调试和解决示例(一)_第25张图片

该屏幕具有一个包含四个单元格的表格视图。 每个单元格上都有其编号。 右上角还有一个小按钮,可以添加更多单元格。

继续并按该按钮。 如您所料,发生了崩溃。 但是...哪行崩溃了? 日志中有什么?

程序调试 (一) —— App Crash的调试和解决示例(一)_第26张图片

Xcode在第32行的AppDelegate.swift中停止。

将异常断点添加到您的项目,然后构建并运行以查看差异。

程序调试 (一) —— App Crash的调试和解决示例(一)_第27张图片

这次,Xcode在第37行的InvalidTableUpdatesViewController.swift中停止了。日志为空,并且没有提供任何信息,因为断点在异常发生之前就已停止。 与之前的崩溃相比,这是另一种崩溃。

当您按继续按钮时,Xcode将返回到AppDelegate.swift中的类声明行,并且日志将包含崩溃信息。

程序调试 (一) —— App Crash的调试和解决示例(一)_第28张图片

日志包含有关崩溃的信息以及崩溃发生时的堆栈跟踪(stack trace)信息。 在大多数情况下,从Xcode进行调试并启用异常断点时,不需要堆栈跟踪信息。 看一下崩溃信息。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 4 into section 0, but there are only 4 rows in section 0 after the update.

1. A Wider View of the Problem

在检查崩溃本身之前,您应该了解addPressed()的用途。这三行执行以下操作:

  • 1) 在section 0的最后一行之后创建一个IndexPath对象。索引4代表第五项,因为索引从0开始。
  • 2) 告诉tableViewnewIndex处插入新行。
  • 3) 将新行添加到itemsList数据源数组。

首先,看一下流程:这是合理的,也是正确的。但是Xcode只是告诉您,事实并非如此。那有什么问题呢?

2. Narrowing Down the Problem

异常断点在第二行停止,因此该应用未将新行添加到itemsList。此时,这似乎是一个简单的解决方法-将新项目添加到itemsList中,然后再将其插入tableView中。它有助于更​​多地了解导致崩溃的行。

确保已启用异常断点,然后生成并运行并再次打开同一屏幕。

打开InvalidTableUpdatesViewController.swift并在导致崩溃的第37行和第44行(即tableView(_:numberOfRowsInSection :)的返回)上添加断点。按添加按钮,使应用程序在第一个断点处停止,然后按继续。现在,查看左侧的调用堆栈:

程序调试 (一) —— App Crash的调试和解决示例(一)_第29张图片

请注意,insertRows(at:with :)在内部对tableView(_:numberOfRowsInSection :)进行了调用,以检查itemsList的新大小。 由于itemsList尚未更新,因此tableView找不到添加到其中的任何东西,这使其处于不一致状态。

换句话说,您告诉tableView有一个新项目,但是tableView却没有发现itemsList增长了。

这证明了table view的行为。 将代码行添加到itemList的其他两行之间。 addPressed()现在应如下所示:

@IBAction func addPressed() {
  let newIndex = IndexPath(row: itemsList.count, section: 0)
  itemsList.append((itemsList.last ?? 0) + 1)
  tableView.insertRows(at: [newIndex], with: .automatic)
}

这将在更新视图之前更新数据源。 构建并运行,然后按添加按钮以查看是否一切正常:

程序调试 (一) —— App Crash的调试和解决示例(一)_第30张图片

Assertions

断言是手动触发的崩溃,您可以将其插入自己的代码中。显而易见的问题是:为什么要编写代码以使自己的应用程序崩溃?

这是一个很好的问题。不管看起来多么不合逻辑,您都会立刻理解为什么这样做会有所帮助。

想象一下,您正在编写一段复杂的代码,并且逻辑中有些流程没有人可以到达,因为到达它们意味着发生了致命的错误。

这些情况非常适合断言。他们会帮助您或其他使用您代码的人发现开发过程中无法正常工作。

1. Writing Your Own Reusable Code

编写framework框架也是断言可能有用的一个很好的例子。如果其他开发人员向您的框架提供了不合理的输入而效果却不理想,则可以引起一个断言。

ForceUnwrappingViewController.swift中的一个方便示例。如果没有将result强制转换为IntString,则showResult(result :)不会发生任何事情,并且使用您代码的人都不会立即知道发生了什么。当然,他们做错了什么,但是如果代码足够聪明地告诉他们什么,那岂不是很棒吗?

要尝试,请在showResult(result :)的末尾添加以下代码块:

else {
  assertionFailure("Only Int or Strings are accepted in this function")
}

如果result不是IntString,则会提出一个断言。 在calculatePressed(_ :)的末尾添加以下代码行以查看其工作方式:

showResult(result: UIView())

在这里,您向showResult(result :)发送了一个非常意外的值……一个UIView

构建并运行,打开Force Unwrapping屏幕,然后按Calculate按钮。

程序调试 (一) —— App Crash的调试和解决示例(一)_第31张图片

您的应用在第65行的ForceUnwrappingViewController.swift中崩溃了。

不出所料,崩溃行是断言调用的地方,但您尚未完全回答问题。 如果开发人员无法涵盖所有情况,崩溃的代码是否应该放在AppStore的最终应用中?

该问题的答案是:没关系。

这些断言确实存在于您的最终产品中,但好像根本就没有。

断言仅在您的应用程序在调试debug配置下构建时起作用。 在发布release配置下,断言不会执行任何操作,这是在AppStore上载应用程序时将如何构建它的方法。

想自己看看吗? 您将在下一步中进行尝试。

2. Changing Your Build Configuration

单击Xcode窗口左上角的CrashGallery目标以进行尝试。 从下拉菜单中选择Edit Scheme,然后从新窗口的左侧选择Run,然后选择Build Configuration中的Release

程序调试 (一) —— App Crash的调试和解决示例(一)_第32张图片

构建并运行,然后再次按Calculate按钮。

程序调试 (一) —— App Crash的调试和解决示例(一)_第33张图片

没有崩溃,没有断言。 它正常工作。 当您的代码获得意外的值时,它什么也不做,因此此步骤无效。

但也请注意,发行版release配置并非用于调试。 您会发现,在选择Release的情况下进行调试时,Xcode的运行情况不会达到预期。 它可能显示执行错误的行,Variables View可能不显示任何值,或者控制台日志可能不评估您打印的表达式。

如果要评估性能,而不是代码跟踪和调试,请使用此配置。

断言是一个方便的工具,可以帮助您的开发人员或其他人在遗忘之前修复它们。 但是请不要过度使用它们,因为它们会变得烦人而不是帮助。

注意:使用preconditionFailure(_:file:line :)fatalError(_:file:line :)而不是assertionFailure(_:file:line :)可以使您的应用在release配置下崩溃。

后记

本篇主要讲述了App Crash的调试和解决示例,感兴趣的给个赞或者关注~~~

程序调试 (一) —— App Crash的调试和解决示例(一)_第34张图片

你可能感兴趣的:(程序调试 (一) —— App Crash的调试和解决示例(一))