Фон
- Какао-приложение с использованием основных данных Два
процессы - демон и основной интерфейс
- Демон постоянно пишет в хранилище данных
- Процесс пользовательского интерфейса читает из тех же данных
магазин
- Столбцы в NSOutlineView в пользовательском интерфейсе, привязанные к
NSTreeController
- NSTreeControllers managedObjectContext связан с
Приложение с ключевым путем
delegate.interpretedMOC
- Для сущности NSTreeControllers задано значение TrainingGroup (подкласс NSManagedObject называется JGTrainingGroup)
Что я хочу
Когда пользовательский интерфейс активирован, представление структуры должно обновляться с использованием последних данных, вставленных демоном.
Проблема
Подход с главной нитью
Я выбираю все интересующие меня сущности, затем перебираю их, выполняя refreshObject: mergeChanges: YES. Это работает хорошо - элементы обновляются правильно. Однако все это выполняется в основном потоке, поэтому пользовательский интерфейс блокируется на 10-20 секунд, пока он обновляется. Хорошо, давайте перенесем эти обновления в NSOperations, которые работают в фоновом режиме.
NSOperation Многопоточный подход
Как только я переместил refreshObject: mergeChanges: call в NSOperation, обновление больше не работает. Когда я добавляю сообщения регистрации, ясно, что новые объекты загружаются подклассом NSOperation и обновляются. Кажется, что независимо от того, что я делаю, NSOutlineView не обновится.
Что я пробовал
Я два дня бездельничал и перепробовал все, что мог придумать.
- Передача objectID в NSOperation для обновления вместо имени объекта.
- Сброс интерпретированного MOC в различных точках - после обновления данных и до перезагрузки схематического представления.
- Я бы подкласс NSOutlineView. Я отбросил свой подкласс и вернул представление в качестве экземпляра NSOutlineView, на случай, если здесь произойдут какие-то забавные события.
- Добавлен вызов restrangeObjects в NSTreeController перед перезагрузкой данных NSOutlineView.
- Убедитесь, что я установил интервал устаревания на 0 во всех контекстах управляемого объекта, которые я использовал.
У меня такое ощущение, что эта проблема как-то связана с кэшированием основных объектов данных в памяти. Но я полностью исчерпал все свои идеи о том, как заставить это работать.
Я буду вечно благодарен всем, кто может пролить свет на то, почему это может не сработать.
Код
Подход с главной нитью
// In App Delegate
-(void)applicationDidBecomeActive:(NSNotification *)notification {
// Delay to allow time for the daemon to save
[self performSelector:@selector(refreshTrainingEntriesAndGroups) withObject:nil afterDelay:3];
}
-(void)refreshTrainingEntriesAndGroups {
NSSet *allTrainingGroups = [[[NSApp delegate] interpretedMOC] fetchAllObjectsForEntityName:kTrainingGroup];
for(JGTrainingGroup *thisTrainingGroup in allTrainingGroups)
[interpretedMOC refreshObject:thisTrainingGroup mergeChanges:YES];
NSError *saveError = nil;
[interpretedMOC save:&saveError];
[windowController performSelectorOnMainThread:@selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// In window controller class
-(void)refreshTrainingView {
[trainingViewTreeController rearrangeObjects]; // Didn't really expect this to have any effect. And it didn't.
[trainingView reloadData];
}
Многопоточный подход NSOperation
// In App Delegate (just the changed method)
-(void)refreshTrainingEntriesAndGroups {
JGRefreshEntityOperation *trainingGroupRefresh = [[JGRefreshEntityOperation alloc] initWithEntityName:kTrainingGroup];
NSOperationQueue *refreshQueue = [[NSOperationQueue alloc] init];
[refreshQueue setMaxConcurrentOperationCount:1];
[refreshQueue addOperation:trainingGroupRefresh];
while ([[refreshQueue operations] count] > 0) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
// At this point if we do a fetch of all training groups, it's got the new objects included. But they don't show up in the outline view.
[windowController performSelectorOnMainThread:@selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// JGRefreshEntityOperation.m
@implementation JGRefreshEntityOperation
@synthesize started;
@synthesize executing;
@synthesize paused;
@synthesize finished;
-(void)main {
[self startOperation];
NSSet *allEntities = [imoc fetchAllObjectsForEntityName:entityName];
for(id thisEntity in allEntities)
[imoc refreshObject:thisEntity mergeChanges:YES];
[self finishOperation];
}
-(void)startOperation {
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isStarted"];
[self setStarted:YES];
[self setExecuting:YES];
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isStarted"];
imoc = [[NSManagedObjectContext alloc] init];
[imoc setStalenessInterval:0];
[imoc setUndoManager:nil];
[imoc setPersistentStoreCoordinator:[[NSApp delegate] interpretedPSC]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:imoc];
}
-(void)finishOperation {
saveError = nil;
[imoc save:&saveError];
if (saveError) {
NSLog(@"Error saving. %@", saveError);
}
imoc = nil;
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
[self setExecuting:NO];
[self setFinished:YES];
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
-(void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainContext = [[NSApp delegate] interpretedMOC];
[mainContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
-(id)initWithEntityName:(NSString *)entityName_ {
[super init];
[self setStarted:false];
[self setExecuting:false];
[self setPaused:false];
[self setFinished:false];
[NSThread setThreadPriority:0.0];
entityName = entityName_;
return self;
}
@end
// JGRefreshEntityOperation.h
@interface JGRefreshEntityOperation : NSOperation {
NSString *entityName;
NSManagedObjectContext *imoc;
NSError *saveError;
BOOL started;
BOOL executing;
BOOL paused;
BOOL finished;
}
@property(readwrite, getter=isStarted) BOOL started;
@property(readwrite, getter=isPaused) BOOL paused;
@property(readwrite, getter=isExecuting) BOOL executing;
@property(readwrite, getter=isFinished) BOOL finished;
-(void)startOperation;
-(void)finishOperation;
-(id)initWithEntityName:(NSString *)entityName_;
-(void)mergeChanges:(NSNotification *)notification;
@end
ОБНОВЛЕНИЕ 1
Я только что нашел этот вопрос. Я не могу понять, как я пропустил это до того, как опубликовал свой, но резюме таково: Базовые данные не были предназначены для того, что я делаю Только один процесс должен использовать хранилище данных.
Проблема сброса / обновления NSManagedObjectContext и NSArrayController
Однако в другой области моего приложения у меня есть два процесса, которые совместно используют хранилище данных, и один из них имеет доступ только для чтения, и это, похоже, работает нормально. Плюс ни в одном из ответов на мой последний вопрос по этой теме не упоминалось, что это не поддерживается в Core Data.
Я собираюсь перестроить свое приложение, чтобы в один момент времени в хранилище данных записывался только один процесс. Я все еще скептически отношусь, что это решит мою проблему все же. Для меня это больше похоже на проблему с обновлением NSOutlineView - объекты создаются в контексте, просто контурное представление не воспринимает их.