[譯] 用這些 iOS 技巧讓你的 APP 效能更佳

little_xia發表於2019-02-18

簡要概括: 良好的效能對於提供良好的使用者體驗至關重要,iOS 使用者通常對其應用程式抱有很高的期望。緩慢且無響應的應用可能會讓使用者放棄使用你的應用,或者更糟糕的是,對應用留下差評。

雖然現代 iOS 硬體功能十分強大,足以處理許多密集和複雜的任務,但是如果你不關心你的 APP 是怎麼執行的話,使用者的裝置仍會出現無響應的情況。在本文中,我們將研究五種優化技巧,使你的 APP 更流暢。

1. 使用可複用的 tableViewCell

譯者注:本例闡述的是使用可複用的 tableViewCell,所以將所有 cell 翻譯成 tableViewCell ,table view 直譯成表檢視

你之前可能在 tableView(_:cellForRowAt:) 中使用了tableView.dequeueReusableCell(withIdentifier:for:)。但你有沒有想過為什麼必須使用這個笨拙的 API,而不是隻傳遞一個 TableViewCell 的陣列?讓我們來看看為什麼。

假設你有一個有一千行的表檢視。如果不使用可複用的 tableViewCell ,我們必須為每一行建立一個新的 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
}
複製程式碼

你可能已經想到,當你滾動到底部時,這將為裝置的記憶體新增一千個 tableViewCell。想象一下如果每個 tableViewCell 都包含一個 UIImageView 和大量文字會發生什麼:一次性載入它們可能會導致應用記憶體溢位!除此之外,每個 tableViewCell 在滾動期間都需要分配新記憶體。如果你快速滾動表檢視,期間會動態分配許多小塊記憶體,這個過程將使 UI 變得卡頓!

為了解決這個問題,Apple 為我們提供了 dequeueReusableCell(withIdentifier:for:) 方法。通過將螢幕上不再可見的 tableViewCell 放入佇列中進行復用,並且當新 tableViewCell 即將在螢幕上可見時(例如,當使用者向下滾動時,下面的後續 tableViewCell),表檢視將從此佇列中檢索 tableViewCell 並在 cellForRowAt indexPath: 方法中修改它。

Cell reuse queue mechanism

iOS 中 tableViewCell 複用佇列圖解(檢視大圖)

通過使用佇列來儲存 tableViewCell,表檢視中不需要建立一千個 tableViewCell。反而,它只需要建立足夠覆蓋表檢視區域的 tableViewCell 就夠了。

通過使用 dequeueReusableCell 方法,我們可以減少應用程式使用的記憶體,並減少記憶體溢位的可能性!

2. 使用看起來像應用首頁的啟動頁

正如 Apple 人機介面指南 (HIG)裡提到的, 啟動螢幕可用於增強對應用程式響應能力的感知:

「它僅用於增強你的應用程式的感知,以便快速啟動並立即使用。每個應用程式都必須提供啟動頁。」

將啟動頁用作啟動畫面以顯示品牌或新增載入動畫是一個常見的錯誤。如 Apple 所述,應將啟動頁設計為與應用的第一個頁面相同:

「設計一個與應用程式首頁幾乎相同的啟動頁。如果你的應用程式在完成啟動後包含著與啟動頁看起來不同的元素,那麼使用者則可能會在啟動頁到應用程式的第一個頁面的過程中感到令人不快的閃屏。」

「啟動頁並不是一個做品牌推廣的機會。避免將程式入口設計成類似啟動頁面或者“關於”頁面的感覺。不要包含徽標或其他品牌元素,除非它們是應用程式第一個頁面的靜態部分。」

使用啟動頁進行載入或品牌化可能會減慢首次使用的時間,並使使用者感覺應用程式執行緩慢。

當你新建 iOS 專案時,Xcode 會建立一個空白的 LaunchScreen.storyboard 供你使用。當應用程式載入檢視控制器和佈局時,將向使用者顯示此頁面。

