跳转至

8.衡量和提升绩效

在许多方面,这是一个不用动脑筋的问题:您应该努力优化您开发的任何应用程序的性能。 性能不佳的应用程序最多会收到差评,最坏的情况是变得没有响应并崩溃。

这对于使用Core Data的应用程序来说也是如此。 幸运的是,由于Core Data的内置优化(如faulting),Core Data的大多数实现已经快速而轻便。

但是,Core Data的灵活性使其成为一个很好的工具,这意味着您可以以负面影响性能的方式使用它。 从设置数据模型的错误选择到低效的获取和搜索,Core Data有很多机会降低应用程序的速度。

你将以一个运行缓慢的内存占用应用程序开始这一章。 在本章的最后,你将拥有一个轻便而快速的应用程序,如果你发现自己的应用程序又重又慢,你将确切地知道该去哪里看,该怎么做,以及如何首先避免这种情况!

入门

与大多数事情一样,性能是内存和速度之间的平衡。 应用的Core Data模型对象可以存在于两个位置:在随机存取存储器(RAM)中或在磁盘上。

访问RAM中的数据比访问磁盘上的数据快得多,但设备的RAM比磁盘空间少得多。

img

特别是iOS设备的可用RAM更少,这会阻止您将大量数据加载到内存中。 由于RAM中的模型对象较少,应用的操作将因频繁的慢速磁盘访问而变慢。 当你加载更多的模型对象到RAM中,你的应用可能会感觉更响应,但你不能饿死其他应用或操作系统将终止你的应用!

启动项目

启动项目EmployeeDirectory是一个基于选项卡栏的应用程序,其中包含员工信息。 这就像联系人应用程序,但对于一个虚构的公司。

Xcode中打开本章的EmployeeDirectory启动项目,构建并运行它。

该应用程序将需要很长时间才能启动,一旦启动,它会感到缓慢,甚至可能在您使用它时崩溃。放心,这是设计!

Note

启动项目甚至可能无法在您的系统上启动。 该应用程序被设计为尽可能缓慢,同时仍然能够在大多数系统上运行,因此您所做的性能改进将很容易被注意到。 如果应用程序拒绝在您的系统上工作,请继续沿着。 您所做的第一组更改应使应用程序能够在最慢的设备上运行。

正如您在下面的屏幕截图中所看到的,第一个选项卡包括一个表视图和一个自定义单元格,其中包含所有员工的基本信息,如姓名和部门。

点击单元格以显示所选员工的更多详细信息,例如开始日期和剩余假期天数。

img

点击该员工的个人资料图片,使图片全屏显示;点击全屏图片上的任何位置即可将其关闭。

应用程序的启动时间相当长,初始员工列表的滚动性能可能需要一些工作。 该应用程序还使用大量内存,您将在下一节中自行测量。

测量、更改、验证

您无需猜测性能瓶颈在哪里,而是可以通过有针对性的方式首先测量应用的性能,从而保存时间和精力。 Xcode提供了专门用于此目的的工具。

理想情况下,您应该测量性能,进行有针对性的更改,然后再次测量以验证更改是否具有预期的影响。

您应根据需要多次重复此测量-更改-验证过程,直到您的应用满足所有性能要求。

img

在本章中,您将执行以下操作:

  • 您将使用GaugesInstrumentXCTest框架在提供的启动项目中测量性能问题。
  • 接下来,您将对代码进行更改,以提高应用程序的性能。
  • 最后,您将通过再次测量来验证更改是否具有预期的结果。

然后,您将重复此循环,直到EmployeeDirectory表现得像核心数据冠军!

测量问题

构建、运行并等待应用程序启动。 完成后,使用 内存报告 查看应用使用的内存量。

要启动内存报告,请首先验证应用程序是否正在运行,然后执行以下步骤:

  1. 单击左侧导航窗格中的Debug navigator
  2. 要获取更多信息,请单击箭头展开 running process -在本例中为 EmployeeDirectory

img

现在,单击 Memory 行,查看内存量表的上半部分:

img

上半部分包括一个 Memory Use 量表,显示您的应用正在使用的内存量和百分比。 对于EmployeeDirectory,您将看到100 MB400 MB的内存正在使用,或者大约是iPhone 6S上可用RAM10%,这是仍然运行iOS 14的性能最差的设备。

Usage Comparison 饼图将此内存块描述为总可用内存的一部分。 它还显示其他进程使用的RAM量,以及可用的空闲RAM量。

现在,看看内存报告的下半部分:

