iOS 性能技巧讓您的應用感覺更高效

已發表: 2022-03-10
快速總結 ↬良好的性能對於提供良好的用戶體驗至關重要,iOS 用戶通常對他們的應用程序抱有很高的期望。 緩慢且無響應的應用程序可能會讓用戶放棄使用您的應用程序,或者更糟糕的是,留下差評。

儘管現代 iOS 硬件足夠強大,可以處理許多密集和復雜的任務,但如果您不注意應用程序的執行方式,設備仍然可能會感到無響應。 在本文中,我們將研究五種優化技巧,讓您的應用程序感覺更靈敏。

1.出列可重用單元格

您之前可能在tableView(_:cellForRowAt:)中使用過tableView.dequeueReusableCell(withIdentifier:for:) 。 有沒有想過為什麼你必須遵循這個尷尬的 API,而不是僅僅傳入一個單元格數組? 讓我們來看看這個推理。

假設您有一個包含一千行的表格視圖。 如果不使用可重複使用的單元格,我們將不得不為每一行創建一個新單元格,如下所示:

 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 }

正如您可能已經想到的那樣,當您滾動到底部時,這將在設備的內存中添加一千個單元格。 想像一下,如果每個單元格都包含一個UIImageView和大量文本會發生什麼:一次加載它們可能會導致應用程序內存不足! 除此之外,每個單元格都需要在滾動期間分配新的內存。 如果你快速滾動一個表格視圖,大量的小塊內存將被動態分配,這個過程會使 UI 卡頓!

為了解決這個問題,Apple 為我們提供了dequeueReusableCell(withIdentifier:for:)方法。 單元重用的工作原理是將屏幕上不再可見的單元放入隊列中,當一個新單元即將在屏幕上可見時(例如,當用戶向下滾動時,下面的後續單元),表格視圖將從此隊列中檢索一個單元格並在cellForRowAt indexPath:方法中對其進行修改。

信元重用隊列機制
單元重用隊列在 iOS 中的工作原理(大預覽)

通過使用隊列來存儲單元格,表格視圖不需要創建一千個單元格。 相反,它只需要足夠的單元格來覆蓋表格視圖的區域。

通過使用dequeueReusableCell ,我們可以減少應用程序使用的內存,並使其不太容易耗盡內存!

跳躍後更多! 繼續往下看↓

2.使用看起來像初始屏幕的啟動屏幕

正如 Apple 的人機界面指南 (HIG) 中所述,啟動屏幕可用於增強對應用響應能力的感知:

“它的唯一目的是增強您的應用程序快速啟動和立即可用的感覺。 每個應用程序都必須提供一個啟動屏幕。”

使用啟動屏幕作為啟動屏幕來顯示品牌或添加加載動畫是一個常見的錯誤。 正如 Apple 所說,將啟動屏幕設計為與應用程序的第一個屏幕相同:

“設計一個與應用程序的第一個屏幕幾乎相同的啟動屏幕。 如果您包含在應用程序完成啟動時看起來不同的元素,人們可能會在啟動屏幕和應用程序的第一個屏幕之間體驗到令人不快的閃光。

“啟動屏幕不是品牌推廣機會。 不要設計看起來像閃屏或“關於”窗口的輸入體驗。 不要包含徽標或其他品牌元素,除非它們是您應用首屏的靜態部分。”

使用啟動屏幕進行加載或品牌推廣可能會減慢首次使用的時間,並使用戶感覺應用程序運行緩慢。

當您開始一個新的 iOS 項目時,將創建一個空白的LaunchScreen.storyboard 。 當應用程序加載視圖控制器和佈局時,該屏幕將顯示給用戶。

為了讓您的應用程序感覺更快,您可以將啟動屏幕設計為類似於將顯示給用戶的第一個屏幕(視圖控制器)。

例如,Safari 應用程序的啟動屏幕類似於它的第一個視圖:

啟動屏幕和第一個視圖看起來相似
啟動屏幕和 Safari 應用程序的第一個視圖的比較(大預覽)

啟動屏幕故事板與任何其他故事板文件一樣,只是您只能使用標準的 UIKit 類,如 UIViewController、UITabBarController 和 UINavigationController。 如果您嘗試使用任何其他自定義子類(例如 UserViewController),Xcode 會通知您禁止使用自定義類名。