譯者注:文段中沒有 Xcode,下文中提及為 Xcode 新建專案

為了讓你的應用感覺更快,你可以將啟動頁設計為與將向使用者顯示的第一個頁面(檢視控制器)類似。

例如,Safari APP 的啟動頁與其第一個頁面類似:

Launch screen and first view look similar

比較:Safari APP的啟動頁和第一個頁面 (檢視大圖)

啟動頁的 storyboard 與任何其他 storyboard 檔案一樣,除了您只能使用標準的 UIKit 類,如 UIViewControllerUITabBarControllerUINavigationController。如果你嘗試使用任何其他自定義子類(例如 UserViewController),Xcode 將提示你禁止使用自定義類名。

Xcode shows error when a custom class is used

啟動頁 storyboard 不能包含非 UIKit 標準類。(檢視大圖)

另外需要注意的是,當 UIActivityIndicatorView 放置在啟動頁上時,不會生成動畫,因為 iOS 只會將啟動頁 storyboard 生成靜態影象並將其展示給使用者。(這在 WWDC 2014 “Platforms State of the Union” 演示中簡要提到, 大概在 01:21:56。)

Apple 的人機介面指南還建議我們不要在啟動頁上包含文字,因為啟動頁是靜態的,應用程式不能將文字本地化以適應不同的語言。

推薦閱讀: 具有面部識別功能的移動應用程式:如何實現

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

檢視控制器的狀態儲存和恢復,允許使用者在離開應用程式後可以返回到之前完全相同的使用者介面狀態。有時,由於記憶體不足,作業系統可能需要在應用程式處於後臺時從記憶體中刪除應用程式,如果不保留狀態,應用程式可能會丟失其對最後一個UI狀態的跟蹤,可能會導致使用者丟失正在進行的操作!

在多工螢幕中,我們可以看到已放在後臺的應用程式列表。我們可以假設這些應用程式仍在後臺執行;實際上,由於記憶體的需求,一些應用程式可能會被系統殺死並重新啟動。我們在多工檢視中看到的應用程式快照實際上是系統在退出應用程式時擷取到的螢幕截圖。(即轉到主螢幕或多工螢幕)。

iOS fabricates the illusion of apps running in the background by taking a screenshot of the most recent view

使用者退出應用程式時 iOS 擷取的應用程式截圖(檢視大圖

iOS 使用這些螢幕截圖來給人一種假象,即應用程式仍在執行或仍在顯示此特定檢視,而應用程式可能已被後臺終止或重新啟動,但此時仍顯示相同的螢幕截圖。

您是否曾體驗過,從多工螢幕恢復應用程式後,該應用程式顯示的使用者介面與多工檢視中顯示的快照有什麼不一樣? 這是因為應用程式沒有實現狀態恢復機制,當應用程式在後臺被殺死時,顯示的資料丟失。這可能會導致糟糕的體驗,因為使用者希望你的應用程式與離開時處於相同的狀態。

在 Apple 的 保留你應用程式的 UI 文章中提及:

「使用者希望你的應用程式與他們離開時處於同一狀態。狀態儲存和恢復可確保應用程式在再次啟動時恢復到以前的狀態。」

UIKit 為簡化狀態保護和恢復做了很多工作:它可以在適當的時間自動處理應用程式狀態的儲存和載入。我們需要做的就是新增一些配置來告訴應用程式支援狀態儲存和恢復,以及告訴應用程式需要儲存哪些資料。

為了實現狀態儲存和恢復,我們可以在 AppDelegate.swift 中實現下面兩個方法:

func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
   return true
}
複製程式碼
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
   return true
}
複製程式碼

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

接下來,我們將告訴應用程式需要保留哪些檢視控制器。我們通過在 storyboard 中指定 restoration ID 來實現這一點:

Setting restoration ID in storyboard

storyboard 中設定 restoration ID (檢視大圖)

