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

版本记录

版本号 时间
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更直观,并向您显示所有变量,而不仅仅是一个。 在许多情况下,它很有用,因此最好对两者都熟悉。

Console Log printing the value of a variable that is also present in the Variables View.
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的第一项。

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

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

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

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

崩溃位于第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

所以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。 这意味着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


Exhibit B: Weak Grip — Weak References

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

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

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

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

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

WeakReferencesViewController具有三个项目:

  • 1) 到stackViewIBOutlet
  • 2) 第二个QuestionViewIBOutlet
  • 3) IBActionswitchValueChanged(_ :),在其中您更改开关的值以删除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时,您就打破了连接。并查看secondQuestionViewIBOutlet定义,您会发现它被标记为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) 告诉tableViewnewIndex处插入新行。
  • 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强制转换为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按钮。

您的应用在第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的调试和解决示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 因为要结局swift3.0中引用snapKit的问题,看到一篇介绍Xcode8,swift3变化的文章,觉得很详细...
    uniapp阅读 4,385评论 0 12
  • 泪化马嵬风, 一场春梦空。 遗留千古恨, 谁与问苍穹。
    明月清泉_e47b阅读 169评论 4 4
  • 春盛秋衰自寻常,何用添词助悲喜。 人生渐行如梦昏,一生多在无知中。
    明心123阅读 228评论 0 3
  • 对我来说时代没有太多意义 自己就是朵奇葩 贴不上时代的标签 这个年代独生子女太多 对亲情的理解会有所怪异 我算一个...
    L丶Ctrl阅读 313评论 0 0
  • 或许你会觉得, 如今社会冷漠漫延, 冰冷总是令人寒颤。 可是, 你是否考虑过自己的内心是否已冰封。 若有光芒洒下,...
    简萌玺阅读 160评论 0 1