【译】iOS性能优化技巧 — 让你的代码更有表现力

1,353 阅读14分钟

翻译地址

良好的性能对于提供良好的用户体验至关重要,而iOS用户通常对他们的应用程序有很高的期望。一个缓慢和反应迟钝的应用程序可能会让用户放弃使用你的应用程序,或者更糟糕的是,留下糟糕的评价。

尽管现代iOS硬件强大到足以处理许多密集而复杂的任务,但如果你对应用程序的表现不小心的话,设备仍然会感到反应迟钝。在本文中,我们将研究五个优化技巧,这些技巧将使您的应用程序更有响应性。

1.脱队列可重用单元

之前。你有没有想过,为什么你必须遵循这个笨拙的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
}

正如您可能已经想到的,当您滚动到底部时,这将向设备的内存中添加1000个单元格。想象一下,如果每个单元包含一个UIImageView还有很多文本:一次全部加载会导致应用程序耗尽内存!除此之外,每个单元格都需要在滚动过程中分配新的内存。如果你快速滚动一个表视图,那么很多小块内存就会被动态分配,这个过程会使UI变得简洁明了!

为了解决这个问题,苹果向我们提供了dequeueReusableCell(withIdentifier:for:)方法。单元重用的工作方式是将屏幕上不再可见的单元放在队列中,当屏幕上即将可见新的单元格时(例如,当用户滚动时,随后的单元格),表视图将从此队列中检索一个单元格,并在cellForRowAt indexPath:方法。

Cell reuse queue mechanism

在IOS中单元重用队列是如何工作的(大预览)

通过使用队列来存储单元格,表视图不需要创建1000个单元格。相反,它只需要足够的单元格来覆盖表视图的区域。

dequeueReusableCell,我们可以减少应用程序使用的内存,使其更容易耗尽内存!

2.使用看起来像初始屏幕的启动屏幕

正如苹果的人机界面指南(HIG),发射屏幕可以用来增强对应用程序响应能力的感知:

“它的唯一目的是增强你对应用程序的感知,使其快速启动,并立即准备使用。”每个应用程序都必须提供一个启动屏幕。“

使用启动屏幕作为启动屏幕显示品牌或添加加载动画是一个常见的错误。如Apple所述,将启动屏幕设计为与应用程序的第一个屏幕相同:

“设计一个与应用程序的第一个屏幕几乎相同的启动屏幕。如果你包含的元素在应用程序完成启动时看起来不一样,人们可能会在启动屏幕和应用程序的第一个屏幕之间经历一个令人不快的闪光灯。

“推出屏幕并不是一个品牌推广的机会。不要设计一个看起来像飞溅屏幕或“关于”窗口的入口体验。不要包含标识或其他品牌元素,除非它们是应用程序第一个屏幕的静态部分。“

使用一个启动屏幕加载或品牌的目的,可以减缓第一次使用的时间,并使用户感到该应用程序是呆滞的。

启动新IOS项目时,将出现空白LaunchScreen.storyboard将被创造。这个屏幕将显示给用户,而应用程序加载视图控制器和布局。

为了让您的应用程序感觉更快,您可以将启动屏幕设计为类似于将显示给用户的第一个屏幕(视图控制器)。

例如,Safari应用程序的启动屏幕类似于其第一个视图:

Launch screen and first view look similar

启动屏幕故事板与任何其他情节提要文件一样,只是您只能使用标准的UIKit类,如UIViewController、UITabBarController和UINavigationController。如果尝试使用任何其他自定义子类(例如UserViewController),Xcode将通知您禁止使用自定义类名。

Xcode shows error when a custom class is used

另外要注意的是UIActivityIndicatorView当放置在启动屏幕上时不会显示动画,因为IOS将从启动屏幕故事板生成静态图像并将其显示给用户。(这一点在WWDC 2014专题介绍中略为提及“国情咨文“,到处01:21:56.)