你也可以選中 Use Storyboard ID 以使用 storyboard ID 作為 restoration ID

如果要在程式碼中設定 restoration ID,我們可以使用檢視控制器的 restorationIdentifier 屬性。

// ViewController.swift
self.restorationIdentifier = "MainVC"
複製程式碼

在狀態保留期間,所有被分配了恢復識別符號的檢視控制器或檢視都會將其狀態儲存到磁碟。

可以將恢復識別符號組合在一起以形成恢復路徑。識別符號是通過檢視層次結構來分組的,從根檢視控制器到當前活動檢視控制器。 假設 MyViewController 嵌入在 navigation 控制器中,navigation 控制器嵌入在另一個 tabbar 控制器中。假設他們使用自己的類名作為恢復識別符號,恢復路徑將如下所示:

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)
   }
} 
複製程式碼

記得在自己的方法底部呼叫父類實現。這樣可確保父類有機會儲存和恢復狀態。

一旦指定儲存的物件解碼完成,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
   }
}
複製程式碼

這些,就是為你的應用程式實現狀態儲存和恢復的基本方法了!請記住,當應用程式被使用者強行關閉時,作業系統將刪除已儲存的狀態,避免在狀態儲存和恢復時出現問題。

此外,請勿將任何模型資料(即應儲存到 UserDefaults 或 Core Data 的資料)儲存到該狀態,即使這樣做似乎很方便。當使用者強制退出你的應用程式時,狀態資料將被刪除,你當然不希望以這種方式丟失模型資料。

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

  1. 使用Xcode構建和啟動應用程式。
  2. 跳轉到要測試狀態保留和恢復的頁面。
  3. 返回主螢幕 (通過向上滑動或雙擊 home 按鈕,或者在用模擬器時鍵入 Shift ⇧ + Cmd ⌘ + H) 將應用程式傳送到後臺。
  4. 通過在Xcode中點選 ⏹ 按鈕,停止程式執行。
  5. 再次啟動應用程式並檢查狀態是否已成功還原。

由於本節僅涵蓋了狀態儲存和恢復的基礎知識,因此我推薦 Apple Inc. 上的以下文章。瞭解更多有關狀態恢復的知識:

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

4. 儘可能減少透明檢視的使用

不透明檢視是指沒有透明度的檢視,意味著放在它後面的任何 UI 元素不可見。我們可以在 Interface Builder 中將檢視設定為不透明:

This will inform the drawing system to skip drawing whatever is behind this view

