簡要概括: 良好的性能對于提供良好的用戶體驗至關(guān)重要,iOS 用戶通常對其應(yīng)用程序抱有很高的期望。緩慢且無響應(yīng)的應(yīng)用可能會讓用戶放棄使用你的應(yīng)用,或者更糟糕的是,對應(yīng)用留下差評。
雖然現(xiàn)代 iOS 硬件功能十分強大,足以處理許多密集和復(fù)雜的任務(wù),但是如果你不關(guān)心你的 APP 是怎么執(zhí)行的話,用戶的設(shè)備仍會出現(xiàn)無響應(yīng)的情況。在本文中,我們將研究五種優(yōu)化技巧,使你的 APP 更流暢。
1. 使用可復(fù)用的 tableViewCell
譯者注:本例闡述的是使用可復(fù)用的
tableViewCell,所以將所有cell翻譯成tableViewCell,table view 直譯成表視圖
你之前可能在 tableView(_:cellForRowAt:) 中使用了tableView.dequeueReusableCell(withIdentifier:for:)。但你有沒有想過為什么必須使用這個笨拙的 API,而不是只傳遞一個 TableViewCell 的數(shù)組?讓我們來看看為什么。
假設(shè)你有一個有一千行的表視圖。如果不使用可復(fù)用的 tableViewCell ,我們必須為每一行創(chuàng)建一個新的 tableViewCell,如下所示:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create a new cell whenever cellForRowAt is called.
let cell = UITableViewCell()
cell.textLabel?.text = "Cell \(indexPath.row)"
return cell
}
復(fù)制代碼
你可能已經(jīng)想到,當你滾動到底部時,這將為設(shè)備的內(nèi)存添加一千個 tableViewCell。想象一下如果每個 tableViewCell 都包含一個 UIImageView 和大量文本會發(fā)生什么:一次性加載它們可能會導(dǎo)致應(yīng)用內(nèi)存溢出!除此之外,每個 tableViewCell 在滾動期間都需要分配新內(nèi)存。如果你快速滾動表視圖,期間會動態(tài)分配許多小塊內(nèi)存,這個過程將使 UI 變得卡頓!
為了解決這個問題,Apple 為我們提供了 dequeueReusableCell(withIdentifier:for:) 方法。通過將屏幕上不再可見的 tableViewCell 放入隊列中進行復(fù)用,并且當新 tableViewCell 即將在屏幕上可見時(例如,當用戶向下滾動時,下面的后續(xù) tableViewCell),表視圖將從此隊列中檢索 tableViewCell 并在 cellForRowAt indexPath: 方法中修改它。

iOS 中 tableViewCell 復(fù)用隊列圖解(查看大圖)
通過使用隊列來存儲 tableViewCell,表視圖中不需要創(chuàng)建一千個 tableViewCell。反而,它只需要創(chuàng)建足夠覆蓋表視圖區(qū)域的 tableViewCell 就夠了。
通過使用 dequeueReusableCell 方法,我們可以減少應(yīng)用程序使用的內(nèi)存,并減少內(nèi)存溢出的可能性!
2. 使用看起來像應(yīng)用首頁的啟動頁
正如 Apple 人機界面指南 (HIG)里提到的, 啟動屏幕可用于增強對應(yīng)用程序響應(yīng)能力的感知:
「它僅用于增強你的應(yīng)用程序的感知,以便快速啟動并立即使用。每個應(yīng)用程序都必須提供啟動頁?!?/p>
將啟動頁用作啟動畫面以顯示品牌或添加加載動畫是一個常見的錯誤。如 Apple 所述,應(yīng)將啟動頁設(shè)計為與應(yīng)用的第一個頁面相同:
「設(shè)計一個與應(yīng)用程序首頁幾乎相同的啟動頁。如果你的應(yīng)用程序在完成啟動后包含著與啟動頁看起來不同的元素,那么用戶則可能會在啟動頁到應(yīng)用程序的第一個頁面的過程中感到令人不快的閃屏。」
「啟動頁并不是一個做品牌推廣的機會。避免將程序入口設(shè)計成類似啟動頁面或者“關(guān)于”頁面的感覺。不要包含徽標或其他品牌元素,除非它們是應(yīng)用程序第一個頁面的靜態(tài)部分?!?/p>
使用啟動頁進行加載或品牌化可能會減慢首次使用的時間,并使用戶感覺應(yīng)用程序運行緩慢。
當你新建 iOS 項目時,Xcode 會創(chuàng)建一個空白的 LaunchScreen.storyboard 供你使用。當應(yīng)用程序加載視圖控制器和布局時,將向用戶顯示此頁面。
譯者注:文段中沒有 Xcode,下文中提及為 Xcode 新建項目
為了讓你的應(yīng)用感覺更快,你可以將啟動頁設(shè)計為與將向用戶顯示的第一個頁面(視圖控制器)類似。
例如,Safari APP 的啟動頁與其第一個頁面類似:

比較:Safari APP的啟動頁和第一個頁面 (查看大圖)
啟動頁的 storyboard 與任何其他 storyboard 文件一樣,除了您只能使用標準的 UIKit 類,如 UIViewController,UITabBarController 和 UINavigationController。如果你嘗試使用任何其他自定義子類(例如 UserViewController),Xcode 將提示你禁止使用自定義類名。

啟動頁 storyboard 不能包含非 UIKit 標準類。(查看大圖)
另外需要注意的是,當 UIActivityIndicatorView 放置在啟動頁上時,不會生成動畫,因為 iOS 只會將啟動頁 storyboard 生成靜態(tài)圖像并將其展示給用戶。(這在 WWDC 2014 “Platforms State of the Union” 演示中簡要提到, 大概在 01:21:56。)
Apple 的人機界面指南還建議我們不要在啟動頁上包含文本,因為啟動頁是靜態(tài)的,應(yīng)用程序不能將文本本地化以適應(yīng)不同的語言。
推薦閱讀: 具有面部識別功能的移動應(yīng)用程序:如何實現(xiàn)
3. 視圖控制器的狀態(tài)恢復(fù)
視圖控制器的狀態(tài)保存和恢復(fù),允許用戶在離開應(yīng)用程序后可以返回到之前完全相同的用戶界面狀態(tài)。有時,由于內(nèi)存不足,操作系統(tǒng)可能需要在應(yīng)用程序處于后臺時從內(nèi)存中刪除應(yīng)用程序,如果不保留狀態(tài),應(yīng)用程序可能會丟失其對最后一個UI狀態(tài)的跟蹤,可能會導(dǎo)致用戶丟失正在進行的操作!
在多任務(wù)屏幕中,我們可以看到已放在后臺的應(yīng)用程序列表。我們可以假設(shè)這些應(yīng)用程序仍在后臺運行;實際上,由于內(nèi)存的需求,一些應(yīng)用程序可能會被系統(tǒng)殺死并重新啟動。我們在多任務(wù)視圖中看到的應(yīng)用程序快照實際上是系統(tǒng)在退出應(yīng)用程序時截取到的屏幕截圖。(即轉(zhuǎn)到主屏幕或多任務(wù)屏幕)。

用戶退出應(yīng)用程序時 iOS 截取的應(yīng)用程序截圖(查看大圖)
iOS 使用這些屏幕截圖來給人一種假象,即應(yīng)用程序仍在運行或仍在顯示此特定視圖,而應(yīng)用程序可能已被后臺終止或重新啟動,但此時仍顯示相同的屏幕截圖。
您是否曾體驗過,從多任務(wù)屏幕恢復(fù)應(yīng)用程序后,該應(yīng)用程序顯示的用戶界面與多任務(wù)視圖中顯示的快照有什么不一樣? 這是因為應(yīng)用程序沒有實現(xiàn)狀態(tài)恢復(fù)機制,當應(yīng)用程序在后臺被殺死時,顯示的數(shù)據(jù)丟失。這可能會導(dǎo)致糟糕的體驗,因為用戶希望你的應(yīng)用程序與離開時處于相同的狀態(tài)。
在 Apple 的 保留你應(yīng)用程序的 UI 文章中提及:
「用戶希望你的應(yīng)用程序與他們離開時處于同一狀態(tài)。狀態(tài)保存和恢復(fù)可確保應(yīng)用程序在再次啟動時恢復(fù)到以前的狀態(tài)?!?/p>
UIKit 為簡化狀態(tài)保護和恢復(fù)做了很多工作:它可以在適當?shù)臅r間自動處理應(yīng)用程序狀態(tài)的保存和加載。我們需要做的就是添加一些配置來告訴應(yīng)用程序支持狀態(tài)保存和恢復(fù),以及告訴應(yīng)用程序需要保存哪些數(shù)據(jù)。
為了實現(xiàn)狀態(tài)保存和恢復(fù),我們可以在 AppDelegate.swift 中實現(xiàn)下面兩個方法:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
復(fù)制代碼
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
復(fù)制代碼
這將告訴應(yīng)用程序自動保存和恢復(fù)應(yīng)用程序的狀態(tài)。
接下來,我們將告訴應(yīng)用程序需要保留哪些視圖控制器。我們通過在 storyboard 中指定 restoration ID 來實現(xiàn)這一點:

在 storyboard 中設(shè)置 restoration ID (查看大圖)
你也可以選中 Use Storyboard ID 以使用 storyboard ID 作為 restoration ID。
如果要在代碼中設(shè)置 restoration ID,我們可以使用視圖控制器的 restorationIdentifier 屬性。
// ViewController.swift
self.restorationIdentifier = "MainVC"
復(fù)制代碼
在狀態(tài)保留期間,所有被分配了恢復(fù)標識符的視圖控制器或視圖都會將其狀態(tài)保存到磁盤。
可以將恢復(fù)標識符組合在一起以形成恢復(fù)路徑。標識符是通過視圖層次結(jié)構(gòu)來分組的,從根視圖控制器到當前活動視圖控制器。 假設(shè) MyViewController 嵌入在 navigation 控制器中,navigation 控制器嵌入在另一個 tabbar 控制器中。假設(shè)他們使用自己的類名作為恢復(fù)標識符,恢復(fù)路徑將如下所示:
TabBarController/NavigationController/MyViewController
復(fù)制代碼
當用戶將 MyViewController 作為活動視圖控制器并離開應(yīng)用程序時,該路徑將會被應(yīng)用程序保存; 那么應(yīng)用程序?qū)⒂涀∫郧暗囊晥D層次結(jié)構(gòu)即(Tab Bar Controller → Navigation Controller → My View Controller)。
在分配了恢復(fù)標識符之后,我們需要在每個保留的視圖控制器里實現(xiàn) encodeRestorableState(with coder:) 和 decodeRestorableState(with coder:) 方法。這兩種方法讓我們指定需要保存或加載的數(shù)據(jù)以及如何對它們進行編碼或解碼。
我們來看看視圖控制器里如何實現(xiàn):
// MyViewController.swift
// MARK: State restoration
// UIViewController already conforms to UIStateRestoring protocol by default
extension MyViewController {
// will be called during state preservation
override func encodeRestorableState(with coder: NSCoder) {
// encode the data you want to save during state preservation
coder.encode(self.username, forKey: "username")
super.encodeRestorableState(with: coder)
}
// will be called during state restoration
override func decodeRestorableState(with coder: NSCoder) {
// decode the data saved and load it during state restoration
if let restoredUsername = coder.decodeObject(forKey: "username") as? String {
self.username = restoredUsername
}
super.decodeRestorableState(with: coder)
}
}
復(fù)制代碼
記得在自己的方法底部調(diào)用父類實現(xiàn)。這樣可確保父類有機會保存和恢復(fù)狀態(tài)。
一旦指定保存的對象解碼完成,applicationFinishedRestoringState() 將被調(diào)用以告訴視圖控制器狀態(tài)已被恢復(fù)。我們可以在此方法中更新視圖控制器的 UI。
// MyViewController.swift
// MARK: State restoration
// UIViewController already conforms to UIStateRestoring protocol by default
extension MyViewController {
...
override func applicationFinishedRestoringState() {
// update the UI here
self.usernameLabel.text = self.username
}
}
復(fù)制代碼
這些,就是為你的應(yīng)用程序?qū)崿F(xiàn)狀態(tài)保存和恢復(fù)的基本方法了!請記住,當應(yīng)用程序被用戶強行關(guān)閉時,操作系統(tǒng)將刪除已保存的狀態(tài),避免在狀態(tài)保存和恢復(fù)時出現(xiàn)問題。
此外,請勿將任何模型數(shù)據(jù)(即應(yīng)保存到 UserDefaults 或 Core Data 的數(shù)據(jù))存儲到該狀態(tài),即使這樣做似乎很方便。當用戶強制退出你的應(yīng)用程序時,狀態(tài)數(shù)據(jù)將被刪除,你當然不希望以這種方式丟失模型數(shù)據(jù)。
要測試狀態(tài)保存和恢復(fù)是否正常,請按照以下步驟操作:
- 使用Xcode構(gòu)建和啟動應(yīng)用程序。
- 跳轉(zhuǎn)到要測試狀態(tài)保留和恢復(fù)的頁面。
- 返回主屏幕 (通過向上滑動或雙擊
home按鈕,或者在用模擬器時鍵入Shift ?+Cmd ?+H) 將應(yīng)用程序發(fā)送到后臺。 - 通過在Xcode中點擊 ? 按鈕,停止程序運行。
- 再次啟動應(yīng)用程序并檢查狀態(tài)是否已成功還原。
由于本節(jié)僅涵蓋了狀態(tài)保存和恢復(fù)的基礎(chǔ)知識,因此我推薦 Apple Inc. 上的以下文章。了解更多有關(guān)狀態(tài)恢復(fù)的知識:
4. 盡可能減少透明視圖的使用
不透明視圖是指沒有透明度的視圖,意味著放在它后面的任何 UI 元素不可見。我們可以在 Interface Builder 中將視圖設(shè)置為不透明:

在 storyboard 中將 UIView 設(shè)置為不透明(查看大圖)
或者我們可以在代碼中修改 UIView 的 isOpaque 屬性:
view.isOpaque = true
復(fù)制代碼
將視圖設(shè)置為不透明將使繪圖系統(tǒng)在渲染屏幕時優(yōu)化一些繪圖性能。
如果視圖具有透明度(即 alpha 低于 1.0),那么 iOS 將需要做些額外的工作來混合視圖層次結(jié)構(gòu)中不同的視圖層以計算出哪些內(nèi)容需要展示。另一方面,如果視圖設(shè)置為不透明,則繪圖系統(tǒng)僅會將此視圖放在前面,并避免在其后面混合多個視圖層的額外工作。
您可以在 iOS 模擬器中通過 Debug → Color Blended Layers 來檢查哪些(透明)圖層正在混合。

在 Simulator 中顯示各種圖層的顏色
當選擇 Color Blended Layers 選項后,你可以看到一些視圖是紅色的,一些是綠色的。 紅色表示視圖不是不透明的,并且其顯示的是在其后面混合的圖層。綠色表示視圖不透明且未進行混合。

盡可能為 UILabel 指定非透明背景顏色以減少顏色混合圖層。(查看大圖)
上面顯示的所有 label(“查看朋友”等)被紅色突出顯示,是因為當 label 被拖動到 storyboard 時,其背景顏色默認設(shè)置為透明。當繪圖系統(tǒng)在 label 區(qū)域附近的進行繪制時,它將詢問 label 后面的圖層并進行一些計算。
優(yōu)化應(yīng)用性能的方法是盡可能減少用紅色突出顯示的視圖數(shù)量。
通過將 label 顏色從 label.backgroundColor = UIColor.clear 修改成 label.backgroundColor = UIColor.white,我們可以減少 label 和它后面的視圖層之間的圖層混合。

許多 label 以紅色突出顯示,因為它們的背景顏色是透明的,導(dǎo)致 iOS 通過混合背后的視圖來計算背景顏色。 (查看大圖)
你可能已經(jīng)注意到,即使你已將 UIImageView 設(shè)置為不透明并為其指定了背景顏色,模擬器仍將在 imageView 上顯示紅色。 這可能是因為你用于 imageView 的圖像具有Alpha通道。
要刪除圖像的 Alpha 通道,可以使用預(yù)覽應(yīng)用程序復(fù)制圖像(Shift? + Cmd?+ S),并在保存時取消選中 Alpha 復(fù)選框。