使用自定義類時 Xcode 顯示錯誤
啟動屏幕故事板不能包含非 UIKit 標準類。 (大預覽)

另一個需要注意的是UIActivityIndicatorView放在啟動屏幕上時不會動畫,因為 iOS 會從啟動屏幕故事板生成靜態圖像並將其顯示給用戶。 (這在 WWDC 2014 演講“Platforms State of the Union”中簡要提及,大約01:21:56 。)

Apple 的 HIG 還建議我們不要在啟動屏幕上包含文本,因為啟動屏幕是靜態的,您無法本地化文本以適應不同的語言。

推薦閱讀具有面部識別功能的移動應用程序:如何使其成為現實

3. 視圖控制器的狀態恢復

狀態保存和恢復允許用戶從剛剛離開應用程序之前返回到完全相同的 UI 狀態。 有時,由於內存不足,操作系統可能需要在應用處於後台時從內存中刪除您的應用,如果不保留,應用可能會丟失其最後的 UI 狀態,從而可能導致用戶丟失工作進行中!

在多任務屏幕中,我們可以看到已置於後台的應用程序列表。 我們可能會假設這些應用程序仍在後台運行; 實際上,由於內存需求,其中一些應用程序可能會被系統殺死並重新啟動。 我們在多任務視圖中看到的應用程序快照實際上是系統在我們退出應用程序時(即進入主屏幕或多任務屏幕)從右側截取的屏幕截圖。

iOS 通過截取最新視圖的屏幕截圖來製造應用程序在後台運行的錯覺
iOS 用戶退出應用時的應用截圖(大預覽)

iOS 使用這些屏幕截圖給人一種應用程序仍在運行或仍在顯示此特定視圖的錯覺,而應用程序可能已經在後台終止或重新啟動,但仍顯示相同的屏幕截圖。

在從多任務屏幕恢復應用程序時,您是否體驗過該應用程序顯示的用戶界面與多任務視圖中顯示的快照不同? 這是因為應用程序沒有實現狀態恢復機制,在後台殺死應用程序時顯示的數據丟失了。 這可能會導致糟糕的體驗,因為用戶希望您的應用處於與離開時相同的狀態。

來自蘋果的文章:

“他們希望您的應用程序與他們離開時的狀態相同。 狀態保存和恢復可確保您的應用在再次啟動時恢復到之前的狀態。”

UIKit 為我們簡化狀態保存和恢復做了很多工作:它在適當的時候自動處理應用程序狀態的保存和加載。 我們需要做的就是添加一些配置來告訴應用程序支持狀態保存和恢復,並告訴應用程序需要保留哪些數據。

要啟用狀態保存和恢復,我們可以在AppDelegate.swift中實現這兩個方法:

 func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
 func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }

這將告訴應用程序自動保存和恢復應用程序的狀態。

接下來,我們將告訴應用程序需要保留哪些視圖控制器。 我們通過在情節提要中指定“恢復 ID”來做到這一點:

在情節提要中設置恢復 ID
在情節提要中設置恢復 ID(大預覽)

您還可以選中“使用 Storyboard ID”以使用 Storyboard ID 作為恢復 ID。

要在代碼中設置恢復 ID,我們可以使用視圖控制器的restorationIdentifier屬性。

 // ViewController.swift self.restorationIdentifier = "MainVC"

在狀態保存期間,任何已分配恢復標識符的視圖控制器或視圖都將其狀態保存到磁盤。

恢復標識符可以組合在一起以形成恢復路徑。 標識符使用視圖層次結構進行分組,從根視圖控制器到當前活動視圖控制器。 假設 MyViewController 嵌入在導航控制器中,該控制器嵌入在另一個選項卡欄控制器中。 假設他們使用自己的類名作為恢復標識符,恢復路徑將如下所示:

 TabBarController/NavigationController/MyViewController

當用戶以 MyViewController 作為活動視圖控制器離開應用程序時,此路徑將由應用程序保存; 那麼應用程序將記住之前顯示的視圖層次結構( Tab Bar ControllerNavigation ControllerMy View Controller )。

