[译] 用这些 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索