Книга: iOS. Приемы программирования
Обсуждение
Обсуждение
Выборка объектов в основном потоке — не самая хорошая идея. Выполнять ее в главном потоке можно лишь в случаях, когда в стеке Core Data совсем немного элементов. Дело в том, что при операции выборки в Core Data обычно выполняется поисковый вызов. Затем этот вызов должен выбрать для вас определенные данные, обычно с помощью предиката. Чтобы сделать пользовательский интерфейс более отзывчивым, лучше всего выполнять такие операции выборки в фоновом контексте.
Вы можете создать в приложении столько контекстов, сколько захотите, однако помните об одном железном правиле. Нельзя передавать управляемые объекты между контекстами в разных потоках, так как объекты не являются потокобезопасными. Таким образом, если вы выбираете объекты в фоновом контексте, то не можете использовать их в главном потоке. Вот как следует передавать управляемые объекты между потоками: объект выбирается в фоновом потоке, а потом переносится в главный контекст (контекст, работающий в основном потоке). Это делается с помощью метода objectWithID: главного контекста. Этот метод принимает объект типа NSManagedObjectID. Поэтому в фоновом потоке мы на самом деле не выбираем управляемые объекты как таковые, а лишь берем их сохраняемые ID, после чего передаем эти ID главному контексту, который сам получает для вас управляемый объект. Итак, вы выполняете в фоновом режиме и поиск, и выборку объектов, затем передаете ID найденных объектов главному контексту. Получением самих объектов занимается уже главный контекст. Если действовать таким образом, главный контекст будет располагать сохраняемыми ID объектов, а получение этих объектов из постоянного хранилища в такой ситуации протекает гораздо быстрее, чем при выполнении полномасштабного поиска в главном контексте.
В этом разделе предполагается, что вы уже создали модель управляемых объектов Person. Подобная модель показана на рис. 16.20.
Рис. 16.20. Простая модель Core Data, используемая в этом разделе
При работе с этой моделью я заполню стек 1000 объектов Person, как показано в следующем коде, а уже потом попробую выбирать информацию из стека:
— (void) populateDatabase{
for (NSUInteger counter = 0; counter < 1000; counter++){
Person *person =
[NSEntityDescription
insertNewObjectForEntityForName: NSStringFromClass([Person class])
inManagedObjectContext: self.managedObjectContext];
person.firstName = [NSString stringWithFormat:@"First name %lu",
(unsigned long)counter];
person.lastName = [NSString stringWithFormat:@"Last name %lu",
(unsigned long)counter];
person.age = @(counter);
}
NSError *error = nil;
if ([self.managedObjectContext save:&error]){
NSLog(@"Managed to populate the database.");
} else {
NSLog(@"Failed to populate the database. Error = %@", error);
}
}
Обратите внимание: я использую класс NSStringFromClass для преобразования имени класса Person в строку и для последующего инстанцирования объектов такого типа. Некоторые программисты предпочитают типизировать Person как строковый литерал. Но если жестко запрограммировать вашу строку таким образом, может возникнуть проблема. Допустим, позже вы решите изменить имя Person в стеке Core Data, а жестко закодированная строка никуда не денется. Она может привести к аварийному завершению вашего приложения во время исполнения, так как объекта модели с именем Person больше не существует. Но если вы примените вышеупомянутую функцию для преобразования имени класса в обычную строку, то при изменении имени класса или отсутствии такого класса получите ошибку времени компиляции. Такие ошибки выявляются еще до ввода приложения в работу, и у вас будет время их исправить.
Прежде чем продолжать обсуждение, оговорюсь: предполагается, что вы уже заполнили базу данных с помощью последнего написанного нами метода. Далее в общих чертах изложено, как мы собираемся выполнять выборку в фоновом контексте.
1. Создаем фоновый контекст с помощью метода-инициализатора initWithConcurrencyType:, относящегося к классу NSManagedObjectContext, затем передаем этому методу значение NSPrivateQueueConcurrencyType. В результате получаем контекст, имеющий собственную закрытую очередь диспетчеризации. Поэтому, если вызвать в контексте блок performBlock:, этот блок будет выполнен в закрытой фоновой очереди.
2. Затем мы собираемся задать в фоновом контексте значение свойства persistentStoreCoordinator, которое будет равно экземпляру координатора нашего постоянного хранилища данных. Таким образом мы свяжем фоновый контекст с координатором постоянного хранилища. В результате, если выполнить выборку в фоновом контексте, эта операция позволит получить данные прямо с диска или из любого другого места, где их может хранить координатор.
3. Выполняем в фоновом контексте вызов performBlock:, а затем даем запрос на выборку. В рамках этого запроса требуется найти в стеке Core Data всех людей, чей возраст относится к диапазону от 100 до 200. Подчеркиваю: реалистичность эксперимента нас в данном случае не волнует. Я хочу лишь продемонстрировать, как действует выборка данных в фоновом режиме. Создавая запрос выборки данных, мы устанавливаем его свойство resultType в значение NSManagedObjectIDResultType. Так мы гарантируем, что результаты, возвращаемые после выполнения этого запроса на выборку, состоят не из управляемых объектов как таковых, а только из ID этих объектов. Как объяснялось ранее, мы не собираемся выбирать сами управляемые объекты, поскольку при выборке этих объектов в фоновом контексте не сможем использовать их в основном потоке. Итак, в фоновом контексте мы только выбираем их ID, а преобразуем эти ID в реальные управляемые объекты уже в главном контексте. После этого такие объекты можно использовать в основном потоке.
Вот как создается запрос на выборку:
— (NSFetchRequest *) newFetchRequest{
NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:
NSStringFromClass([Person class])];
request.fetchBatchSize = 20;
request.predicate =
[NSPredicate predicateWithFormat:@"(age >= 100) AND (age <= 200)"];
request.resultType = NSManagedObjectIDResultType;
return request;
}
А вот как мы будем создавать фоновый контекст и выполнять в нем запрос на выборку данных:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
__weak NSManagedObjectContext *mainContext = self.managedObjectContext;
__weak AppDelegate *weakSelf = self;
__block NSMutableArray *mutablePersons = nil;
/* Создаем фоновый контекст */
NSManagedObjectContext *backgroundContext =
[[NSManagedObjectContext alloc]
initWithConcurrencyType: NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator =
self.persistentStoreCoordinator;
/* Выполняем блок в фоновом контексте */
[backgroundContext performBlock: ^{
NSError *error = nil;
NSArray *personIds = [backgroundContext
executeFetchRequest: [weakSelf newFetchRequest]
error:&error];
if (personIds!= nil && error == nil){
mutablePersons = [[NSMutableArray alloc]
initWithCapacity: personIds.count];
/* Теперь переходим в главный контекст и получаем эти объекты
в главном контексте, исходя из их ID */
dispatch_async(dispatch_get_main_queue(), ^{
for (NSManagedObjectID *personId in personIds){
Person *person = (Person *)[mainContext
objectWithID: personId];
[mutablePersons addObject: person];
}
[weakSelf processPersons: mutablePersons];
});
} else {
NSLog(@"Failed to execute the fetch request.");
}
}];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Этот код собирает все управляемые объекты в виде массива, а затем вызывает в делегате нашего приложения метод processPersons:, обрабатывающий результаты массива. Напишем этот метод следующим образом:
— (void) processPersons:(NSArray *)paramPersons{
for (Person *person in paramPersons){
NSLog(@"First name = %@, last name = %@, age = %ld",
person.firstName,
person.lastName,
(long)person.age.integerValue);
}
}