img

下半部分由一个图表组成,显示了一段时间内的RAM使用情况。 对于EmployeeDirectory,您将看到两个不同的区域。

  1. 在第一次启动时,EmployeeDirectory在加载主要员工列表之前执行导入操作。 暂时忽略内存中的这些峰值。
  2. 下一个内存使用块发生在导入操作之后,此时员工列表可见。 一旦应用程序完全加载列表,您可以看到内存使用相当稳定。

Note

如果您使用iPhone 6SiPhone SE以外的设备,包括iOS模拟器,您的内存量表可能与这些截图不完全相同。 利用率百分比将基于测试设备上的可用RAM量,可能与iPhone 6S上的可用RAM不匹配。

考虑到应用程序中只有50个员工记录,RAM的使用率相当高。数据模型本身可能在这里出错,因此您将从这里开始调查。

探索数据源

Xcode中,打开项目导航器,点击 EmployeeDirectory.xcdatamodeld 查看数据模型。 启动项目的模型由一个具有11个属性的Employee实体和一个具有两个属性的Sale实体组成。

Employee实体上,aboutaddressdepartmentemailguidnamephone属性为字符串类型;active是一个布尔值; picture是二进制数据;startDate为日期,vacationDays为整数。 EmployeeEmployee之间为 * to-many * 关系,包含amount整数属性和dateDate属性。

首次启动时,应用程序将从绑定的JSON文件seed.json导入示例数据。 以下是JSON的摘录:

{
  "guid": "769adb89-82ad-4b39-be41-d02b89de7b94",
  "active": true,
  "picture": "face10.jpg",
  "name": "Kasey Mcfarland",
  "vacationDays": 2,
  "department": "Marketing",
  "startDate": "1979-09-05",
  "email": "kaseymcfarland@liquicom.com",
  "phone": "+1 (909) 561-2981",
  "address": "201 Lancaster Avenue, West Virginia, 2583",
  "about": "Dolore reprehenderit ... voluptate consectetur.\r\n"
},

Note

您可以通过修改位于AppDelegate.swift顶部的amountToImportaddSalesRecords常量来改变应用从seed.json文件导入的数据量和类型。 现在,将这些常量设置为默认值。

在性能方面,显示员工姓名、部门、电子邮件地址和电话号码的文本与个人资料图片的大小相比是无关紧要的,个人资料图片的大小大到足以潜在地影响列表的性能。

现在您已经度量了问题并为将来的比较提供了基线,接下来将对数据模型进行更改以减少使用的RAM量。

进行更改以提高性能

高内存使用率的罪魁祸首可能是员工个人资料图片。 由于图片存储为二进制数据属性,因此当您访问员工记录时,Core Data将分配内存并加载整个图片-即使您只需要访问员工的姓名或电子邮件地址!

这里的解决方案是将图片拆分为一个单独的相关记录。 从理论上讲,您将能够有效地访问员工记录,然后只在真正需要时才加载图片。

首先,单击EmployeeDirectory.xcdatamodeld打开可视化模型编辑器。 首先在模型中创建对象或图元。 在底部工具栏中,单击Add Entity加号(+)按钮,添加新实体。

将实体命名为 EmployeePicture。 然后单击该图元,并确保在“实用程序”(Utilities)部分中选择了第四个选项卡。 将class改为 EmployeePicture,Module改为 Current Product Module,Codegen改为 Manual/None

通过单击左侧面板中的实体名称或图表视图中实体的图表,确保选中 EmployeePicture 实体。

接下来,单击并按住右下角(编辑器样式分段控件旁边)的加号(+)按钮,然后从弹出窗口中单击 Attribute Type 。 将新属性命名为 picture

最后,在数据模型检查器中,将【属性类型】修改为Binary Data,并勾选Allows External选项。

您的编辑器应类似于以下内容:

img

如前所述,二进制数据属性通常存储在数据库中。 如果勾选 Allows External Storage 选项,Core Data会自动决定是将数据作为单独的文件保存到磁盘,还是将其保留在SQLite数据库中。

选中Employee实体,将 picture 属性重命名为 pictureThumbnail。 为此,请在图表视图中选择图片属性,然后在数据模型检查器中编辑名称。

您已经更新了模型,将原始图片存储在单独的实体中,并将缩略图存储在主Employee实体上。 当应用程序从核心数据中获取员工实体时,较小的缩略图图片将不需要那么多RAM。 一旦你完成了项目的其余部分的修改,你将有机会测试一下,并验证应用程序使用的RAM比以前少。

