NSProgress странное поведение

У меня есть одна большая задача, которая состоит из нескольких подзадач. и я хочу добавить отчеты о прогрессе для этой большой задачи.
для этого я хочу использовать NSProgressи в соответствии с документацией класса я могу выполнить этот вид прогресса подзадачи, используя его дочерний родительский механизм.

Итак, чтобы упростить его, допустим, у меня есть большая задача, состоящая из одной подзадачи (конечно, в реальной жизни было бы больше подзадач). Вот что я сделал:

#define kFractionCompletedKeyPath @"fractionCompleted"  

- (void)runBigTask {
    _progress = [NSProgress progressWithTotalUnitCount:100]; // 100 is arbitrary 

    [_progress addObserver:self
                forKeyPath:kFractionCompletedKeyPath
                   options:NSKeyValueObservingOptionNew
                   context:NULL];

    [_progress becomeCurrentWithPendingUnitCount:100]; 
    [self subTask];
    [_progress resignCurrent];
} 

- (void)subTask {
    NSManagedObjectContext *parentContext = self.managedObjectContext; // self is AppDelegate in this example
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [bgContext setParentContext:parentContext];

    [bgContext performBlockAndWait:^{
        NSInteger totalUnit = 1000;
        NSInteger completedUnits = 0;
        NSProgress *subProgress = [NSProgress progressWithTotalUnitCount:totalUnit];

        for (int i=0; i < totalUnit; i++) {   

            // run some Core Data related code...  

            completedUnits++;
            subProgress.completedUnitCount = completedUnits;
        }
    }];
}      

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:kFractionCompletedKeyPath]) {
        if ([object isKindOfClass:[NSProgress class]]) {
            NSProgress *progress = (NSProgress *)object;
            NSLog(@"progress… %f", progress.fractionCompleted);
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

Как вы можете видеть, подзадача использует фоновый контекст для запуска некоторого кода, связанного с Core Data, а фоновый контекст использует основной контекст в качестве родительского контекста.
Это вызывает некоторое странное КВО свойства прогресса "FractionCompleted".

это печать:

progress… 1.000000 // why???
progress… 0.500000 // why?????
progress… 1.000000 // why???????
progress… 0.666650 // why???????????
progress… 0.666990
progress… 0.667320
progress… 0.667660
progress… 0.667990
progress… 0.668320
...  
progress… 1.000000  

Как вы можете видеть, печать начинается с 1,0, 0,5 и 1,0, а затем идет до 0,66?
отсюда он работает нормально и идет к 1,0, как я ожидаю.

Я попытался понять, почему это происходит, и заметил, что если я удаляю родительский контекст из фонового контекста, он работает нормально! Я получаю прогресс от 0,0 до 1,0.

Есть идеи, почему это происходит? и как я могу это исправить?

Я добавил очень простой проект, чтобы продемонстрировать эту проблему (вы можете удалить setParentContext: call, чтобы убедиться, что он работает без него)

2 ответа

Решение

Трассировка стека при этом выглядит следующим образом:

(lldb) bt
* thread #1: tid = 0x81f2, 0x0000000105bffcda Foundation`-[NSProgress setTotalUnitCount:], queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000105bffcda Foundation`-[NSProgress setTotalUnitCount:]
    frame #1: 0x0000000105bfeb1b Foundation`+[NSProgress progressWithTotalUnitCount:] + 87
    frame #2: 0x0000000105a31213 Foundation`_NSReadBytesFromFileWithExtendedAttributes + 287
    frame #3: 0x0000000105a3109d Foundation`-[NSData(NSData) initWithContentsOfFile:] + 89
    frame #4: 0x0000000105a30b40 Foundation`+[NSDictionary(NSDictionary) newWithContentsOf:immutable:] + 101
    frame #5: 0x0000000105a5622a Foundation`+[NSDictionary(NSDictionary) dictionaryWithContentsOfFile:] + 45
    frame #6: 0x00000001043c4560 CoreData`-[NSManagedObjectModelBundle initWithPath:] + 224
    frame #7: 0x00000001043c42ed CoreData`-[NSManagedObjectModel initWithContentsOfURL:] + 205
    frame #8: 0x00000001040f723f CDProgress`-[AppDelegate managedObjectModel](self=0x00007fbe48c21f90, _cmd=0x000000010459b37b) + 223 at AppDelegate.m:127
    frame #9: 0x00000001040f7384 CDProgress`-[AppDelegate persistentStoreCoordinator](self=0x00007fbe48c21f90, _cmd=0x000000010459c1cb) + 228 at AppDelegate.m:142
    frame #10: 0x00000001040f708c CDProgress`-[AppDelegate managedObjectContext](self=0x00007fbe48c21f90, _cmd=0x0000000104598f0d) + 92 at AppDelegate.m:111
    frame #11: 0x00000001040f6bdb CDProgress`-[AppDelegate subTask](self=0x00007fbe48c21f90, _cmd=0x00000001040f7997) + 43 at AppDelegate.m:45
    frame #12: 0x00000001040f6b89 CDProgress`-[AppDelegate runTask](self=0x00007fbe48c21f90, _cmd=0x00000001040f7928) + 233 at AppDelegate.m:40
    frame #13: 0x00000001040f6a4b CDProgress`-[AppDelegate application:didFinishLaunchingWithOptions:](self=0x00007fbe48c21f90, _cmd=0x0000000104f5dba9, application=0x00007fbe48f00fb0, launchOptions=0x0000000000000000) + 571 at AppDelegate.m:26
    frame #14: 0x000000010477c5a5 UIKit`-[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 234
    frame #15: 0x000000010477d0ec UIKit`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 2463
    frame #16: 0x000000010477fe5c UIKit`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1350
    frame #17: 0x000000010477ed22 UIKit`-[UIApplication workspaceDidEndTransaction:] + 179
    frame #18: 0x00000001088092a3 FrontBoardServices`__31-[FBSSerialQueue performAsync:]_block_invoke + 16
    frame #19: 0x000000010615fabc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    frame #20: 0x0000000106155805 CoreFoundation`__CFRunLoopDoBlocks + 341
    frame #21: 0x00000001061555c5 CoreFoundation`__CFRunLoopRun + 2389
    frame #22: 0x0000000106154a06 CoreFoundation`CFRunLoopRunSpecific + 470
    frame #23: 0x000000010477e799 UIKit`-[UIApplication _run] + 413
    frame #24: 0x0000000104781550 UIKit`UIApplicationMain + 1282
    frame #25: 0x00000001040f7793 CDProgress`main(argc=1, argv=0x00007fff5bb09308) + 115 at main.m:16
    frame #26: 0x000000010686f145 libdyld.dylib`start + 1
(lldb) 

Здесь происходит то, что, когда модель загружается, она читает файл plist. Чтение вызовов plist -[NSData initWithContentsOfFile:], который вызывает +[NSProgress progressWithTotalUnitCount:] в основной теме. Как отмечено в примечаниях к выпуску, это создаст NSProgress, который является дочерним по отношению к текущему прогрессу. initWithContentsOfFile: на самом деле делает это, и создает нового ребенка из NSProgress Вы создали:

<NSProgress: 0x7f9353596f80> : Parent: 0x0 / Fraction completed: 0.0000 / Completed: 0 of 1  
   <_NSProgressGroup: 0x7f935601a0d0> : Portion of parent: 100 Children: 1
      <NSProgress: 0x7f935600bf50> : Parent: 0x7f9353596f80 / Fraction completed: 0.0000 / Completed: 0 of 0 

Здесь происходит то, что перед вами добавляется дополнительная работа. На данный момент он ничего не знает о дополнительной работе, которую вы собираетесь добавить. Ребенок добавлен initWithContentsOfFile: завершает, удаляется из дерева, а затем вы начинаете добавлять свою работу.

Текущий прогресс начинается с 0 и достигает 100%. Вы видите 100%, потому что ваши параметры KVO не включают NSKeyValueObservingOptionInitial,

NSData добавляет прогресс ребенка, который начинается с 0 и достигает 100%.

Ваша задача Core Data добавляет дочерний элемент, который начинается с 0 и (в конце концов) становится равным 100%.

Ключевым моментом здесь является то, что вы используете performBlockAndWait:, Хотя сам блок выполняется в частной очереди, этот метод заблокирует вызывающий поток, что приведет к задержке ваших уведомлений KVO. performBlockAndWait: также, если возможно, будет повторно использовать вызывающий поток, о чем следует знать.

Если вы редактируете свой subTask метод обернуть себя с NSProgress, чтобы служить родителем для всей единицы работы, отставив текущий в конце, вы, вероятно, получите поведение ближе к тому, что вы ожидаете:

- (void)subTask {
    NSProgress  *progress   = [NSProgress progressWithTotalUnitCount:1];
    NSManagedObjectContext *parentContext = self.managedObjectContext;
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [bgContext setParentContext:parentContext];

    [progress becomeCurrentWithPendingUnitCount:1];
    [bgContext performBlock:^{

    ... stuff

    [progress resignCurrent];
}

NSProgress может быть немного трудно обернуть голову, но с некоторым опытом это становится легче. Я обещаю!

Похоже, там должен быть счетчик NSProgress внутри [NSManagedObjectModel initWithContentsOfURL:], Перед тем как войти [self subTask]Вы настраиваете себя на получение уведомлений о любых индикаторах прогресса (путем настройки _progress как текущие, так и регистрирующие себя, чтобы наблюдать за изменениями). Затем внутри этой рутины вы называете ленивым добытчиком self.managedObjectContextкоторый в свою очередь вызывает [NSManagedObjectModel initWithContentsOfURL:], который, по-видимому, имеет счетчик прогресса в 2 единицы. Кажется, вам нужно быть очень осторожным, когда вы размещаете звонки [NSProgress becomeCurrentWithPendingUnitCount:] а также [NSProgress resignCurrent],

Другие вопросы по тегам