苹果的HIG还建议我们不要在我们的发布屏幕上包含文本,因为发布屏幕是静态的,你不能本地化文本来迎合不同的语言。

推荐阅读具有人脸识别功能的移动应用程序:如何使其成为现实

3.视图控制器的状态恢复

状态保存和恢复允许用户在离开应用程序之前返回完全相同的UI状态。有时,由于内存不足,操作系统可能需要在应用程序处于后台时将应用程序从内存中删除,如果应用程序不被保存,应用程序可能会丢失其最后的UI状态,这可能会导致用户失去他们正在进行的工作!

在多任务屏幕上,我们可以看到一个应用程序列表,这些应用程序都放在后台。我们可能假设这些应用程序仍然在后台运行;实际上,由于内存需求,其中一些应用程序可能会被系统杀死并重新启动。我们在多任务视图中看到的应用快照实际上是系统在退出应用程序时截取的截图(即回家或多任务屏幕)。

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

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”来做到这一点:

Setting restoration ID in storyboard

您也可以检查“使用故事板ID”以使用故事板ID作为还原ID。

要在代码中设置还原ID,我们可以使用restorationIdentifier属性的视图控制器。

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

在状态保存期间,任何已分配恢复标识符的视图控制器或视图都将其状态保存到磁盘。

恢复标识符可以组合在一起形成恢复路径。标识符使用视图层次结构分组,从根视图控制器到当前活动视图控制器。假设MyViewController嵌入在导航控制器中,导航控制器嵌入到另一个选项卡控制器中。假设它们使用自己的类名作为恢复标识符,恢复路径将如下所示:

TabBarController/NavigationController/MyViewController

当用户以MyViewController为活动视图控制器离开应用程序时,此路径将由应用程序保存;然后应用程序将记住前面显示的视图层次结构(标签条控制器 → 导航控制器 → 我的视图控制器).

在分配恢复标识符之后,我们将需要为每个保留的视图控制器实现encodeRestorableState(用编码器:)和DecdeRestorableState(用编码器:)方法。这两种方法让我们指定需要保存或加载哪些数据,以及如何对它们进行编码或解码。

让我们看看视图控制器:

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

给你!这些是实现应用程序状态维护和恢复的基本方法。请记住,当应用程序被用户强制关闭时,操作系统将删除保存的状态,以避免陷入崩溃状态,以防状态保存和恢复出现问题。

此外,不要将任何模型数据(即本应保存到UserDefault或Core数据的数据)存储到状态,尽管这样做似乎很方便。当用户强制退出应用程序时,状态数据将被删除,您当然不希望这样丢失模型数据。