保存圖像時,取消選中 Alpha 復(fù)選框以取消 Alpha 通道。 (查看大圖)
5. 在后臺線程中處理繁重的功能(GCD)
因為 UIKit 僅適用于主線程,所以在主線程上執(zhí)行繁重的處理工作會降低 UI 的速度。主線程使用 UIKit 不僅要處理和響應(yīng)用戶的交互,還需要繪制屏幕。
譯者注: 將touch input 翻譯成交互,是因為點擊和輸入屬于交互范疇
使應(yīng)用程序保持響應(yīng)的關(guān)鍵是盡可能多的將繁重處理任務(wù)放到后臺線程。應(yīng)當盡量避免在主線程上執(zhí)行復(fù)雜的計算,網(wǎng)絡(luò)和繁重的IO操作(例如,磁盤的讀取和寫入)。
你可能曾經(jīng)使用過突然對你的操作停止響應(yīng)的應(yīng)用程序,就好像應(yīng)用程序已掛起。這很可能是因為應(yīng)用程序在主線程上運行繁重的計算任務(wù)。
主線程中通常在 UIKit 任務(wù)(如處理用戶輸入)和一些間隔很小的輕量級任務(wù)之間交替。如果在主線程上運行繁重的任務(wù),那么 UIKit 需要等到繁重的任務(wù)完成以后才能處理用戶交互。

這是主線程處理 UI 任務(wù)的方式以及在執(zhí)行繁重任務(wù)時導(dǎo)致 UI 掛起的原因。(查看大圖)
默認情況下,視圖控制器生命周期方法(如 viewDidLoad)和 IBOutlet 相關(guān)方法是在主線程上執(zhí)行。 要將繁重的處理任務(wù)移到后臺線程,我們可以使用Apple提供的 Grand Central Dispatch 隊列。
以下是切換隊列的例子:
// Switch to background thread to perform heavy task.
DispatchQueue.global(qos: .default).async {
// Perform heavy task here.
// Switch back to main thread to perform UI-related task.
DispatchQueue.main.async {
// Update UI.
}
}
復(fù)制代碼
qos 代表著「quality of service」。不同的 QoS 值表示任務(wù)不同的優(yōu)先級。對于在具有較高 QoS 值的隊列中分配的任務(wù),操作系統(tǒng)將分配更多的 CPU 時間、CPU 功率和 I/O 吞吐量,這意味著任務(wù)將在具有更高QoS值的隊列中更快地完成。較高的 QoS 值也會因使用更多資源而消耗更多能量。
以下是從最高優(yōu)先級到最低優(yōu)先級的 QoS 值列表:

按性能和能效排序的 QoS 值 (查看大圖)
Apple 提供了 一個簡單的表格 其中包含用于不同任務(wù)的 QoS 值的示例。
需要記住,所有 UIKit 代碼始終都應(yīng)該在主線程上執(zhí)行。在后臺線程上修改 UIKit 對象(例如 UILabel 和 UIImageView)可能會產(chǎn)生意想不到的后果,例如UI實際上沒有更新,發(fā)生崩潰等等。
在 Apple 的 主線程檢查器 文章中提及:
「在主線程以外的線程上更新 UI 是一種常見錯誤,這可能導(dǎo)致 UI 不更新,視覺缺陷,數(shù)據(jù)損壞以及崩潰。」
我建議觀看 Apple 的 WWDC 2012 視頻上的 UI 并發(fā),以便更好地了解如何構(gòu)建響應(yīng)式應(yīng)用。
后記
性能優(yōu)化需要你在應(yīng)用程序的功能之上編寫更多的代碼或配置其他設(shè)置。這可能會使您的應(yīng)用程序交付時間超出預(yù)期,并且您將來會有更多代碼需要維護,而更多代碼意味著更多潛在的bug。
在花時間優(yōu)化應(yīng)用之前,先問問自己應(yīng)用是否已經(jīng)流暢,或者是否有一些真正需要優(yōu)化的無響應(yīng)的部分?;ㄙM大量時間優(yōu)化已經(jīng)很流暢的應(yīng)用程序來減少 0.01 秒的耗時是不值得的,最好將這些時間花在開發(fā)更好的功能或優(yōu)先級更高的任務(wù)。
- 原文地址:iOS Performance Tricks To Make Your App Feel More Performant
- 原文作者:Axel
- 譯文出自:掘金翻譯計劃
- 本文永久鏈接:github.com/xitu/gold-m…
- 譯者:LoneyIsError
- 校對者:EdmondWang