在 storyboard 中將 UIView 設定為不透明(檢視大圖

或者我們可以在程式碼中修改 UIView 的 isOpaque 屬性:

view.isOpaque = true
複製程式碼

將檢視設定為不透明將使繪圖系統在渲染螢幕時優化一些繪圖效能。

如果檢視具有透明度(即 alpha 低於 1.0),那麼 iOS 將需要做些額外的工作來混合檢視層次結構中不同的檢視層以計算出哪些內容需要展示。另一方面,如果檢視設定為不透明,則繪圖系統僅會將此檢視放在前面,並避免在其後面混合多個檢視層的額外工作。

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

Green is non-color blended, red is blended layer

在 Simulator 中顯示各種圖層的顏色

當選擇 Color Blended Layers 選項後,你可以看到一些檢視是紅色的,一些是綠色的。 紅色表示檢視不是不透明的,並且其顯示的是在其後面混合的圖層。綠色表示檢視不透明且未進行混合。

With an opaque color background, the layer doesn’t need to blend with another layer

儘可能為 UILabel 指定非透明背景顏色以減少顏色混合圖層。(檢視大圖)

上面顯示的所有 label(“檢視朋友”等)被紅色突出顯示,是因為當 label 被拖動到 storyboard 時,其背景顏色預設設定為透明。當繪圖系統在 label 區域附近的進行繪製時,它將詢問 label 後面的圖層並進行一些計算。

優化應用效能的方法是儘可能減少用紅色突出顯示的檢視數量。

通過將 label 顏色從 label.backgroundColor = UIColor.clear 修改成 label.backgroundColor = UIColor.white,我們可以減少 label 和它後面的檢視層之間的圖層混合。

Using a transparent background color will cause layer blending

許多 label 以紅色突出顯示,因為它們的背景顏色是透明的,導致 iOS 通過混合背後的檢視來計算背景顏色。 (檢視大圖)

你可能已經注意到,即使你已將 UIImageView 設定為不透明併為其指定了背景顏色,模擬器仍將在 imageView 上顯示紅色。 這可能是因為你用於 imageView 的影象具有Alpha通道。

要刪除影象的 Alpha 通道,可以使用預覽應用程式複製影象(Shift⇧ + Cmd⌘+ S),並在儲存時取消選中 Alpha 核取方塊。

Uncheck the ‘Alpha’ checkbox when saving an image to discard the alpha channel.

儲存影象時,取消選中 Alpha 核取方塊以取消 Alpha 通道。 (檢視大圖)

5. 在後臺執行緒中處理繁重的功能(GCD)

因為 UIKit 僅適用於主執行緒,所以在主執行緒上執行繁重的處理工作會降低 UI 的速度。主執行緒使用 UIKit 不僅要處理和響應使用者的互動,還需要繪製螢幕。

譯者注: 將touch input 翻譯成互動,是因為點選和輸入屬於互動範疇

使應用程式保持響應的關鍵是儘可能多的將繁重處理任務放到後臺執行緒。應當儘量避免在主執行緒上執行復雜的計算,網路和繁重的IO操作(例如,磁碟的讀取和寫入)。

你可能曾經使用過突然對你的操作停止響應的應用程式,就好像應用程式已掛起。這很可能是因為應用程式在主執行緒上執行繁重的計算任務。

主執行緒中通常在 UIKit 任務(如處理使用者輸入)和一些間隔很小的輕量級任務之間交替。如果在主執行緒上執行繁重的任務,那麼 UIKit 需要等到繁重的任務完成以後才能處理使用者互動。

Avoid running performance-intensive or time-consuming task on the main thread

這是主執行緒處理 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 代表著「quality of service」。不同的 QoS 值表示任務不同的優先順序。對於在具有較高 QoS 值的佇列中分配的任務,作業系統將分配更多的 CPU 時間、CPU 功率和 I/O 吞吐量,這意味著任務將在具有更高QoS值的佇列中更快地完成。較高的 QoS 值也會因使用更多資源而消耗更多能量。

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

Quality-of-service values of queue sorted by performance and energy efficiency

按效能和能效排序的 QoS 值 (檢視大圖)

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

需要記住,所有 UIKit 程式碼始終都應該在主執行緒上執行。在後臺執行緒上修改 UIKit 物件(例如 UILabelUIImageView)可能會產生意想不到的後果,例如UI實際上沒有更新,發生崩潰等等。

在 Apple 的 主執行緒檢查器 文章中提及:

「在主執行緒以外的執行緒上更新 UI 是一種常見錯誤,這可能導致 UI 不更新,視覺缺陷,資料損壞以及崩潰。」

我建議觀看 Apple 的 WWDC 2012 視訊上的 UI 併發,以便更好地瞭解如何構建響應式應用。

後記

效能優化需要你在應用程式的功能之上編寫更多的程式碼或配置其他設定。這可能會使您的應用程式交付時間超出預期,並且您將來會有更多程式碼需要維護,而更多程式碼意味著更多潛在的bug。

在花時間優化應用之前,先問問自己應用是否已經流暢,或者是否有一些真正需要優化的無響應的部分。花費大量時間優化已經很流暢的應用程式來減少 0.01 秒的耗時是不值得的,最好將這些時間花在開發更好的功能或優先順序更高的任務。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章