可以使用关系将两个实体链接在一起。 这样,当应用程序需要更高质量,更大的图片时,它仍然可以通过关系检索它。

选中Employee实体,点击右下角的加号(+)按钮。 本次选择Add Relationship。 将关系命名为 picture,将目标设置为 EmployeePicture,最后将 Delete Rule 设置为 Cascade

核心数据关系应该总是双向的,所以现在添加一个对应的关系。 选中EmployeePicture实体,添加新关系。 将新关系命名为 employee,将 Destination 设置为 Employee,最后将 Inverse 设置为 picture

你的模型现在应该看起来像这样:

img

现在您已经完成了对模型的更改,您需要为新的EmployeePicture实体创建一个NSManagedObject子类。 这个子类将允许您从代码中访问新实体。

右键单击EmployeeDirectory组文件夹,选择New File。 选择Cocoa Touch Class模板,点击Next。 将该类命名为 EmployeePicture,并将其作为 NSManagedObject 的子类。 确保 Language 选择Swift**,单击Next,最后单击Create

选择 EmployeePicture.swift,并将自动生成的代码替换为以下代码:

import Foundation
import CoreData

public class EmployeePicture: NSManagedObject {
}

extension EmployeePicture {
  @nonobjc 
  public class func fetchRequest() ->
    NSFetchRequest<EmployeePicture> {
    return NSFetchRequest<EmployeePicture>(
      entityName: "EmployeePicture")
  }

  @NSManaged public var picture: Data?
  @NSManaged public var employee: Employee?
}

这是一个非常简单的类,只有两个属性。 第一个是 picture,它匹配您刚刚在可视化数据模型编辑器中创建的 EmployeePicture 实体上的单个属性。 第二个属性 employee 匹配您在 EmployeePicture 实体上创建的关系。

Note

也可以让Xcode自动创建 EmployeePicture 类。 要通过这种方式添加新类,请打开 EmployeeDirectory。xcdatamodeld,进入 Editor ▸ Create NSManagedObject Subclass… ,选择数据模型,然后在接下来的两个对话框中选择 EmployeePicture 实体。 在最后一个框中选择语言选项Swift。 如果你被问到,对创建一个Objective-C桥接头说“不”。 单击Create按钮,保存文件。

然后,选择 Employee.swift 文件,并更新代码以使用新的 pictureThumbnail 属性和 picture 关系。 将picture变量重命名为pictureThumbnail,并添加一个新变量picture,类型为EmployeePicture。 你的变量现在看起来像这样:

@NSManaged public var about: String?
@NSManaged public var active: NSNumber?
@NSManaged public var address: String?
@NSManaged public var department: String?
@NSManaged public var email: String?
@NSManaged public var guid: String?
@NSManaged public var name: String?
@NSManaged public var phone: String?
@NSManaged public var pictureThumbnail: Data?
@NSManaged public var picture: EmployeePicture?
@NSManaged public var startDate: Date?
@NSManaged public var vacationDays: NSNumber?
@NSManaged public var sales: NSSet?

接下来,您需要更新应用程序的其余部分,以使用新的实体和属性。

打开 EmployeeListViewController.swift,在tableView(_:cellForRowAt:)中找到以下几行代码。

它应该很容易找到,因为它将在下一行有一个错误标记!

