版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2020.01.17 星期五 |
前言
程序總會(huì)有bug,如果有好的調(diào)試技巧和方法,那么就是事半功倍,這個(gè)專題專門和大家分享下和調(diào)試相關(guān)的技巧。希望可以幫助到大家。
開始
首先看下主要內(nèi)容:
在本教程中,您將了解導(dǎo)致應(yīng)用崩潰的原因以及如何解決該問題。
接著看下寫作環(huán)境:
Swift 5, iOS 13, Xcode 11
應(yīng)用程序崩潰是開發(fā)周期的自然組成部分。 面臨的挑戰(zhàn)是要了解崩潰背后的真正原因并應(yīng)用正確的修補(bǔ)程序,而不僅僅是隱藏崩潰。
在本教程中,您將查看一些崩潰示例,對(duì)其進(jìn)行調(diào)查,了解它們?yōu)槭裁磿?huì)發(fā)生,最后,一勞永逸地修復(fù)它們。
在開始之前,了解有關(guān)Swift的一些詳細(xì)信息非常有價(jià)值,這樣您就可以進(jìn)一步了解遇到的錯(cuò)誤:
- Swift使用靜態(tài)類型
(static typing)
,這意味著編譯器在編譯時(shí)就知道值的類型。 - 它確保您在使用變量之前先對(duì)其進(jìn)行初始化。
- 它還會(huì)通知您可能的
nil
值,并確保您知道如何在代碼中使用它們。
在修復(fù)項(xiàng)目時(shí),您將對(duì)這些要點(diǎn)有更多的了解。 現(xiàn)在,該忙起來了。
打開入門項(xiàng)目。 您會(huì)發(fā)現(xiàn)一個(gè)名為CrashGallery
的項(xiàng)目。
該項(xiàng)目顯示了一些導(dǎo)致應(yīng)用崩潰的常見情況。它是專門為演示這些方案并幫助您了解它們而設(shè)計(jì)的。
gallery
展示了三種展品,展示了不同的崩潰場(chǎng)景:
- 1) Force Unwrapping:顯示某些不正確使用
nil
值的情況。 - 2) Weak References:從
storyboard
中說明用戶界面中的引用鏈,以及如何意外斷開引用鏈并使應(yīng)用崩潰。 - 3) Invalid Table Updates:顯示與
UITableView
共同的邏輯差異的示例,它將使您的應(yīng)用程序崩潰。
您將研究所有這些崩潰情況,以了解如何找到它們以及如何進(jìn)行修復(fù)。但是在開始查看崩潰及其原因之前,請(qǐng)花一點(diǎn)時(shí)間回顧一下三個(gè)重要工具,以幫助您在崩潰發(fā)生時(shí)進(jìn)行跟蹤。
Tools to Help You Fix and Resolve Crashes
查明崩潰的原因可能很棘手。幸運(yùn)的是,有一些有用的工具可以使這項(xiàng)工作變得更加容易。本教程的第一步是了解最重要的三個(gè)。
1. Breakpoints
您將介紹的第一個(gè)便捷工具是斷點(diǎn),它使您的應(yīng)用在指定的行上暫停執(zhí)行,因此您可以調(diào)查該點(diǎn)對(duì)象的狀態(tài)。
要在任何行上創(chuàng)建斷點(diǎn),只需在源文件中單擊要停止執(zhí)行的行號(hào)即可。
但是,如果您不確定應(yīng)該看哪行怎么辦?
每當(dāng)從Xcode運(yùn)行的應(yīng)用程序崩潰時(shí),調(diào)試器就會(huì)向您顯示崩潰的行。但是有時(shí)候,這行你并不知道在哪里。對(duì)于這種情況,還有一種方便的斷點(diǎn):異常斷點(diǎn)(exception breakpoint)
。
發(fā)生崩潰時(shí),異常斷點(diǎn)會(huì)自動(dòng)停止應(yīng)用程序,并向您顯示導(dǎo)致該行的行。現(xiàn)在,這并不總是您需要解決的問題。崩潰可能是由于之前幾行的錯(cuò)誤所致,但是該行在應(yīng)用中顯示“嘿……我無法繼續(xù)進(jìn)行”。
要添加異常斷點(diǎn),請(qǐng)打開Debug navigator
,然后單擊導(dǎo)航器左下角的+。從結(jié)果菜單中選擇Exception Breakpoint…
。單擊結(jié)果對(duì)話框外的任意位置以設(shè)置斷點(diǎn)。
注意:異常斷點(diǎn)是由
Objective-C
運(yùn)行時(shí)中發(fā)生的錯(cuò)誤觸發(fā)的,這在大多數(shù)情況下是UIKit
內(nèi)部的錯(cuò)誤。大多數(shù)Swift崩潰都會(huì)使調(diào)試器停止在您要查找的實(shí)際行上。
2. Console Log
控制臺(tái)日志位于Xcode
窗口的底部。 該應(yīng)用運(yùn)行時(shí),它將顯示大量有用的日志。 每當(dāng)您的應(yīng)用崩潰時(shí),您都會(huì)發(fā)現(xiàn)一條日志消息,其中包含有關(guān)崩潰性質(zhì)的信息,無論是索引超出范圍的異常,nil
引用還是其他。
該日志還包含有關(guān)警告的信息,因此即使您的應(yīng)用程序沒有崩潰也要引起注意。 它可能會(huì)突出顯示可以幫助您改善應(yīng)用程序的內(nèi)容。
應(yīng)用未運(yùn)行時(shí),此窗口將完全為空。 當(dāng)您運(yùn)行應(yīng)用程序時(shí),它將開始顯示日志。
3. Variables View
用于調(diào)查崩潰的第三個(gè)有價(jià)值的工具是Variables View
。 與控制臺(tái)日志類似,當(dāng)應(yīng)用程序未運(yùn)行時(shí),它將完全為空;但是,當(dāng)應(yīng)用程序執(zhí)行時(shí),它將保持為空。
當(dāng)您暫停執(zhí)行時(shí),該視圖將僅顯示當(dāng)前作用域中變量的值,這與斷點(diǎn)并存。
控制臺(tái)日志還顯示變量的值,但是Variables View
更直觀,并向您顯示所有變量,而不僅僅是一個(gè)。 在許多情況下,它很有用,因此最好對(duì)兩者都熟悉。
現(xiàn)在,您已經(jīng)知道修復(fù)該損壞的應(yīng)用程序,構(gòu)建和運(yùn)行入門應(yīng)用程序所需的工具,并了解第一個(gè)展覽。
The Infamous nil
Swift引入了可選參數(shù)(optionals)
,這意味著對(duì)象或表達(dá)式可能具有值,也可能沒有值。 您不能假設(shè)自己將永遠(yuǎn)擁有值。 這是您的應(yīng)用崩潰的最常見原因。
在第一個(gè)exhibit
中,您會(huì)看到其中的一些情況,但是最好先了解Xcode所提供的功能,以幫助您確定崩潰的位置,發(fā)生的情況以及原因。 相當(dāng)多的偵探工作。
Exhibit A: Dark Force – Force Unwrapping
構(gòu)建并運(yùn)行該應(yīng)用程序,然后在圖庫屏幕中打開標(biāo)題為Force Unwrapping
的第一項(xiàng)。
此屏幕的任務(wù)是計(jì)算頂部寫的數(shù)字總和。 頂部的文字視圖包含電視節(jié)目Lost中輸入的數(shù)字,并用逗號(hào)分隔。
當(dāng)您點(diǎn)擊Calculate
按鈕時(shí),數(shù)字總和將顯示在屏幕上。 試一試。
太好了,它可以按您的預(yù)期工作。 現(xiàn)在,對(duì)其進(jìn)行處理,并在數(shù)字序列的末尾添加,two
:
點(diǎn)擊Calculate
看一下發(fā)生了什么...,程序崩潰了
崩潰位于第49
行的ForceUnwrappingViewController.swift
中。看看Xcode向您顯示的內(nèi)容–觸發(fā)崩潰的行上有一個(gè)紅色突出顯示。
控制臺(tái)日志包含有關(guān)崩潰的信息,并且Variables View
顯示了calculateSum(items :)
范圍內(nèi)的item
和finalValue
的值。
item
的值為“ two”
,因此當(dāng)您將其轉(zhuǎn)換為Int
時(shí),它會(huì)失敗,并給出nil
值。 強(qiáng)制解包!
操作符導(dǎo)致了崩潰。
1. Proving Your Case
不要把上述猜當(dāng)成事實(shí); 質(zhì)疑它,并確保真正是導(dǎo)致崩潰的原因。 修復(fù)崩潰問題后,您并不想反復(fù)嘗試。 您希望110%
確定要解決的問題。
要測(cè)試您的理論,請(qǐng)?jiān)诳刂婆_(tái)日志中鍵入以下命令:
po Int(item)
在表達(dá)式之前輸入的po
命令代表打印對(duì)象(print object)
,這是一個(gè)LLDB
命令,用于打印對(duì)象的描述。 您也可以使用p
,但控制臺(tái)中的結(jié)果看起來會(huì)略有不同。
控制臺(tái)輸出將為nil
:
所以Int(item)
為nil
,執(zhí)行po Int(item)
時(shí)! 您會(huì)獲得一些其他信息。
此結(jié)果與崩潰上記錄的錯(cuò)誤相同,因此您對(duì)崩潰的來源絕對(duì)正確。
可是等等! 其他值如何工作?
在導(dǎo)致崩潰的同一行上添加一個(gè)斷點(diǎn),然后重新啟動(dòng)應(yīng)用程序。 在計(jì)算總和之前,請(qǐng)記住先寫,two
。
斷點(diǎn)上item
的值為4
,并且Int(item)
的結(jié)果給出一個(gè)值而不是nil
。
2. Finding the Right Solution
Int(_ :)
在item
的值為4
時(shí)有效,但在其值為two
時(shí)無效。 換句話說,當(dāng)值是帶有數(shù)字的字符串而不是帶有字母的字符串時(shí),即使它們構(gòu)成數(shù)字的名稱,它也可以工作。
要解決此崩潰,請(qǐng)?jiān)?code>calculateSum(items :)中替換以下代碼行:
finalValue += Int(item)!
使用下面的代碼
if let intValue = Int(item) {
finalValue += intValue
}
上面的代碼在使用Int(item)
之前檢查其結(jié)果是否為nil
,以防崩潰。
通過單擊藍(lán)色箭頭禁用斷點(diǎn),它將變?yōu)榘胪该鞯乃{(lán)色。 在數(shù)字之后的文本字段中構(gòu)建并運(yùn)行并添加所需的任何類型的文本。
它不再崩潰,但是否已完全修復(fù)? 下面不添加數(shù)字,請(qǐng)刪除最后一個(gè),然后重試。
該應(yīng)用程序在58
行的ForceUnwrappingViewController.swift
中再次崩潰。
日志相關(guān)信息如下所示:
Could not cast value of type 'Swift.String' (0x7fff879c3f88) to 'Swift.Int' (0x7fff879c1e48).
崩潰行強(qiáng)制將結(jié)果強(qiáng)制轉(zhuǎn)換為Int
,而您提供的值是String
。 這意味著valueToShow
為nil
,當(dāng)您強(qiáng)制對(duì)其進(jìn)行拆包時(shí),該應(yīng)用程序崩潰,類似于上面已修復(fù)的崩潰。
僅當(dāng)總數(shù)大于100
時(shí),calculateSum(items :)
才會(huì)顯示總和。否則,消息應(yīng)為Sum is too low
。
這是一個(gè)簡(jiǎn)單的解決方法。 用以下代碼塊替換showResult(result :)中的代碼:
if let intValue = result as? Int {
sumLabel.text = "\(intValue)"
} else if let stringValue = result as? String {
sumLabel.text = stringValue
}
在這里,您檢查是否可以將result
強(qiáng)制轉(zhuǎn)換為Int
,然后創(chuàng)建其值的字符串并將其添加到label
中。 如果可以將其強(qiáng)制轉(zhuǎn)換為字符串,則按原樣使用該值。
構(gòu)建并運(yùn)行。 當(dāng)總數(shù)低于100
時(shí),您會(huì)看到錯(cuò)誤消息Sum is too low
。
Exhibit B: Weak Grip — Weak References
您要解決的第二個(gè)崩潰涉及一種不尋常的顯示和隱藏視圖的方法。
Weak References
屏幕是一個(gè)包含兩個(gè)步驟的簡(jiǎn)單表單,其中,只有第一個(gè)問題的答案為yes
時(shí),第二步才處于激活狀態(tài)。
注意:除了此應(yīng)用程序中顯示的方法以外,還有很多方法可以實(shí)現(xiàn)相同的結(jié)果。 目的是顯示導(dǎo)致崩潰的方案,而不是設(shè)計(jì)出能正常工作的表單。
當(dāng)您關(guān)閉開關(guān)時(shí),第二個(gè)問題消失了,但是當(dāng)您再次將其打開時(shí)…發(fā)生了崩潰。
該應(yīng)用程序在WeakReferencesViewController.swift
第37
行中崩潰了。
WeakReferencesViewController
具有三個(gè)項(xiàng)目:
- 1) 到
stackView
的IBOutlet
。 - 2) 第二個(gè)
QuestionView
的IBOutlet
。 - 3)
IBAction
到switchValueChanged(_ :)
,在其中您更改開關(guān)的值以刪除secondQuestionView
或?qū)⑵渲匦绿砑拥?code>stackView的底部。
有兩種方法可以弄清Xcode
為什么顯示nil
:從Variables View
中瀏覽值,或檢查從控制臺(tái)日志崩潰行中找到的兩個(gè)變量的值。
從調(diào)試器的輸出中可以看出,secondQuestionView
的值為nil
,但是為什么呢? 在switchValueChanged(_ :)
的第一行上添加一個(gè)斷點(diǎn),然后重新啟動(dòng)應(yīng)用程序以開始調(diào)查。
構(gòu)建并運(yùn)行。
當(dāng)您關(guān)閉開關(guān)時(shí),secondQuestionView
不會(huì)為nil
。但是,當(dāng)視圖消失后再次打開時(shí),它已經(jīng)為nil
。
1. Understanding the Crash
這樣做的原因是由于UIKit
中的引用鏈(reference chain)
。每個(gè)視圖都強(qiáng)引用(strong reference)
其中顯示的子視圖。只要secondQuestionView
在屏幕上的視圖層次結(jié)構(gòu)中,就會(huì)有對(duì)其的強(qiáng)引用。
因此,當(dāng)您從第二個(gè)ViewView的superview
中刪除了第二個(gè)QuestionView
時(shí),您就打破了連接。并查看secondQuestionView
的IBOutlet
定義,您會(huì)發(fā)現(xiàn)它被標(biāo)記為weak
。因此,它從內(nèi)存中釋放,并且其引用更改為nil
,因?yàn)闆]有人持有它以防止這樣做。
一旦從secondQuestionView
聲明中刪除了weak
關(guān)鍵字,崩潰將消失。您可以對(duì)stackView
進(jìn)行相同的操作,以防萬一,但是由于從不從父視圖中移除stackView
,因此它對(duì)崩潰沒有影響。
刪除weak
關(guān)鍵字,然后構(gòu)建并運(yùn)行以再次嘗試該場(chǎng)景。
您會(huì)看到該表格現(xiàn)在可以正常工作了。 該視圖出現(xiàn)并根據(jù)需要消失。
Exhibit C: Unexpected Updates — Invalid Table Updates
第三次崩潰與之前的崩潰略有不同。 更多的是數(shù)據(jù)不匹配。
在圖庫屏幕上打開第三個(gè)項(xiàng)目,稱為Invalid Table Updates
,現(xiàn)在開始研究下。
該屏幕具有一個(gè)包含四個(gè)單元格的表格視圖。 每個(gè)單元格上都有其編號(hào)。 右上角還有一個(gè)小按鈕,可以添加更多單元格。
繼續(xù)并按該按鈕。 如您所料,發(fā)生了崩潰。 但是...哪行崩潰了? 日志中有什么?
Xcode在第32
行的AppDelegate.swift
中停止。
將異常斷點(diǎn)添加到您的項(xiàng)目,然后構(gòu)建并運(yùn)行以查看差異。
這次,Xcode
在第37
行的InvalidTableUpdatesViewController.swift
中停止了。日志為空,并且沒有提供任何信息,因?yàn)閿帱c(diǎn)在異常發(fā)生之前就已停止。 與之前的崩潰相比,這是另一種崩潰。
當(dāng)您按繼續(xù)按鈕時(shí),Xcode
將返回到AppDelegate.swift
中的類聲明行,并且日志將包含崩潰信息。
日志包含有關(guān)崩潰的信息以及崩潰發(fā)生時(shí)的堆棧跟蹤(stack trace)
信息。 在大多數(shù)情況下,從Xcode進(jìn)行調(diào)試并啟用異常斷點(diǎn)時(shí),不需要堆棧跟蹤信息。 看一下崩潰信息。
*** 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
在檢查崩潰本身之前,您應(yīng)該了解addPressed()
的用途。這三行執(zhí)行以下操作:
- 1) 在
section 0
的最后一行之后創(chuàng)建一個(gè)IndexPath
對(duì)象。索引4
代表第五項(xiàng),因?yàn)樗饕龔?code>0開始。 - 2) 告訴
tableView
在newIndex
處插入新行。 - 3) 將新行添加到
itemsList
數(shù)據(jù)源數(shù)組。
首先,看一下流程:這是合理的,也是正確的。但是Xcode只是告訴您,事實(shí)并非如此。那有什么問題呢?
2. Narrowing Down the Problem
異常斷點(diǎn)在第二行停止,因此該應(yīng)用未將新行添加到itemsList
。此時(shí),這似乎是一個(gè)簡(jiǎn)單的解決方法-將新項(xiàng)目添加到itemsList
中,然后再將其插入tableView
中。它有助于更??多地了解導(dǎo)致崩潰的行。
確保已啟用異常斷點(diǎn),然后生成并運(yùn)行并再次打開同一屏幕。
打開InvalidTableUpdatesViewController.swift
并在導(dǎo)致崩潰的第37
行和第44
行(即tableView(_:numberOfRowsInSection :)
的返回)上添加斷點(diǎn)。按添加按鈕,使應(yīng)用程序在第一個(gè)斷點(diǎn)處停止,然后按繼續(xù)。現(xiàn)在,查看左側(cè)的調(diào)用堆棧:
請(qǐng)注意,insertRows(at:with :)
在內(nèi)部對(duì)tableView(_:numberOfRowsInSection :)
進(jìn)行了調(diào)用,以檢查itemsList
的新大小。 由于itemsList
尚未更新,因此tableView
找不到添加到其中的任何東西,這使其處于不一致狀態(tài)。
換句話說,您告訴tableView
有一個(gè)新項(xiàng)目,但是tableView
卻沒有發(fā)現(xiàn)itemsList
增長(zhǎng)了。
這證明了table view
的行為。 將代碼行添加到itemList
的其他兩行之間。 addPressed()
現(xiàn)在應(yīng)如下所示:
@IBAction func addPressed() {
let newIndex = IndexPath(row: itemsList.count, section: 0)
itemsList.append((itemsList.last ?? 0) + 1)
tableView.insertRows(at: [newIndex], with: .automatic)
}
這將在更新視圖之前更新數(shù)據(jù)源。 構(gòu)建并運(yùn)行,然后按添加按鈕以查看是否一切正常:
Assertions
斷言是手動(dòng)觸發(fā)的崩潰,您可以將其插入自己的代碼中。顯而易見的問題是:為什么要編寫代碼以使自己的應(yīng)用程序崩潰?
這是一個(gè)很好的問題。不管看起來多么不合邏輯,您都會(huì)立刻理解為什么這樣做會(huì)有所幫助。
想象一下,您正在編寫一段復(fù)雜的代碼,并且邏輯中有些流程沒有人可以到達(dá),因?yàn)榈竭_(dá)它們意味著發(fā)生了致命的錯(cuò)誤。
這些情況非常適合斷言。他們會(huì)幫助您或其他使用您代碼的人發(fā)現(xiàn)開發(fā)過程中無法正常工作。
1. Writing Your Own Reusable Code
編寫framework
框架也是斷言可能有用的一個(gè)很好的例子。如果其他開發(fā)人員向您的框架提供了不合理的輸入而效果卻不理想,則可以引起一個(gè)斷言。
ForceUnwrappingViewController.swift
中的一個(gè)方便示例。如果沒有將result
強(qiáng)制轉(zhuǎn)換為Int
或String
,則showResult(result :)
不會(huì)發(fā)生任何事情,并且使用您代碼的人都不會(huì)立即知道發(fā)生了什么。當(dāng)然,他們做錯(cuò)了什么,但是如果代碼足夠聰明地告訴他們什么,那豈不是很棒嗎?
要嘗試,請(qǐng)?jiān)?code>showResult(result :)的末尾添加以下代碼塊:
else {
assertionFailure("Only Int or Strings are accepted in this function")
}
如果result
不是Int
或String
,則會(huì)提出一個(gè)斷言。 在calculatePressed(_ :)
的末尾添加以下代碼行以查看其工作方式:
showResult(result: UIView())
在這里,您向showResult(result :)
發(fā)送了一個(gè)非常意外的值……一個(gè)UIView
!
構(gòu)建并運(yùn)行,打開Force Unwrapping
屏幕,然后按Calculate
按鈕。
您的應(yīng)用在第65
行的ForceUnwrappingViewController.swift
中崩潰了。
不出所料,崩潰行是斷言調(diào)用的地方,但您尚未完全回答問題。 如果開發(fā)人員無法涵蓋所有情況,崩潰的代碼是否應(yīng)該放在AppStore
的最終應(yīng)用中?
該問題的答案是:沒關(guān)系。
這些斷言確實(shí)存在于您的最終產(chǎn)品中,但好像根本就沒有。
斷言僅在您的應(yīng)用程序在調(diào)試debug
配置下構(gòu)建時(shí)起作用。 在發(fā)布release
配置下,斷言不會(huì)執(zhí)行任何操作,這是在AppStore上載應(yīng)用程序時(shí)將如何構(gòu)建它的方法。
想自己看看嗎? 您將在下一步中進(jìn)行嘗試。
2. Changing Your Build Configuration
單擊Xcode窗口左上角的CrashGallery
目標(biāo)以進(jìn)行嘗試。 從下拉菜單中選擇Edit Scheme
,然后從新窗口的左側(cè)選擇Run
,然后選擇Build Configuration
中的Release
。
構(gòu)建并運(yùn)行,然后再次按Calculate
按鈕。
沒有崩潰,沒有斷言。 它正常工作。 當(dāng)您的代碼獲得意外的值時(shí),它什么也不做,因此此步驟無效。
但也請(qǐng)注意,發(fā)行版release
配置并非用于調(diào)試。 您會(huì)發(fā)現(xiàn),在選擇Release
的情況下進(jìn)行調(diào)試時(shí),Xcode
的運(yùn)行情況不會(huì)達(dá)到預(yù)期。 它可能顯示執(zhí)行錯(cuò)誤的行,Variables View
可能不顯示任何值,或者控制臺(tái)日志可能不評(píng)估您打印的表達(dá)式。
如果要評(píng)估性能,而不是代碼跟蹤和調(diào)試,請(qǐng)使用此配置。
斷言是一個(gè)方便的工具,可以幫助您的開發(fā)人員或其他人在遺忘之前修復(fù)它們。 但是請(qǐng)不要過度使用它們,因?yàn)樗鼈儠?huì)變得煩人而不是幫助。
注意:使用preconditionFailure(_:file:line :)
或fatalError(_:file:line :)
而不是assertionFailure(_:file:line :)
可以使您的應(yīng)用在release
配置下崩潰。
后記
本篇主要講述了App Crash的調(diào)試和解決示例,感興趣的給個(gè)贊或者關(guān)注~~~