版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.01.17 星期五 |
前言
程序总会有bug,如果有好的调试技巧和方法,那么就是事半功倍,这个专题专门和大家分享下和调试相关的技巧。希望可以帮助到大家。
开始
首先看下主要内容:
在本教程中,您将了解导致应用崩溃的原因以及如何解决该问题。
接着看下写作环境:
Swift 5, iOS 13, Xcode 11
应用程序崩溃是开发周期的自然组成部分。 面临的挑战是要了解崩溃背后的真正原因并应用正确的修补程序,而不仅仅是隐藏崩溃。
在本教程中,您将查看一些崩溃示例,对其进行调查,了解它们为什么会发生,最后,一劳永逸地修复它们。
在开始之前,了解有关Swift的一些详细信息非常有价值,这样您就可以进一步了解遇到的错误:
- Swift使用静态类型
(static typing)
,这意味着编译器在编译时就知道值的类型。 - 它确保您在使用变量之前先对其进行初始化。
- 它还会通知您可能的
nil
值,并确保您知道如何在代码中使用它们。
在修复项目时,您将对这些要点有更多的了解。 现在,该忙起来了。
打开入门项目。 您会发现一个名为CrashGallery
的项目。
该项目显示了一些导致应用崩溃的常见情况。它是专门为演示这些方案并帮助您了解它们而设计的。
gallery
展示了三种展品,展示了不同的崩溃场景:
- 1) Force Unwrapping:显示某些不正确使用
nil
值的情况。 - 2) Weak References:从
storyboard
中说明用户界面中的引用链,以及如何意外断开引用链并使应用崩溃。 - 3) Invalid Table Updates:显示与
UITableView
共同的逻辑差异的示例,它将使您的应用程序崩溃。
您将研究所有这些崩溃情况,以了解如何找到它们以及如何进行修复。但是在开始查看崩溃及其原因之前,请花一点时间回顾一下三个重要工具,以帮助您在崩溃发生时进行跟踪。
Tools to Help You Fix and Resolve Crashes
查明崩溃的原因可能很棘手。幸运的是,有一些有用的工具可以使这项工作变得更加容易。本教程的第一步是了解最重要的三个。
1. Breakpoints
您将介绍的第一个便捷工具是断点,它使您的应用在指定的行上暂停执行,因此您可以调查该点对象的状态。
要在任何行上创建断点,只需在源文件中单击要停止执行的行号即可。
但是,如果您不确定应该看哪行怎么办?
每当从Xcode运行的应用程序崩溃时,调试器就会向您显示崩溃的行。但是有时候,这行你并不知道在哪里。对于这种情况,还有一种方便的断点:异常断点(exception breakpoint)
。
发生崩溃时,异常断点会自动停止应用程序,并向您显示导致该行的行。现在,这并不总是您需要解决的问题。崩溃可能是由于之前几行的错误所致,但是该行在应用中显示“嘿……我无法继续进行”。
要添加异常断点,请打开Debug navigator
,然后单击导航器左下角的+。从结果菜单中选择Exception Breakpoint…
。单击结果对话框外的任意位置以设置断点。
注意:异常断点是由
Objective-C
运行时中发生的错误触发的,这在大多数情况下是UIKit
内部的错误。大多数Swift崩溃都会使调试器停止在您要查找的实际行上。
2. Console Log
控制台日志位于Xcode
窗口的底部。 该应用运行时,它将显示大量有用的日志。 每当您的应用崩溃时,您都会发现一条日志消息,其中包含有关崩溃性质的信息,无论是索引超出范围的异常,nil
引用还是其他。
该日志还包含有关警告的信息,因此即使您的应用程序没有崩溃也要引起注意。 它可能会突出显示可以帮助您改善应用程序的内容。
应用未运行时,此窗口将完全为空。 当您运行应用程序时,它将开始显示日志。
3. Variables View
用于调查崩溃的第三个有价值的工具是Variables View
。 与控制台日志类似,当应用程序未运行时,它将完全为空;但是,当应用程序执行时,它将保持为空。
当您暂停执行时,该视图将仅显示当前作用域中变量的值,这与断点并存。
控制台日志还显示变量的值,但是Variables View
更直观,并向您显示所有变量,而不仅仅是一个。 在许多情况下,它很有用,因此最好对两者都熟悉。
现在,您已经知道修复该损坏的应用程序,构建和运行入门应用程序所需的工具,并了解第一个展览。
The Infamous nil
Swift引入了可选参数(optionals)
,这意味着对象或表达式可能具有值,也可能没有值。 您不能假设自己将永远拥有值。 这是您的应用崩溃的最常见原因。
在第一个exhibit
中,您会看到其中的一些情况,但是最好先了解Xcode所提供的功能,以帮助您确定崩溃的位置,发生的情况以及原因。 相当多的侦探工作。
Exhibit A: Dark Force – Force Unwrapping
构建并运行该应用程序,然后在图库屏幕中打开标题为Force Unwrapping
的第一项。
此屏幕的任务是计算顶部写的数字总和。 顶部的文字视图包含电视节目Lost中输入的数字,并用逗号分隔。
当您点击Calculate
按钮时,数字总和将显示在屏幕上。 试一试。
太好了,它可以按您的预期工作。 现在,对其进行处理,并在数字序列的末尾添加,two
:
点击Calculate
看一下发生了什么...,程序崩溃了
崩溃位于第49
行的ForceUnwrappingViewController.swift
中。看看Xcode向您显示的内容–触发崩溃的行上有一个红色突出显示。
控制台日志包含有关崩溃的信息,并且Variables View
显示了calculateSum(items :)
范围内的item
和finalValue
的值。
item
的值为“ two”
,因此当您将其转换为Int
时,它会失败,并给出nil
值。 强制解包!
操作符导致了崩溃。
1. Proving Your Case
不要把上述猜当成事实; 质疑它,并确保真正是导致崩溃的原因。 修复崩溃问题后,您并不想反复尝试。 您希望110%
确定要解决的问题。
要测试您的理论,请在控制台日志中键入以下命令:
po Int(item)
在表达式之前输入的po
命令代表打印对象(print object)
,这是一个LLDB
命令,用于打印对象的描述。 您也可以使用p
,但控制台中的结果看起来会略有不同。
控制台输出将为nil
:
所以Int(item)
为nil
,执行po Int(item)
时! 您会获得一些其他信息。
此结果与崩溃上记录的错误相同,因此您对崩溃的来源绝对正确。
可是等等! 其他值如何工作?
在导致崩溃的同一行上添加一个断点,然后重新启动应用程序。 在计算总和之前,请记住先写,two
。
断点上item
的值为4
,并且Int(item)
的结果给出一个值而不是nil
。
2. Finding the Right Solution
Int(_ :)
在item
的值为4
时有效,但在其值为two
时无效。 换句话说,当值是带有数字的字符串而不是带有字母的字符串时,即使它们构成数字的名称,它也可以工作。
要解决此崩溃,请在calculateSum(items :)
中替换以下代码行:
finalValue += Int(item)!
使用下面的代码
if let intValue = Int(item) {
finalValue += intValue
}
上面的代码在使用Int(item)
之前检查其结果是否为nil
,以防崩溃。
通过单击蓝色箭头禁用断点,它将变为半透明的蓝色。 在数字之后的文本字段中构建并运行并添加所需的任何类型的文本。
它不再崩溃,但是否已完全修复? 下面不添加数字,请删除最后一个,然后重试。
该应用程序在58
行的ForceUnwrappingViewController.swift
中再次崩溃。
日志相关信息如下所示:
Could not cast value of type 'Swift.String' (0x7fff879c3f88) to 'Swift.Int' (0x7fff879c1e48).
崩溃行强制将结果强制转换为Int
,而您提供的值是String
。 这意味着valueToShow
为nil
,当您强制对其进行拆包时,该应用程序崩溃,类似于上面已修复的崩溃。
仅当总数大于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
。
Exhibit B: Weak Grip — Weak References
您要解决的第二个崩溃涉及一种不寻常的显示和隐藏视图的方法。
Weak References
屏幕是一个包含两个步骤的简单表单,其中,只有第一个问题的答案为yes
时,第二步才处于激活状态。
注意:除了此应用程序中显示的方法以外,还有很多方法可以实现相同的结果。 目的是显示导致崩溃的方案,而不是设计出能正常工作的表单。
当您关闭开关时,第二个问题消失了,但是当您再次将其打开时…发生了崩溃。
该应用程序在WeakReferencesViewController.swift
第37
行中崩溃了。
WeakReferencesViewController
具有三个项目:
- 1) 到
stackView
的IBOutlet
。 - 2) 第二个
QuestionView
的IBOutlet
。 - 3)
IBAction
到switchValueChanged(_ :)
,在其中您更改开关的值以删除secondQuestionView
或将其重新添加到stackView
的底部。
有两种方法可以弄清Xcode
为什么显示nil
:从Variables View
中浏览值,或检查从控制台日志崩溃行中找到的两个变量的值。
从调试器的输出中可以看出,secondQuestionView
的值为nil
,但是为什么呢? 在switchValueChanged(_ :)
的第一行上添加一个断点,然后重新启动应用程序以开始调查。
构建并运行。
当您关闭开关时,secondQuestionView
不会为nil
。但是,当视图消失后再次打开时,它已经为nil
。
1. Understanding the Crash
这样做的原因是由于UIKit
中的引用链(reference chain)
。每个视图都强引用(strong reference)
其中显示的子视图。只要secondQuestionView
在屏幕上的视图层次结构中,就会有对其的强引用。
因此,当您从第二个ViewView的superview
中删除了第二个QuestionView
时,您就打破了连接。并查看secondQuestionView
的IBOutlet
定义,您会发现它被标记为weak
。因此,它从内存中释放,并且其引用更改为nil
,因为没有人持有它以防止这样做。
一旦从secondQuestionView
声明中删除了weak
关键字,崩溃将消失。您可以对stackView
进行相同的操作,以防万一,但是由于从不从父视图中移除stackView
,因此它对崩溃没有影响。
删除weak
关键字,然后构建并运行以再次尝试该场景。
您会看到该表格现在可以正常工作了。 该视图出现并根据需要消失。
Exhibit C: Unexpected Updates — Invalid Table Updates
第三次崩溃与之前的崩溃略有不同。 更多的是数据不匹配。
在图库屏幕上打开第三个项目,称为Invalid Table Updates
,现在开始研究下。
该屏幕具有一个包含四个单元格的表格视图。 每个单元格上都有其编号。 右上角还有一个小按钮,可以添加更多单元格。
继续并按该按钮。 如您所料,发生了崩溃。 但是...哪行崩溃了? 日志中有什么?
Xcode在第32
行的AppDelegate.swift
中停止。
将异常断点添加到您的项目,然后构建并运行以查看差异。
这次,Xcode
在第37
行的InvalidTableUpdatesViewController.swift
中停止了。日志为空,并且没有提供任何信息,因为断点在异常发生之前就已停止。 与之前的崩溃相比,这是另一种崩溃。
当您按继续按钮时,Xcode
将返回到AppDelegate.swift
中的类声明行,并且日志将包含崩溃信息。
日志包含有关崩溃的信息以及崩溃发生时的堆栈跟踪(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) 告诉
tableView
在newIndex
处插入新行。 - 3) 将新行添加到
itemsList
数据源数组。
首先,看一下流程:这是合理的,也是正确的。但是Xcode只是告诉您,事实并非如此。那有什么问题呢?
2. Narrowing Down the Problem
异常断点在第二行停止,因此该应用未将新行添加到itemsList
。此时,这似乎是一个简单的解决方法-将新项目添加到itemsList
中,然后再将其插入tableView
中。它有助于更多地了解导致崩溃的行。
确保已启用异常断点,然后生成并运行并再次打开同一屏幕。
打开InvalidTableUpdatesViewController.swift
并在导致崩溃的第37
行和第44
行(即tableView(_:numberOfRowsInSection :)
的返回)上添加断点。按添加按钮,使应用程序在第一个断点处停止,然后按继续。现在,查看左侧的调用堆栈:
请注意,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)
}
这将在更新视图之前更新数据源。 构建并运行,然后按添加按钮以查看是否一切正常:
Assertions
断言是手动触发的崩溃,您可以将其插入自己的代码中。显而易见的问题是:为什么要编写代码以使自己的应用程序崩溃?
这是一个很好的问题。不管看起来多么不合逻辑,您都会立刻理解为什么这样做会有所帮助。
想象一下,您正在编写一段复杂的代码,并且逻辑中有些流程没有人可以到达,因为到达它们意味着发生了致命的错误。
这些情况非常适合断言。他们会帮助您或其他使用您代码的人发现开发过程中无法正常工作。
1. Writing Your Own Reusable Code
编写framework
框架也是断言可能有用的一个很好的例子。如果其他开发人员向您的框架提供了不合理的输入而效果却不理想,则可以引起一个断言。
ForceUnwrappingViewController.swift
中的一个方便示例。如果没有将result
强制转换为Int
或String
,则showResult(result :)
不会发生任何事情,并且使用您代码的人都不会立即知道发生了什么。当然,他们做错了什么,但是如果代码足够聪明地告诉他们什么,那岂不是很棒吗?
要尝试,请在showResult(result :)
的末尾添加以下代码块:
else {
assertionFailure("Only Int or Strings are accepted in this function")
}
如果result
不是Int
或String
,则会提出一个断言。 在calculatePressed(_ :)
的末尾添加以下代码行以查看其工作方式:
showResult(result: UIView())
在这里,您向showResult(result :)
发送了一个非常意外的值……一个UIView
!
构建并运行,打开Force Unwrapping
屏幕,然后按Calculate
按钮。
您的应用在第65
行的ForceUnwrappingViewController.swift
中崩溃了。
不出所料,崩溃行是断言调用的地方,但您尚未完全回答问题。 如果开发人员无法涵盖所有情况,崩溃的代码是否应该放在AppStore
的最终应用中?
该问题的答案是:没关系。
这些断言确实存在于您的最终产品中,但好像根本就没有。
断言仅在您的应用程序在调试debug
配置下构建时起作用。 在发布release
配置下,断言不会执行任何操作,这是在AppStore上载应用程序时将如何构建它的方法。
想自己看看吗? 您将在下一步中进行尝试。
2. Changing Your Build Configuration
单击Xcode窗口左上角的CrashGallery
目标以进行尝试。 从下拉菜单中选择Edit Scheme
,然后从新窗口的左侧选择Run
,然后选择Build Configuration
中的Release
。
构建并运行,然后再次按Calculate
按钮。
没有崩溃,没有断言。 它正常工作。 当您的代码获得意外的值时,它什么也不做,因此此步骤无效。
但也请注意,发行版release
配置并非用于调试。 您会发现,在选择Release
的情况下进行调试时,Xcode
的运行情况不会达到预期。 它可能显示执行错误的行,Variables View
可能不显示任何值,或者控制台日志可能不评估您打印的表达式。
如果要评估性能,而不是代码跟踪和调试,请使用此配置。
断言是一个方便的工具,可以帮助您的开发人员或其他人在遗忘之前修复它们。 但是请不要过度使用它们,因为它们会变得烦人而不是帮助。
注意:使用preconditionFailure(_:file:line :)
或fatalError(_:file:line :)
而不是assertionFailure(_:file:line :)
可以使您的应用在release
配置下崩溃。
后记
本篇主要讲述了App Crash的调试和解决示例,感兴趣的给个赞或者关注~~~