if let picture = employee.picture {

这段代码从employee对象获取图片数据,准备制作图像。 现在完整的图片保存在一个单独的实体中,您应该使用新添加的pictureThumbnail属性。 更新文件以匹配以下代码:

if let picture = employee.pictureThumbnail {

接下来,打开 EmployeeDetailViewController。swift,在configureView()中找到以下代码。 同样,它应该在错误旁边:

if let picture = employee.picture {

您需要更新已设置的图片,就像在 EmployeeListViewController.swift中所做的那样。 与单元格图片一样,员工详细信息视图也只有一个小图片,因此只需要缩略图版本。 将代码更新为如下所示:

if let picture = employee.pictureThumbnail {

接下来打开 EmployeePictureViewController.swift,在configureView()中找到以下代码:

guard let employeePicture = employee?.picture else {
  return
}

这一次,您希望使用高质量版本的图片,因为图像将全屏显示。 更新文件,使用您在 Employee 实体上创建的 picture 关系,访问图片的高质量版本:

guard let employeePicture = employee?.picture?.picture else {
  return
}

在构建和运行之前还有一件事要做。 打开 AppDelegate.swift,在importJSONSeedData(_:)中找到如下一行代码:

employee.picture = pictureData

现在您有了一个单独的实体来存储高质量的图片,您需要更新这行代码来设置pictureThumbnail属性和picture关系。

将上一行改为:

employee.pictureThumbnail =
  imageDataScaledToHeight(pictureData, height: 120)

let pictureObject =
  EmployeePicture(context: coreDataStack.mainContext)

pictureObject.picture = pictureData

employee.picture = pictureObject

首先,使用imageDataScaledToHeightpictureThumbnail设置为原始图片的较小版本。 接下来,创建一个新的“EmployeePicture”实体。

将新的EmployeePicture实体上的picture属性设置为pictureData常量。 最后,将employee实体上的picture关系设置为新创建的picture实体。

Note

imageDataScaledToHeight获取图片数据,调整大小为传入的高度,设置质量为80%,返回新的图片数据。

如果您的应用需要图片并通过网络调用检索数据,则应确保API尚未包含图片的较小缩略图版本。 像这样动态转换图像会有一个小的性能开销。

由于您更改了型号,请从测试设备中删除该应用。 构建并运行应用程序。给予看! 你应该看到你之前看到的:

img

应用程序应该像以前一样工作,你甚至可能会注意到由于缩略图而导致的小性能差异。 但这一变化的主要原因是提高内存使用率。

验证更改

现在您已经对项目进行了所有必要的更改,是时候看看您是否实际改进了应用程序。

在应用运行时,使用 Memory Report 查看应用的内存使用量。 这一次,它只消耗了大约20 MB60 MBRAM,大约是iPhone 6S总可用RAM2%

img

现在看看报告的下半部分。 和上次一样,最初的峰值来自导入操作,您可以忽略它。这一次平坦的面积要低得多。

img

恭喜,您已经通过调整数据模型减少了此应用程序的RAM使用量!

首先,您使用内存报告工具 measured 应用的性能。 接下来,您对Core Data存储和访问应用数据的方式进行了 changes 。 最后,您已 verified 更改是否提高了应用的性能。

取数与性能

Core Data是应用数据的保管者。 任何时候你想访问数据,你都必须用一个fetch请求来检索它。

例如,当应用加载员工列表时,它需要执行提取。 但是每次访问持久性存储都会产生开销。 你不想获取比你需要的更多的数据--只要足够,这样你就不会经常回到磁盘。 请记住,磁盘访问比RAM访问慢得多。

为了获得最大的性能,您需要在任意给定时间获取的对象数量和让许多记录占用RAM中宝贵空间的有用性之间取得平衡。

应用程序的启动时间有点慢,这表明初始获取发生了一些事情。

获取批量大小

核心数据提取请求包括fetchBatchSize属性,这使得很容易提取足够的数据,但不会太多。

如果未设置批处理大小,Core Data将使用默认值0,这将禁用批处理。

通过设置非零正批处理大小,可以限制返回到批处理大小的数据量。 随着应用程序需要更多数据,Core Data会自动执行更多批处理操作。 如果搜索EmployeeDirectory应用程序的源代码,则不会看到任何对fetchBatchSize的调用。 这表明了另一个潜在的改进领域!

让我们看看是否有任何地方可以使用批量大小来提高应用程序的性能。

测量问题

您将使用 Instruments 工具分析获取操作在应用中的位置。

首先,选择一个iPhone模拟器目标,然后从Xcode的菜单栏中选择 Product,然后选择 Profile(或按+ I)。 这将构建应用程序并启动仪器。

Note

您只能将仪器核心数据模板与模拟器一起使用,因为该模板需要DTrace工具,而真实的的iOS设备上不提供该工具。 您可能还需要为目标选择一个开发团队,以使仪器能够运行。

您将看到以下选择窗口:

img

选择Core Data模板,点击Choose。 这将启动Instruments窗口。 如果这是您第一次启动仪器,系统可能会要求您输入密码以授权仪器分析正在运行的进程-不用担心,在此对话框中输入密码是安全的。

仪器启动后,单击窗口左上角的 Record 按钮。

一旦EmployeeDirectory启动,上下滚动员工列表大约20秒,然后单击代替记录按钮的 Stop 按钮。

点击Fetches工具。 Instruments窗口应类似于以下内容:

img

默认的Core Data模板包括以下工具,可帮助您调整和监控性能:

  • Faults Instrument :捕获有关导致缓存未命中的故障事件的信息。 这可以帮助诊断低内存情况下的性能。
  • Fetches Instrument :捕获提取计数和提取操作的持续时间。 这将帮助您平衡获取请求的数量与每个请求的大小。
  • Saves Instrument :捕获有关托管对象上下文保存事件的信息。 将数据写入磁盘可能会对性能和电池造成影响,因此此工具可以帮助您确定是否应该将数据批量保存为一个大保存,而不是许多小的保存。

由于您单击了“提取”工具,Instruments窗口底部的详细信息部分将显示有关所发生的每个提取的详细信息。

这三行中的每一行都对应于应用程序中的同一行代码。前两行是私有的Core Data,因此可以忽略它们。

注意最后一行。 此行包括 fetch entity fetch count fetch duration ,单位为微秒。 右边是调用者树。

EmployeeDirectory导入50个员工。 fetch计数显示50,这意味着应用程序同时从Core Data中获取 * 所有 * 员工。 这不是很有效率!

Fetches工具证实了您的体验,提取速度很慢,很容易被注意到,正如您所看到的,它需要大约5,000微秒。 应用程序必须在使表视图可见并准备好进行用户交互之前完成此获取。

Note

根据您的Mac,屏幕上的数字(以及条形图的粗细)可能与这些屏幕截图中显示的数字不一致。 更快的Mac会有更快的读取速度。 不要担心-重要的是修改项目后您将看到的时间变化。

改进性能

打开 EmployeeListViewController.swift,在employeeFetchRequest(_:)中找到如下代码行:

let fetchRequest: NSFetchRequest<Employee> =
  Employee.fetchRequest()

此代码使用 Employee 实体创建一个fetch请求。 您尚未设置批处理大小,因此默认值为0,这意味着不进行批处理。 将获取请求的批处理大小设置为10,用以下内容替换上面的内容:

let fetchRequest: NSFetchRequest<Employee> =
  Employee.fetchRequest()
fetchRequest.fetchBatchSize = 10

如何确定最佳批量大小? 一个好的经验法则是将批处理大小设置为在任何给定时间显示在屏幕上的项目数量的两倍左右。 雇员列表在屏幕上一次显示三到五个雇员,所以10是合理的批量大小。

验证更改

现在您已经对项目进行了必要的更改,是时候再次查看您是否实际改进了应用程序。

要测试此修复程序,请首先构建并运行应用程序,并确保它仍然有效。

下次再次启动仪器:在Xcode中,选择Product】,然后选择Profile,或者按⌘ + I,重复前面的步骤。 请记住,在单击仪器中的 Stop 按钮之前,上下滚动员工列表约 20秒

Note

要使用最新的代码,请确保您从Xcode启动应用程序,这会触发构建,而不仅仅是点击Instruments中的红色按钮。

这一次,Core Data Instrument应如下所示:

img

现在有多个提取,初始提取更快!

第一次获取看起来与原始获取类似,因为它正在获取所有50个员工。 然而,这一次,它只获取对象的计数,而不是整个对象,这使得获取持续时间更短。 既然您已经在请求上设置了批处理大小,Core Data就会自动执行此操作。

在第一次提取之后,您可以看到以10个为一批的大量提取。 当您滚动员工列表时,仅在需要时才获取新实体。

您已经将初始提取的时间减少到了原来的三分之一,并且后续的提取时间更短,速度更快。 恭喜你,你的应用程序的速度再次提高!

高级取数

获取请求使用谓词来限制返回的数据量。 如上所述,为了获得最佳性能,您应该将获取的数据量限制为所需的最小值:获取的数据越多,获取所需的时间就越长。

Note

Fetch Predicate Performance :您可以使用谓词来限制获取请求。 如果获取请求需要复合谓词,则可以通过将限制性更强的谓词放在首位来提高效率。 如果谓词包含字符串比较,则尤其如此。 例如,具有"(active == YES)AND(name CONTAINS[cd] %@)"格式的谓词可能比"(name CONTAINS[cd] %@)AND(active == YES)"更有效。 有关更多谓词性能优化,请参阅Apple的谓词编程指南:apple.co/2a1Rq2n

构建并运行EmployeeDirectory,然后选择第二个标签为Departments的选项卡。 此选项卡显示部门列表以及每个部门的员工数量。

点击部门单元格以查看所选部门的员工列表。 img

点击每个部门单元格中的详细信息披露(也称为信息图标),以显示员工总数、在职员工和员工可用假期天数的明细。

img

第一个屏幕简单地列出了部门和每个部门的员工数量。 这里没有太多的数据,但仍然可能存在性能问题。 我们去看看

测量问题

您将使用 XCTest 框架来测量部门列表屏幕的性能,而不是使用InstrumentsXCTest通常用于单元测试,但它也包含用于测试性能的有用工具。

Note

有关单元测试和核心数据的更多信息,请查看第7章“单元测试”。

首先,熟悉应用程序如何创建部门列表屏幕。 打开 DepartmentListViewController.swift,在totalEmployeesPerDepartment()中找到以下代码。

//1
let fetchRequest: NSFetchRequest<Employee> =
  Employee.fetchRequest()

let fetchResults: [Employee]
do {
  fetchResults = 
    try coreDataStack.mainContext.fetch(fetchRequest)
} catch let error as NSError {
  print("ERROR: \(error.localizedDescription)")
  return [[String: String]]()
}

//2
var uniqueDepartments: [String: Int] = [:]
for department in fetchResults.compactMap({ $0.department }) {
  uniqueDepartments[department, default: 0] += 1
}

//3
return uniqueDepartments.map { department, headCount in
  [
    "department": department,
    "headCount": String(headCount)
  ]
}

此代码执行以下操作:

  1. 它使用Employee实体创建一个获取请求,然后获取所有雇员。
  2. 它遍历雇员部门并构建一个字典,其中键是部门名称,值是该部门的雇员数量。
  3. 它用部门列表屏幕所需的信息构建一个字典数组。

现在来衡量一下这段代码的性能。

打开 DepartmentListViewControllerTests.swift(注意文件名中的Tests后缀)并添加以下方法:

func testTotalEmployeesPerDepartment() {
  measureMetrics(
    [.wallClockTime],
    automaticallyStartMeasuring: false
  ) {
    let departmentList = DepartmentListViewController()
    departmentList.coreDataStack =
      CoreDataStack(modelName: "EmployeeDirectory")

    startMeasuring()
    _ = departmentList.totalEmployeesPerDepartment()
    stopMeasuring()
  }
}

此函数使用measureMetrics来查看代码执行所需的时间。

你必须每次都建立一个新的Core Data堆栈,这样你的结果就不会被Core Data出色的缓存能力所扭曲,这将使后续的测试运行非常快!

在块内部,首先创建一个DepartmentListViewController并给予它一个CoreDataStack。 然后,调用totalEmployeesPerDepartment来检索每个部门的员工数量。

现在您需要运行此测试。 在Xcode的菜单栏中,选择Product,然后选择Test,或者按下⌘ + U。 这将构建应用程序并运行测试。

一旦测试完成运行,Xcode将看起来像这样:

img

注意两个新的东西:

  1. testTotalEmployeesPerDepartment旁边有一个绿色的复选标记。 这意味着测试运行并通过。
  2. 右侧有一条消息,显示测试所用的时间。

在低规格的测试设备上,您可能会看到长达0的时间。执行totalEmployeesPerDepartment操作需要252秒。 这些结果看起来不错,但仍有改进的空间。

Note

根据测试设备的不同,您可能会得到不同的测试结果。 不要担心-重要的是修改项目后您将看到的时间变化。

改进性能

totalEmployeesPerDepartment的当前实现使用一个fetch请求来遍历所有员工记录。 还记得本章中的第一个优化吗?将全尺寸照片拆分为一个单独的实体? 这里有一个类似的问题:Core Data加载整个员工记录,但您真正需要的只是按部门统计员工数量。

以某种方式按部门对记录进行分组并进行计数会更有效。 您不需要员工姓名和照片缩略图等详细信息!

打开 DepartmentListViewController.swift 并在totalEmployeesPerDepartment()下面添加如下方法:

func totalEmployeesPerDepartmentFast() -> [[String: String]] {
  //1
  let expressionDescription = NSExpressionDescription()
  expressionDescription.name = "headCount"

  //2
  let arguments = [NSExpression(forKeyPath: "department")]
  expressionDescription.expression = NSExpression(
    forFunction: "count:",
    arguments: arguments
  )

  //3
  let fetchRequest: NSFetchRequest<NSDictionary> =
    NSFetchRequest(entityName: "Employee")
  fetchRequest.propertiesToFetch =
    ["department", expressionDescription]
  fetchRequest.propertiesToGroupBy = ["department"]
  fetchRequest.resultType = .dictionaryResultType

  //4
  var fetchResults: [NSDictionary] = []
  do {
    fetchResults =
      try coreDataStack.mainContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("ERROR: \(error.localizedDescription)")
    return [[String: String]]()
  }
  return fetchResults as? [[String: String]] ?? []
}

这段代码仍然使用一个fetch请求来填充部门列表屏幕,但它利用了NSExpression

它是这样工作的:

  1. 首先,创建一个NSExpressionDescription并将其命名为headCount
  2. 接下来,使用department属性的count:函数创建NSExpression
  3. 接下来,创建一个带有Employee实体的获取请求。 这一次,fetch请求应该只使用propertiesToFetch获取所需的最小属性;您只需要前面创建的department属性和calculated属性。 fetch请求还按department属性对结果进行分组。 您对托管对象不感兴趣,因此获取请求返回类型为DictionaryResultType。 这将返回一个字典数组,每个字典包含一个部门名称和一个员工计数--正是您所需要的!
  4. 最后,执行fetch请求。

viewDidLoad()中找到以下代码行:

items = totalEmployeesPerDepartment()

这一行代码使用了旧的慢函数来填充部门列表屏幕。 通过调用您刚刚创建的函数来替换它:

items = totalEmployeesPerDepartmentFast()

现在,应用程序使用更快的NSExpression支持的获取请求填充部门列表屏幕的表视图数据源。

Note

NSExpression是一个功能强大的API,但很少使用,至少很少直接使用。 当您创建带有比较操作的谓词时,您可能不知道,但实际上您是在使用表达式。 NSExpression中有许多预构建的统计和算术表达式,包括averagesumcountminmaxmedianmodestddev。 有关全面概述,请参阅NSExpression文档。

验证更改

现在您已经对项目进行了所有必要的更改,现在是时候再次查看您是否改进了应用程序的性能。

打开 DepartmentListViewControllerTests.swift 并添加一个新函数来测试刚刚创建的totalEmployeesPerDepartmentFast函数。

func testTotalEmployeesPerDepartmentFast() {
  measureMetrics(
    [.wallClockTime],
    automaticallyStartMeasuring: false
  ) {
    let departmentList = DepartmentListViewController()
    departmentList.coreDataStack =
      CoreDataStack(modelName: "EmployeeDirectory")

    startMeasuring()
    _ = departmentList.totalEmployeesPerDepartmentFast()
    stopMeasuring()
  }
}

和前面一样,这个测试使用measureMetrics来查看一个特定的函数需要多长时间;在本例中为totalEmployeesPerDepartmentFast

现在您需要运行此测试。 在Xcode的菜单栏中,选择Product,然后选择Test,或者按下⌘ + U。 这将构建应用程序并运行测试。 一旦测试完成运行,Xcode将看起来类似于以下内容:

img

这一次,您将看到两条消息,其中包含总执行时间,每个测试函数旁边都有一条消息。

Note

如果您没有看到时间消息,您可以在测试期间生成的日志中查看每个测试运行的详细信息。 在Xcode的菜单栏中,选择ViewDebug AreaShow Debug Area

根据您的测试设备,新函数totalEmployeesPerDepartmentFast大约需要0.002秒完成。 这比0快多了。原始函数totalEmployeesPerDepartment使用的时间为10.3秒。 速度提升了100%以上!

取数

正如您已经看到的,您的应用程序并不总是需要来自Core Data对象的所有信息;一些屏幕仅仅需要具有某些属性的对象的计数。

例如,员工详细信息屏幕显示员工自从加入公司以来所做的销售总数。

img

对于这个应用程序的目的,你不关心每一个单独的销售内容-例如,销售日期或购买者的姓名-只有多少销售有总数。

测量问题

您将再次使用XCTest来测量员工详细信息屏幕的性能。

打开 EmployeeDetailViewController.swift,找到salesCountForEmployee(_:)

func salesCountForEmployee(_ employee: Employee) -> String {
  let fetchRequest: NSFetchRequest<Sale> = Sale.fetchRequest()
  fetchRequest.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(Sale.employee), employee]
  )

  let context = employee.managedObjectContext
  do {
    let results = try context?.fetch(fetchRequest)
    return "\(results?.count ?? 0)"
  } catch let error as NSError {
    print("Error: \(error.localizedDescription)")
    return "0"
  }
}

此代码获取给定雇员的所有销售额,然后返回返回数组的计数。

获取完整的销售对象只是为了查看给定员工的销售量可能是浪费的。 这可能是另一个提高性能的机会!

让我们在尝试解决问题之前先衡量一下问题。

打开 EmployeeDetailViewControllerTests.swift,找到testCountSales()

func testCountSales() {
  measureMetrics(
    [.wallClockTime],
    automaticallyStartMeasuring: false
  ) {
    let employee = getEmployee()
    let employeeDetails = EmployeeDetailViewController()
    startMeasuring()
    _ = employeeDetails.salesCountForEmployee(employee)
    stopMeasuring()
  }
}

与前面的示例一样,此函数使用measureMetrics来查看单个函数运行所需的时间。 测试从一个方便的方法中获取一个雇员,创建一个EmployeeDetailViewController,开始测量,然后调用有问题的方法。

Xcode的菜单栏中,选择Product,然后选择Test,或者按下⌘ + U。 这将构建应用程序并运行测试。

一旦测试完成运行,您将在这个测试方法旁边看到一个时间,就像以前一样。

img

性能还不算太差--但仍有一些改进的空间。

改进性能

在上一个示例中,您使用了NSExpression对数据进行分组,并按部门提供员工计数,而不是返回实际记录本身。 你在这里也会做同样的事情。

打开 EmployeeDetailViewController.swift 并将以下代码添加到salesCountForEmployee(_:)方法下面的类中。

func salesCountForEmployeeFast(_ employee: Employee) -> String {

  let fetchRequest: NSFetchRequest<Sale> = Sale.fetchRequest()
  fetchRequest.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(Sale.employee), employee]
  )

  let context = employee.managedObjectContext

  do {
    let results = try context?.count(for: fetchRequest)
    return "\(results ?? 0)"
  } catch let error as NSError {
    print("Error: \(error.localizedDescription)")
    return "0"
  }
}