分配恢復標識符後,我們需要為每個保留的視圖控制器實現 encodeRestorableState(with coder:) 和 decodeRestorableState(with coder:) 方法。 這兩種方法讓我們指定需要保存或加載哪些數據以及如何對它們進行編碼或解碼。

讓我們看看視圖控制器:

 // 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) } } // 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) } }

請記住在您自己的方法的底部調用超類實現。 這確保了父類有機會保存和恢復狀態。

一旦對象完成解碼,將調用applicationFinishedRestoringState()來告訴視圖控制器狀態已恢復。 我們可以在這個方法中更新視圖控制器的 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 } } // 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 } }

你有它! 這些是為您的應用程序實現狀態保存和恢復的基本方法。 請記住,當應用程序被用戶強制關閉時,操作系統會刪除保存的狀態,以免在狀態保存和恢復出現問題時卡在損壞狀態。

此外,不要將任何模型數據(即應該保存到 UserDefaults 或 Core Data 的數據)存儲到狀態中,即使這樣做看起來很方便。 當用戶強制退出您的應用程序時,狀態數據將被刪除,您當然不希望以這種方式丟失模型數據。

要測試狀態保存和恢復是否正常工作,請按照以下步驟操作:

  1. 使用 Xcode 構建和啟動應用程序。
  2. 導航到您要測試的狀態保存和恢復屏幕。
  3. 返回主屏幕(通過向上滑動或雙擊主頁按鈕,或在模擬器中按Shift ⇧ + Cmd ⌘ + H )將應用程序發送到後台。
  4. 通過按下按鈕停止 Xcode 中的應用程序。
  5. 再次啟動應用,查看狀態是否恢復成功。

因為本節只涉及狀態保存和恢復的基礎知識,所以我推薦蘋果公司的以下文章,以獲得更深入的狀態恢復知識:

  1. 保存和恢復狀態
  2. UI 保存過程
  3. UI 恢復過程

4. 盡可能減少非透明視圖的使用

不透明視圖是沒有透明度的視圖,這意味著放置在其後面的任何 UI 元素根本不可見。 我們可以在 Interface Builder 中將視圖設置為不透明:

這將通知繪圖系統跳過繪製此視圖後面的任何內容
在情節提要中將 UIView 設置為不透明(大預覽)

或者我們可以使用 UIView 的isOpaque屬性以編程方式完成:

 view.isOpaque = true

將視圖設置為不透明將使繪圖系統在渲染屏幕時優化一些繪圖性能。

如果視圖具有透明度(即 alpha 低於 1.0),那麼 iOS 將不得不通過在視圖層次結構中混合不同的視圖層來計算應顯示的內容。 另一方面,如果一個視圖被設置為不透明,那麼繪圖系統只會把這個視圖放在前面,避免混合多個視圖層在它後面的額外工作。

您可以通過檢查DebugColor Blended Layers來檢查 iOS 模擬器中哪些圖層正在混合(非透明)。

綠色是非顏色混合,紅色是混合層
在模擬器中顯示顏色混合層

在檢查了顏色混合圖層選項後,您可以看到一些視圖是紅色的,一些是綠色的。 紅色表示視圖不是不透明的,並且其輸出顯示是在其後面混合圖層的結果。 綠色表示視圖是不透明的並且沒有進行混合。

使用不透明的顏色背景,該圖層不需要與另一圖層混合
盡可能為 UILabel 分配不透明的背景顏色,以減少顏色混合層。 (大預覽)

上面顯示的標籤(“查看朋友”等)以紅色突出顯示,因為當標籤被拖到情節提要時,其背景顏色默認設置為透明。 當繪圖系統在標籤區域附近合成顯示時,它會詢問標籤後面的圖層並進行一些計算。

優化應用程序性能的一種方法是盡可能減少以紅色突出顯示的視圖數量。

通過將label.backgroundColor = UIColor.clear更改為label.backgroundColor = UIColor.white ,我們可以減少標籤與其後面的視圖層之間的層混合。

使用透明背景顏色會導致圖層混合
許多標籤以紅色突出顯示,因為它們的背景顏色是透明的,導致 iOS 通過混合其後面的視圖來計算背景顏色。 (大預覽)

