<簡書 — 劉小壯> http://www.jianshu.com/p/01f36026da7dgit
在以前的文章中,已經講了不少關於
CoreData
使用相關的知識點。這篇文章中主要講兩個方面,NSFetchedResultsController
和版本遷移。github文章題目中雖然有**「高級」**兩個字,其實講的東西並不高級,只是由於上一篇文章中東西太多了,把兩個較複雜的知識點挪到這篇文章中。數據庫
文章中若有疏漏或錯誤,還請各位及時提出,謝謝!數組
在開發過程當中會常常用到UITableView
這樣的視圖類,這些視圖類須要本身管理其數據源,包括網絡獲取、本地存儲都須要寫代碼進行管理。緩存
而在CoreData
中提供了NSFetchedResultsController
類(fetched results controller
,也叫FRC
),FRC
能夠管理UITableView
或UICollectionView
的數據源。這個數據源主要指本地持久化的數據,也能夠用這個數據源配合着網絡請求數據一塊兒使用,主要看業務需求了。網絡
本篇文章會使用UITableView
做爲視圖類,配合NSFetchedResultsController
進行後面的演示,UICollectionView
配合NSFetchedResultsController
的使用也是相似,這裏就不都講了。數據結構
就像上面說到的,NSFetchedResultsController
就像是上面兩種視圖的數據管理者同樣。FRC
能夠監聽一個MOC
的改變,若是MOC
執行了託管對象的增刪改操做,就會對本地持久化數據發生改變,FRC
就會回調對應的代理方法,回調方法的參數會包括執行操做的類型、操做的值、indexPath
等參數。多線程
實際使用時,經過FRC
**「綁定」一個MOC
,將UITableView
嵌入在FRC
的執行流程中。在任何地方對這個「綁定」**的MOC
存儲區作修改,都會觸發FRC
的回調方法,在FRC
的回調方法中嵌入UITableView
代碼並作對應修改便可。app
由此能夠看出FRC
最大優點就是,始終和本地持久化的數據保持統一。只要本地持久化的數據發生改變,就會觸發FRC
的回調方法,從而在回調方法中更新上層數據源和UI
。這種方式講的簡單一點,就能夠叫作數據帶動UI。ide
可是須要注意一點,在FRC
的初始化中傳入了一個MOC
參數,FRC
只能監測傳入的MOC
發生的改變。假設其餘MOC
對同一個存儲區發生了改變,FRC
則不能監測到這個變化,不會作出任何反應。
因此使用FRC
時,須要注意FRC
只能對一個MOC
的變化作出反應,因此在CoreData
持久化層設計時,儘可能一個存儲區只對應一個MOC
,或設置一個負責UI
的MOC
,這在後面多線程部分會詳細講解。
在寫代碼以前,先對以前的模型文件結構作一些修改。
講FRC
的時候,只須要用到Employee
這一張表,其餘表和設置直接忽略。須要在Employee
原有字段的基礎上,增長一個String
類型的sectionName
字段,這個字段就是用來存儲section title
的,在下面的文章中將會詳細講到。
下面例子是比較經常使用的FRC
初始化方式,初始化時指定的MOC
,還用以前講過的MOC
初始化代碼,UITableView
初始化代碼這裏也省略了,主要突出FRC
的初始化。
// 建立請求對象,並指明操做Employee表 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"]; // 設置排序規則,指明根據height字段升序排序 NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES]; request.sortDescriptors = @[heightSort]; // 建立NSFetchedResultsController控制器實例,並綁定MOC NSError *error = nil; fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:@"sectionName" cacheName:nil]; // 設置代理,並遵照協議 fetchedResultController.delegate = self; // 執行獲取請求,執行後FRC會從持久化存儲區加載數據,其餘地方能夠經過FRC獲取數據 [fetchedResultController performFetch:&error]; // 錯誤處理 if (error) { NSLog(@"NSFetchedResultsController init error : %@", error); } // 刷新UI [tableView reloadData];
在上面初始化FRC
時,傳入的sectionNameKeyPath:
參數,是指明當前託管對象的哪一個屬性當作section
的title
,在本文中就是Employee
表的sectionName
字段爲section
的title
。從NSFetchedResultsSectionInfo
協議的indexTitle
屬性獲取這個值。
在sectionNameKeyPath:
設置屬性名後,就以這個屬性名做爲分組title
,相同的title
會被分到一個section
中。
初始化FRC
時參數managedObjectContext:
傳入了一個MOC
參數,FRC
只能監測這個傳入的MOC
發生的本地持久化改變。就像上面介紹時說的,其餘MOC
對同一個持久化存儲區發生的改變,FRC
則不能監測到這個變化。
再日後面看到cacheName:
參數,這個參數我設置的是nil
。參數的做用是開啓FRC
的緩存,對獲取的數據進行緩存並指定一個名字。能夠經過調用deleteCacheWithName:
方法手動刪除緩存。
可是這個緩存並無必要,緩存是根據NSFetchRequest
對象來匹配的,若是當前獲取的數據和以前緩存的相匹配則直接拿來用,可是在獲取數據時每次獲取的數據均可能不一樣,緩存不能被命中則很難派上用場,並且緩存還佔用着內存資源。
在FRC
初始化完成後,調用performFetch:
方法來同步獲取持久化存儲區數據,調用此方法後FRC
保存數據的屬性纔會有值。獲取到數據後,調用tableView
的reloadData
方法,會回調tableView
的代理方法,能夠在tableView
的代理方法中獲取到FRC
的數據。調用performFetch:
方法第一次獲取到數據並不會回調FRC
代理方法。
FRC
中包含UITableView
執行過程當中須要的相關數據,能夠經過FRC
的sections
屬性,獲取一個遵照<NSFetchedResultsSectionInfo>
協議的對象數組,數組中的對象就表明一個section
。
在這個協議中有以下定義,能夠看出這些屬性和UITableView
的執行流程是緊密相關的。
@protocol NSFetchedResultsSectionInfo /* Name of the section */ @property (nonatomic, readonly) NSString *name; /* Title of the section (used when displaying the index) */ @property (nullable, nonatomic, readonly) NSString *indexTitle; /* Number of objects in section */ @property (nonatomic, readonly) NSUInteger numberOfObjects; /* Returns the array of objects in the section. */ @property (nullable, nonatomic, readonly) NSArray *objects; @end // NSFetchedResultsSectionInfo
在使用過程當中應該將FRC
和UITableView
相互嵌套,在FRC
的回調方法中嵌套UITableView
的視圖改變邏輯,在UITableView
的回調中嵌套數據更新的邏輯。這樣能夠始終保證數據和UI的同步,在下面的示例代碼中將會演示FRC
和UITableView
的相互嵌套。
// 經過FRC的sections數組屬性,獲取全部section的count值 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return fetchedResultController.sections.count; } // 經過當前section的下標從sections數組中取出對應的section對象,並從section對象中獲取全部對象count - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return fetchedResultController.sections[section].numberOfObjects; } // FRC根據indexPath獲取託管對象,並給cell賦值 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath]; cell.textLabel.text = emp.name; return cell; } // 建立FRC對象時,經過sectionNameKeyPath:傳遞進去的section title的屬性名,在這裏獲取對應的屬性值 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return fetchedResultController.sections[section].indexTitle; } // 是否能夠編輯 - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } // 這裏是簡單模擬UI刪除cell後,本地持久化區數據和UI同步的操做。在調用下面MOC保存上下文方法後,FRC會回調代理方法並更新UI - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // 刪除託管對象 Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; [context deleteObject:emp]; // 保存上下文環境,並作錯誤處理 NSError *error = nil; if (![context save:&error]) { NSLog(@"tableView delete cell error : %@", error); } } }
上面是UITableView
的代理方法,代理方法中嵌套了FRC
的數據獲取代碼,這樣在刷新視圖時就能夠保證使用最新的數據。而且在代碼中簡單實現了刪除cell
後,經過MOC
調用刪除操做,使本地持久化數據和UI
保持一致。
就像上面cellForRowAtIndexPath:
方法中使用的同樣,FRC
提供了兩個方法輕鬆轉換indexPath
和NSManagedObject
的對象,在實際開發中這兩個方法很是實用,這也是FRC
和UITableView
、UICollectionView
深度融合的表現。
- (id)objectAtIndexPath:(NSIndexPath *)indexPath; - (nullable NSIndexPath *)indexPathForObject:(id)object;
// Cell數據源發生改變會回調此方法,例如添加新的託管對象等 - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath { switch (type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeUpdate: { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; cell.textLabel.text = emp.name; } break; } } // Section數據源發生改變回調此方法,例如修改section title等。 - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch (type) { case NSFetchedResultsChangeInsert: [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; default: break; } } // 本地數據源發生改變,將要開始回調FRC代理方法。 - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [tableView beginUpdates]; } // 本地數據源發生改變,FRC代理方法回調完成。 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [tableView endUpdates]; } // 返回section的title,能夠在這裏對title作進一步處理。這裏修改title後,對應section的indexTitle屬性會被更新。 - (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName { return [NSString stringWithFormat:@"sectionName %@", sectionName]; }
上面就是當本地持久化數據發生改變後,被回調的FRC
代理方法的實現,能夠在對應的實現中完成本身的代碼邏輯。
在上面的章節中講到刪除cell
後,本地持久化數據同步的問題。在刪除cell
後在tableView
代理方法的回調中,調用了MOC
的刪除方法,使本地持久化存儲和UI
保持同步,並回調到下面的FRC
代理方法中,在代理方法中對UI
作刪除操做,這樣一套由UI的改變引起的刪除流程就完成了。
目前爲止已經實現了數據和UI
的雙向同步,即UI
發生改變後本地存儲發生改變,本地存儲發生改變後UI
也隨之改變。能夠經過下面添加數據的代碼來測試一下,NSFetchedResultsController
就講到這裏了。
- (void)addMoreData { Employee *employee = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context]; employee.name = [NSString stringWithFormat:@"lxz 15"]; employee.height = @(15); employee.brithday = [NSDate date]; employee.sectionName = [NSString stringWithFormat:@"3"]; NSError *error = nil; if (![context save:&error]) { NSLog(@"MOC save error : %@", error); } }
CoreData
版本遷移的方式有不少,通常都是先在Xcode
中,原有模型文件的基礎上,建立一個新版本的模型文件,而後在此基礎上作不一樣方式的版本遷移。
本章節將會講三種不一樣的版本遷移方案,但都不會講太深,都是從使用的角度講起,能夠知足大多數版本遷移的需求。
在已經運行程序並經過模型文件生成數據庫後,再對模型文件進行的修改,若是隻是修改已有實體屬性的默認值、最大最小值、Fetch Request
等屬性自身包含的參數時,並不會發生錯誤。若是修改模型文件的結構,或修改屬性名、實體名等,形成模型文件的結構發生改變,這樣再次運行程序就會致使崩潰。
在開發測試過程當中,能夠直接將原有程序卸載就能夠解決這個問題,可是本地以前存儲的數據也會消失。若是是線上程序,就涉及到版本遷移的問題,不然會致使崩潰,並提示以下錯誤:
CoreData: error: Illegal attempt to save to a file that was never opened. "This NSPersistentStoreCoordinator has no persistent stores (unknown). It cannot perform a save operation.". No last error recorded.
然而在需求不斷變化的過程當中,後續版本確定會對原有的模型文件進行修改,這時就須要用到版本遷移的技術,下面開始講版本遷移的方案。
本文中講的幾種版本遷移方案,在遷移以前都須要對原有的模型文件建立新版本。
選中須要作遷移的模型文件 -> 點擊菜單欄Editor -> Add Model Version -> 選擇基於哪一個版本的模型文件(通常都是選擇目前最新的版本),新建模型文件完成。
對於新版本模型文件的命名,我在建立新版本模型文件時,通常會拿當前工程版本號當作後綴,這樣在模型文件版本比較多的時候,就能夠很容易將模型文件版本和工程版本對應起來。
添加完成後,會發現以前的模型文件會變成一個文件夾,裏面包含着多個模型文件。
在新建的模型文件中,裏面的文件結構和以前的文件結構相同。後續的修改都應該在新的模型文件上,以前的模型文件不要再動了,在修改完模型文件後,記得更新對應的模型類文件。
基於新的模型文件,對Employee
實體作以下修改,下面的版本遷移也以此爲例。
添加一個String
類型的屬性,設置屬性名爲sectionName
。
此時還應該選中模型文件,設置當前模型文件的版本。這裏選擇將最新版本設置爲剛纔新建的1.1.0版本
,模型文件設置工做完成。
Show The File Inspector -> Model Version -> Current 設置爲最新版本。
對模型文件的設置已經完成了,接下來系統還要知道咱們想要怎樣遷移數據。在遷移過程當中可能會存在多種可能,蘋果將這個靈活性留給了咱們完成。剩下要作的就是編寫遷移方案以及細節的代碼。
輕量級版本遷移方案很是簡單,大多數遷移工做都是由系統完成的,只須要告訴系統遷移方式便可。在持久化存儲協調器(PSC
)初始化對應的持久化存儲(NSPersistentStore
)對象時,設置options
參數便可,參數是一個字典。PSC
會根據傳入的字典,自動推斷版本遷移的過程。
######字典中設置的key:
NSMigratePersistentStoresAutomaticallyOption
設置爲YES
,CoreData
會試着把低版本的持久化存儲區遷移到最新版本的模型文件。
NSInferMappingModelAutomaticallyOption
設置爲YES
,CoreData
會試着以最爲合理地方式自動推斷出源模型文件的實體中,某個屬性到底對應於目標模型文件實體中的哪個屬性。
版本遷移的設置是在建立MOC
時給PSC
設置的,爲了使代碼更直觀,下面只給出發生變化部分的代碼,其餘MOC
的初始化代碼都不變。
// 設置版本遷移方案 NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES, NSInferMappingModelAutomaticallyOption : @YES}; // 建立持久化存儲協調器,並將遷移方案的字典當作參數傳入 [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
假設須要對已存在實體進行更名操做,須要將重命名後的實體Renaming ID
,設置爲以前的實體名。下面是Employee
實體進行操做。
修改後再使用實體時,應該將實體名設爲最新的實體名,這裏也就是Employee2
,並且數據庫中的數據也會遷移到Employee2
表中。
Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context]; emp.name = @"lxz"; emp.brithday = [NSDate date]; emp.height = @1.9; [context save:nil];
輕量級遷移方案只是針對增長和改變實體、屬性這樣的一些簡單操做,假設有更復雜的遷移需求,就應該使用Xcode
提供的遷移模板(Mapping Model
)。經過Xcode
建立一個後綴爲.xcmappingmodel
的文件,這個文件是專門用來進行數據遷移用的,一些變化關係也會體如今模板中,看起來很是直觀。
這裏還以上面更改實體名,並遷移實體數據爲例子,將Employee
實體遷移到Employee2
中。首先將Employee
實體更名爲Employee2
,而後建立Mapping Model
文件。
Command + N 新建文件 -> 選擇 Mapping Model -> 選擇源文件 Source Model -> 選擇目標文件 Target Model -> 命名 Mapping Model 文件名 -> Create 建立完成。
如今就建立好一個Mapping Model
文件,文件中顯示了實體、屬性、Relationships
,源文件和目標文件之間的關係。實體命名是EntityToEntity
的方式命名的,實體包含的屬性和關聯關係,都會被添加到遷移方案中(Entity Mapping
,Attribute Mapping
,Relationship Mapping
)。
在遷移文件的下方是源文件和目標文件的關係。
在上面圖中更名後的Employee2
實體並無遷移關係,因爲是更名後的實體,系統還不知道實體應該怎樣作遷移。因此選中Mapping Model
文件的Employee2 Mappings
,能夠看到右側邊欄的Source
爲invalid value
。由於要從Employee
實體遷移數據過來,因此將其選擇爲Employee
,遷移關係就設置完成了。
設置完成後,還應該將以前EmployeeToEmployee
的Mappings
刪除,由於這個實體已經被Employee2
替代,它的Mappings
也被Employee2 Mappings
所替代,不然會報錯。
在實體的遷移過程當中,還能夠經過設置Predicate
的方式,來簡單的控制遷移過程。例如只須要遷移一部分指定的數據,就能夠經過Predicate
來指定。能夠直接在右側Filter Predicate
的位置設置過濾條件,格式是$source.height < 100
,$source
表明數據源的實體。
若是還存在更復雜的遷移需求,並且上面的遷移方式不能知足,能夠考慮更復雜的遷移方式。假設要在遷移過程當中,對遷移的數據進行更改,這時候上面的遷移方案就不能知足需求了。
對於上面提到的問題,在Mapping Model
文件中選中實體,能夠看到Custom Policy
這個選項,選項對應的是NSEntityMigrationPolicy
的子類,能夠建立並設置一個子類,並重寫這個類的方法來控制遷移過程。
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;
版本遷移在需求的變動中確定是要發生的,可是咱們應該儘可能避免這樣的狀況發生。在最開始設計模型文件數據結構的時候,就應該設計一個比較完善而且容易應對變化的結構,這樣後面就算髮生變化也不會對結構主體形成大的改動。
好多同窗都問我有Demo
沒有,其實文章中貼出的代碼組合起來就是個Demo
。後來想了想,仍是給本系列文章配了一個簡單的Demo
,方便你們運行調試,後續會給全部博客的文章都加上Demo
。
Demo
只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo
一塊兒學習,只看Demo
仍是不能理解更深層的原理。Demo
中幾乎每一行代碼都會有註釋,各位能夠打斷點跟着Demo
執行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github
這兩天更新了一下文章,將CoreData
系列的六篇文章整合在一塊兒,作了一個PDF
版的《CoreData Book》,放在我Github上了。PDF
上有文章目錄,方便閱讀。
若是你以爲不錯,請把PDF幫忙轉到其餘羣裏,或者你的朋友,讓更多的人瞭解CoreData,衷心感謝!