此代码与您在上一节中查看的函数非常相似。 主要的区别是,现在调用的不是execute(_:),而是count(for:)。 在configureView()中找到以下一行代码:

salesCountLabel.text = salesCountForEmployee(employee)

这行代码使用旧的salescount函数填充部门详细信息屏幕上的标签。 通过调用您刚刚创建的函数来替换它:

salesCountLabel.text = salesCountForEmployeeFast(employee)

验证更改

现在您已经对项目进行了必要的更改,是时候再次查看您是否改进了应用程序。打开 EmployeeDetailViewControllerTests.swift 并添加一个新函数来测试刚刚创建的salesCountForEmployeeFast函数。

func testCountSalesFast() {
  measureMetrics(
    [.wallClockTime],
    automaticallyStartMeasuring: false
  ) {
    let employee = getEmployee()
    let employeeDetails = EmployeeDetailViewController()
    startMeasuring()
    _ = employeeDetails.salesCountForEmployeeFast(employee)
    stopMeasuring()
  }
}

这个测试与前一个测试完全相同,除了它使用了新的、更快的函数。

Xcode的菜单栏中,选择Product,然后选择Test,或者按下⌘U。 这将构建应用程序并运行测试。

img

看起来很棒-另一个性能改进在你的腰带!

使用关系

