> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-qi
本章主要介绍了一些影响Core Data的性能问题,以及优化的方法。如果你对CoreData的其他方面感兴趣请查看我之前的笔记或直接购买[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)
## **Chapter 9:Measuring and Boosting Performance**
### **一、Getting started**
性能其实是一个需要在内存用量与速度之间的平衡问题,访问内存中的数据比磁盘中的数据要快很多,但是往内存中存入大量数据又会引起触发low memory warnings,你的程序又很快会被系统干掉。所以这都要靠开发者自己去平衡。
本章提供了一个关于“雇员名录”的Start Project,基于tab-bar。第一次启动时间会非常的长,这也是作者故意这么做的,方便我们接下来优化。
### **二、Measure,change,verify**
关于性能优化,作者主要列举了三个步,通过对这三步的反复执行形成一个个闭环,从而达到性能最优的目标。
![](https://box.kancloud.cn/2015-08-21_55d6eca9c38c9.jpg)
①. 使用Gauges,Instruments和XCTest framework等工具衡量性能。
②. 针对第1步结果,改写code提高性能。
③. 验证新加code对性能的提升。
反复执行这三步,达到性能最优的目的。
1. **Measuring the problem** 首先运行这个Start APP,切换到Memory Report查看具体的内存用量。
![](https://box.kancloud.cn/2015-08-21_55d6eca9e275d.jpg)
![](https://box.kancloud.cn/2015-08-21_55d6ecaa1ccc5.jpg)
~~~
上图2稳定下来的内存用量基本上就是整个程序最终的内存占用。可以看到整个内存占用还是相当可观的。接下来我们结合代码来分析一下。
~~~
通过对seed.json以及载入seed的方法的review,发现引起内存占用的主要是载入了大量的图片,一个解决办法就是将图片从整个数据结构中剥离出来,真正需要时再载入内存。
2. **Making changes to improve performance** 书中的做法是在数据model中新添加了一个EmployeePicture实体,用来专门存放图片相关属性(这里为其添加了一个picture的属性)。这里的picture属性设置为**Binary Data**并勾选了**Allows External Storage**,意味着由Core Data自己决定将这些二进制数据单独保存到磁盘中还是保留在sqlite数据库中。
将原Employee实体的**picture**属性重名为**pictureThumbnail**,表明在Employee中只保存缩略图,从而缩减内存用量。
在model中创建Employee与EmployeePicture之间的relationship。接下来就是对代码进行一些修改。如果是从网络API获取的图片,要看是否提供了缩略图的版本,因为这里将图片转换成缩略图的*绘图方法*也是会消耗一些性能的。
3. **Verify the changes** 再次使用Memory Report来验证内存用量,发现已经从当初的392MB缩减为41MB。
### **三、Fetching and performance**
对Fetch进行优化同样是要平衡内存的占用率,为了提高启动速率,我们来削减不必要的fetch。Core Data提供了一个**fetchBatchSize**属性来避免Fetch多余数据,该属性的默认值为0(没有启用)。
接着我们使用**Instruments**工具来分析一下(要选择Instruments Core Data template),默认的Core Data template包括下面三个工具:
* Core Data Fetches Instrument >Captures fetch count and duration of fetch operations. This will help you balance the number of fetch requests versus the size of each request.
* Core Data Cache Misses Instrument >Captures information about fault events that result in cache misses. This can help diagnose performance in low-memory situations.
* Core Data Saves Instrument >Captures information on managed object context save events. Writing data out to disk can be a performance and battery hit, so this instrument can help you determine whether you should batch things into one big save rather than many small ones.
我们点击Record按钮,等待20秒后停止,选择Core Data Fetches工具,下面的窗口会展示详细的信息,包括**fetch entity,fetch count,fetch duration**
![](https://box.kancloud.cn/2015-08-21_55d6ecaa3c02f.jpg)
通过上图可以看出一次imports了50个employees,而且完成整个fetch花了2000微秒,并不十分高效。
接着就来修改代码,书里将fetchRequest.fetchBatchSize = 10,至于fetchBatchSize这个具体的数字应为屏幕显示条目数的2倍,iPhone一次大概能显示5行,因此这里设为10比较合适。
![](https://box.kancloud.cn/2015-08-21_55d6ecaa65e32.jpg)
最后来验证我们的修改效果,同样是运行Core Data Fetches tool 20秒,这次可以看到多个fetches,排第一fetch因为只fetch count而不是所有的objects,所以速度是最快的,随后,我们滚动scrollView,fetch counts每次为10,表明每fetch一次最多取回10个objcet,也符合之前fetchBatchSize的设置。
更进一步的做法是使用**predicates**条件来限制返回的data,仅仅fetch需要的数据即可。如果存在多个*predicate*,那么将更具限制性的条件放在前面性能会更好一些。更多细节见官方[Guide](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Predicates/AdditionalChapters/Introduction.html)
> "(active == YES) AND (name CONTAINS[cd] %@)"就比"(name CONTAINS[cd] %@) AND (active == YES)"更加高效。
最后我们换一种工具来衡量性能,这次使用的是**XCTest framework**,这也是Xcode 6新增加的特性,可以用来进行性能方面的测试,关于单元测试可以看我[Core Data by tutorials 笔记(六)](http://chengway.in/post/categories/ji-zhu/core-data-by-tutorials-bi-ji-liu)
打开**DepartmentListViewControllerTests.swift**,添加一个测试方法:
~~~
func testTotalEmployeesPerDepartment(){
measureMetrics([XCTPerformanceMetric_WallClockTime],
automaticallyStartMeasuring: false) {
let departmentList = DepartmentListViewController()
departmentList.coreDataStack = CoreDataStack()
//标记开始measuring
self.startMeasuring()
let items = departmentList.totalEmployeesPerDepartment()
self.stopMeasuring()
}
}
~~~
这个方法使用**measureMetrics**来衡量具体code的执行时间。CMD+U执行测试方法,会显示具体消耗的时间(不同设备需要的时间也不相同)。
我们来修改代码只获取employee的数目**count**而不是实际的对象,书中这里用到了[NSExpressionDescription](http://chengway.in/post/categories/ji-zhu/core-data-by-tutorials-bi-ji-er),修改完成后继续验证,果然又快了很多。
接着我们来获取sales的数目,同样是先验证,然后再修改代码,再次验证。这次我们在设置完predicate后,直接使用[countForFetchRequest](http://chengway.in/post/categories/ji-zhu/core-data-by-tutorials-bi-ji-er)来获取couts数目。
获取sales还有没有更快的方法呢,答案是肯定的,因为employee有一个属性就是sales,所以我们可以简单地使用relationship来获取:
~~~
public func salesCountForEmployeeSimple(employee:Employee) -> String {
return "\(employee.sales.count)"
}
~~~