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

簡要歸納: 良好的性能對於提供良好的用戶體驗相當重要,iOS 用戶一般對其應用程序抱有很高的指望。緩慢且無響應的應用可能會讓用戶放棄使用你的應用,或者更糟糕的是,對應用留下差評。html

雖然現代 iOS 硬件功能十分強大,足以處理許多密集和複雜的任務,可是若是你不關心你的 APP 是怎麼執行的話,用戶的設備仍會出現無響應的狀況。在本文中,咱們將研究五種優化技巧,使你的 APP 更流暢。前端

1. 使用可複用的 tableViewCell

譯者注:本例闡述的是使用可複用的 tableViewCell,因此將全部 cell 翻譯成 tableViewCell ,table view 直譯成表視圖android

你以前可能在 tableView(_:cellForRowAt:) 中使用了tableView.dequeueReusableCell(withIdentifier:for:)。但你有沒有想過爲何必須使用這個笨拙的 API,而不是隻傳遞一個 TableViewCell 的數組?讓咱們來看看爲何。ios

假設你有一個有一千行的表視圖。若是不使用可複用的 tableViewCell ,咱們必須爲每一行建立一個新的 tableViewCell,以下所示:git

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 變得卡頓!github

爲了解決這個問題,Apple 爲咱們提供了 dequeueReusableCell(withIdentifier:for:) 方法。經過將屏幕上再也不可見的 tableViewCell 放入隊列中進行復用,而且當新 tableViewCell 即將在屏幕上可見時(例如,當用戶向下滾動時,下面的後續 tableViewCell),表視圖將今後隊列中檢索 tableViewCell 並在 cellForRowAt indexPath: 方法中修改它。swift

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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索