上面的代码速度很快,但更快的方法似乎仍然需要做很多工作。 你必须创建一个fetch请求,创建一个谓词,获取上下文的引用,执行fetch请求并获取结果。

Employee实体有一个sales属性,该属性包含一个Set,其中包含Sale类型的对象。

打开 EmployeeDetailViewController.swift 并在salesCountForEmployeeFast(_:)方法下面添加以下新方法:

func salesCountForEmployeeSimple(
  _ employee: Employee
) -> String {
  "\(employee.sales?.count ?? 0)"
}

看起来好多了 通过在Employee实体上使用sales关系,代码变得简单得多,也更容易理解。

更新视图控制器和测试以使用此方法,遵循与上述相同的模式。

现在就来看看性能的变化。

img

挑战

使用刚刚学到的技术,尝试提高DepartmentDetailsViewController类的性能。 不要忘记编写测试来测量执行前后的时间。 作为提示,有许多方法提供计数,而不是完整的记录;这些可能以某种方式被优化以避免加载记录的内容。

关键点

  • 由于Core Data的内置优化(如faulting),Core Data的大多数实现已经快速而轻便。
  • 在对核心数据性能进行改进时,您应该进行测量,进行有针对性的更改,然后再次进行测量,以验证更改是否产生了预期的影响。
  • 对数据模型进行小的更改(例如将大型二进制blob移动到其他实体)可以提高性能。
  • 为获得最佳性能,应将获取的数据量限制为所需的最小值:获取的数据越多,获取所需的时间就越长。
  • 性能是内存和速度之间的平衡。 在应用中使用Core Data时,请始终牢记这一平衡。