要测试状态保存和恢复是否运行良好,请执行以下步骤:

  1. 使用Xcode构建并启动应用程序。
  2. 导航到要测试的状态保存和恢复屏幕。
  3. 返回到主屏幕(通过滑动或双击“主页”按钮或按下移位⇧+CMD⌘+H(在模拟器中)将应用程序发送到后台。
  4. 按下⏹按钮,停止Xcode中的应用程序。
  5. 再次启动应用程序,并检查状态是否已成功恢复。

由于本节只介绍了状态保护和恢复的基本知识,所以我推荐苹果公司的以下文章。要了解更多关于国家恢复的深入知识:

  1. 维护和恢复国家
  2. UI保存过程
  3. UI恢复过程

4.尽量减少非不透明视图的使用。

不透明视图是没有透明度的视图,这意味着放置在其后面的任何UI元素都是不可见的。我们可以在InterfaceBuilder中将视图设置为不透明的:

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

或者我们可以使用isOpaqueUIView的财产:

view.isOpaque = true

将视图设置为不透明将使绘图系统在呈现屏幕时优化一些绘图性能。

如果视图具有透明度(即alpha值低于1.0),那么IOS将不得不做额外的工作,通过在视图层次结构中混合不同的视图层来计算应该显示什么。另一方面,如果一个视图被设置为不透明的,那么绘图系统只会将这个视图放在前面,并避免将其后面的多个视图层混合的额外工作。

通过检查,可以检查IOS模拟器中哪些层是混合的(非不透明的)。调试 → 彩色混合层.

Green is non-color blended, red is blended layer

彩色混合层选项,您可以看到一些视图是红色的,有些是绿色的。红色表示视图不是不透明的,它的输出显示是混合在视图后面的层的结果。绿色表示视图是不透明的,没有进行混合。

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

上述标签(“查看朋友”等)以红色高亮显示,因为当标签拖到情节提要时,其背景色默认设置为透明。当绘图系统在标签区域附近组合显示时,它会要求标签后面的图层并进行一些计算。

优化应用程序性能的一种方法是尽可能减少用红色高亮显示视图的数量。

通过改变到,我们可以减少标签和它后面的视图层之间的层混合。

Using a transparent background color will cause layer blending

您可能已经注意到,即使您将UIImageView设置为不透明并为其分配了背景色,模拟器仍将在图像视图中显示红色。这可能是因为用于图像视图的图像有一个alpha通道。

要删除图像的alpha通道,可以使用预览应用程序复制图像移位⇧ + CMD⌘+ S),并在保存时取消选中“Alpha”复选框。

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

5.将繁重的处理功能传递给后台线程(GCD)

因为UIKit只在主线程上工作,所以在主线程上执行大量处理会减慢UI的速度。UIKit的主线程不仅用于处理和响应用户输入,而且还用于绘制屏幕。

使应用程序响应的关键是将尽可能多的繁重处理任务转移到后台线程。避免在主线程上进行复杂的计算、联网和繁重的IO操作(例如读取和写入磁盘)。

你可能曾经使用过一个应用程序,它突然变得对你的触摸输入没有反应,感觉就像这个应用挂起了。这很可能是因为应用程序在主线程上运行了大量的计算任务。

主线程通常在UIKit任务(例如处理用户输入)和一些小间隔的轻任务之间交替。如果一个繁重的任务在主线程上运行,那么UIKit需要等到繁重的任务完成后才能处理触摸输入。

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

默认情况下,在主线程上执行视图控制器生命周期方法(如viewDidLoad)和IBOutlet函数中的代码。若要将繁重的处理任务移动到后台线程,我们可以使用中央调度苹果公司提供的排队服务。

下面是交换队列的模板:

// 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值的队列中分配的任务分配更多的CPU时间和CPU功率I/O吞吐量,这意味着在具有较高QoS值的队列中,任务将完成得更快。更高的QoS值也将消耗更多的能源,因为它使用更多的资源。

以下是从最高优先级到最低优先级的QoS值列表:

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

苹果公司提供了一张方便的桌子提供了用于不同任务的QoS值的示例。

要记住的一件事是,所有的UIKit代码都应该在主线程上执行。修改UIKit对象(例如UILabelUIImageView)在后台,线程可能会产生意外的结果,比如UI实际上没有更新,崩溃发生,等等。

苹果的文章:

在主线程之外的线程上更新UI是一个常见错误,可能导致UI更新丢失、视觉缺陷、数据损坏和崩溃。

我建议你看苹果2012年用户界面并发视频以更好地理解如何构建一个响应性的应用程序。

注记

性能优化的折衷之处在于,您必须编写更多的代码或在应用程序的功能之上配置额外的设置。这可能会使您的应用程序交付得比预期的要晚,将来您将有更多的代码需要维护,而更多的代码可能意味着更多的bug。

在花时间优化你的应用程序之前,问问自己这个应用程序是否已经平滑了,或者它是否有一些真正需要优化的没有响应性的部分。花大量的时间来优化一个已经很流畅的应用程序以减少0.01秒的时间可能是不值得的,因为开发更好的特性或其他优先事项的时间可能会更好。