您可能已經註意到,即使您將 UIImageView 設置為不透明並為其分配了背景色,模擬器仍會在圖像視圖中顯示紅色。 這可能是因為您用於圖像視圖的圖像具有 Alpha 通道。

要刪除圖像的 Alpha 通道,您可以使用 Preview 應用程序複製圖像( Shift ⇧ + Cmd ⌘ + S ),並在保存時取消選中“Alpha”複選框。

保存圖像時取消選中“Alpha”複選框以丟棄 Alpha 通道。
保存圖像時取消選中“Alpha”複選框以丟棄 Alpha 通道。 (大預覽)

5. 將繁重的處理函數傳遞給後台線程 (GCD)

因為 UIKit 只在主線程上工作,所以在主線程上執行繁重的處理會減慢 UI。 UIKit 使用主線程不僅處理和響應用戶輸入,還繪製屏幕。

使應用程序響應的關鍵是將盡可能多的繁重處理任務轉移到後台線程。 避免在主線程上進行複雜的計算、聯網和繁重的 IO 操作(例如讀寫磁盤)。

您可能曾經使用過突然對您的觸摸輸入無響應的應用程序,並且感覺該應用程序已掛起。 這很可能是由於應用程序在主線程上運行繁重的計算任務造成的。

主線程通常會在 UIKit 任務(例如處理用戶輸入)和一些輕量級任務之間以較小的間隔交替進行。 如果一個繁重的任務正在主線程上運行,那麼 UIKit 將需要等到繁重的任務完成後才能處理觸摸輸入。

避免在主線程上運行性能密集或耗時的任務
以下是主線程如何處理 UI 任務以及為什麼在執行繁重的任務時它會導致 UI 掛起。 (大預覽)

默認情況下,視圖控制器生命週期方法(例如 viewDidLoad)和 IBOutlet 函數中的代碼在主線程上執行。 要將繁重的處理任務轉移到後台線程,我們可以使用 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. } }

qos代表“服務質量”。 不同的服務質量值表示指定任務的不同優先級。 操作系統將為分配在具有較高 QoS 值的隊列中的任務分配更多的 CPU 時間和 CPU 功率 I/O 吞吐量,這意味著任務將在具有較高 QoS 值的隊列中更快地完成。 較高的 QoS 值也將消耗更多的能量,因為它使用更多的資源。

以下是從最高優先級到最低優先級的 QoS 值列表:

按性能和能效排序的隊列服務質量值
按性能和能效排序的隊列的服務質量值(大預覽)

Apple 提供了一個方便的表格,其中包含用於不同任務的 QoS 值的示例。

要記住的一件事是所有 UIKit 代碼都應該始終在主線程上執行。 在後台線程上修改 UIKit 對象(例如UILabelUIImageView )可能會產生意想不到的後果,例如 UI 沒有實際更新、發生崩潰等。

來自蘋果的文章:

“在主線程以外的線程上更新 UI 是一個常見錯誤,可能導致錯過 UI 更新、視覺缺陷、數據損壞和崩潰。”

我建議觀看 Apple 關於 UI 並發的 WWDC 2012 視頻,以更好地了解如何構建響應式應用程序。

筆記

性能優化的權衡是您必須在應用程序的功能之上編寫更多代碼或配置其他設置。 這可能會使您的應用程序交付晚於預期,並且您將來將有更多代碼需要維護,而更多代碼意味著潛在的更多錯誤。

在花時間優化你的應用程序之前,問問自己應用程序是否已經很流暢,或者它是否有一些反應遲鈍的部分確實需要優化。 花大量時間優化已經流暢的應用程序以減少 0.01 秒可能不值得,因為時間可以更好地用於開發更好的功能或其他優先事項。

更多資源

  • “A Suite of Delicious iOS Eye Candy”,Tim Oliver,2018 年東京 iOS 聚會(視頻)
  • “在 iOS 上構建並髮用戶界面”,Andy Matuschak,WWDC 2012(視頻)
  • “在啟動過程中保留應用程序的 UI”,Apple
  • “並發編程指南:調度隊列”,文檔存檔,Apple
  • “主線程檢查器”,Apple