> 原文出處: 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**
關于性能優化,作者主要列舉了三個步,通過對這三步的反復執行形成一個個閉環,從而達到性能最優的目標。

①. 使用Gauges,Instruments和XCTest framework等工具衡量性能。
②. 針對第1步結果,改寫code提高性能。
③. 驗證新加code對性能的提升。
反復執行這三步,達到性能最優的目的。
1. **Measuring the problem**?首先運行這個Start APP,切換到Memory Report查看具體的內存用量。


~~~
上圖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**

通過上圖可以看出一次imports了50個employees,而且完成整個fetch花了2000微秒,并不十分高效。
接著就來修改代碼,書里將fetchRequest.fetchBatchSize = 10,至于fetchBatchSize這個具體的數字應為屏幕顯示條目數的2倍,iPhone一次大概能顯示5行,因此這里設為10比較合適。

最后來驗證我們的修改效果,同樣是運行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)"
}
~~~