Блокировка exposureMode с наблюдателем ключевых значений вызывает сбой

Я делаю некоторое обнаружение движения в области экрана. Перед началом обнаружения я хочу установить фокус и экспозицию и заблокировать их, чтобы они не запускали ложное движение. Поэтому я отправляю AVCaptureFocusModeAutoFocus и AVCaptureExposureModeAutoExpose на устройство и добавляю KeyvalueObserver. Когда наблюдатель говорит, что он закончил фокусировку и изменение экспозиции, он блокирует их (и начинает обнаружение движения). Все работает отлично с фокусом, но блокировка экспозиция сбой приложения в течение нескольких секунд", несмотря на наличие идентичного кода в обоих случаях.

static void * const MyAdjustingFocusObservationContext = (void*)&MyAdjustingFocusObservationContext;
static void * const MyAdjustingExposureObservationContext = (void*)&MyAdjustingExposureObservationContext;

-(void)focusAtPoint{

   CGPoint point;
   if(fromRight) point.x = 450.0/480.0;
   else point.x = 30.0/480.0;
   point.y = 245.0/320.0;

   AVCaptureDevice *device =[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

   if(device != nil) {
       NSError *error;
       if([device lockForConfiguration:&error]){

          if([device isExposureModeSupported:AVCaptureFocusModeContinuousAutoFocus] && [device isFocusPointOfInterestSupported]) {
             [device setFocusPointOfInterest:point];
             [device setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
             [device addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:MyAdjustingFocusObservationContext];
             NSLog(@"focus now");
          }

          if([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure] && [device isExposurePointOfInterestSupported]) {
             [device setExposurePointOfInterest:point];
             [device setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
             [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:MyAdjustingExposureObservationContext];
             NSLog(@"expose now");
          }

          [device unlockForConfiguration];
      }else{
        NSLog(@"Error in Focus Mode");
      }        
  }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

   AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
  NSError *error;

  if([keyPath isEqualToString:@"adjustingFocus"]){   
    if(![object isAdjustingFocus]){
       [device removeObserver:self forKeyPath:keyPath context:context];
       if([device isFocusModeSupported:AVCaptureFocusModeLocked]) {
          [device lockForConfiguration:&error];
          device.focusMode = AVCaptureFocusModeLocked;
          [device unlockForConfiguration];
          NSLog(@" focus locked");
       }
    }
  }

  if([keyPath isEqualToString:@"adjustingExposure"]){    
    if(![object isAdjustingExposure]){
       [device removeObserver:self forKeyPath:keyPath context:context];
       if([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
          [device lockForConfiguration:&error];
          device.exposureMode=AVCaptureExposureModeLocked; //causes the crash
          [device unlockForConfiguration];
          NSLog(@" exposure locked");
       }
    }
  }

Если я прокомментирую строку " устройство.exposureMode=AVCaptureExposureModeLocked " все работает нормально (за исключением того, что фокус не блокируется). Если я перемещаю линию в фокус-наблюдатель, все работает нормально (за исключением того, что экспозиция иногда блокируется до того, как она установлена правильно). Если я блокирую экспозицию каким-то другим способом, например, через таймер, она работает.

журнал сбоев не помогите мне (Надеюсь, кто-то может интерпретировать его)

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x00000000
Crashed Thread:  0

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   Foundation                      0x3209d5e2 NSKVOPendingNotificationRelease + 6
1   CoreFoundation                  0x317b21c8 __CFArrayReleaseValues + 352
2   CoreFoundation                  0x317419f8 _CFArrayReplaceValues + 308
3   CoreFoundation                  0x3174391c CFArrayRemoveValueAtIndex + 80
4   Foundation                      0x3209d6b6 NSKeyValuePopPendingNotificationPerThread + 38
5   Foundation                      0x32090328 NSKeyValueDidChange + 356
6   Foundation                      0x3206a6ce -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 90
7   AVFoundation                    0x30989fd0 -[AVCaptureFigVideoDevice handleNotification:payload:] + 1668
8   AVFoundation                    0x30983f60 -[AVCaptureDeviceInput handleNotification:payload:] + 84
9   AVFoundation                    0x3098fc64 avcaptureSessionFigRecorderNotification + 924
10  AVFoundation                    0x309b1c64 AVCMNotificationDispatcherCallback + 188
11  CoreFoundation                  0x317cee22 __CFNotificationCenterAddObserver_block_invoke_0 + 122
12  CoreFoundation                  0x31753034 _CFXNotificationPost + 1424
13  CoreFoundation                  0x3175460c CFNotificationCenterPostNotification + 100
14  CoreMedia                       0x31d3db8e CMNotificationCenterPostNotification + 114
15  Celestial                       0x34465aa4 FigRecorderRemoteCallbacksServer_NotificationIsPending + 628
16  Celestial                       0x34465826 _XNotificationIsPending + 66
17  Celestial                       0x344657dc figrecordercallbacks_server + 96
18  Celestial                       0x34465028 remrec_ClientPortCallBack + 172
19  CoreFoundation                  0x317cc5d8 __CFMachPortPerform + 116
20  CoreFoundation                  0x317d7170    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 32
21  CoreFoundation                  0x317d7112 __CFRunLoopDoSource1 + 134
22  CoreFoundation                  0x317d5f94 __CFRunLoopRun + 1380
23  CoreFoundation                  0x31748eb8 CFRunLoopRunSpecific + 352
24  CoreFoundation                  0x31748d44 CFRunLoopRunInMode + 100
25  GraphicsServices                0x3530c2e6 GSEventRunModal + 70
26  UIKit                           0x3365e2fc UIApplicationMain + 1116
27  ShootKing                       0x000ed304 main (main.m:16)
28  ShootKing                       0x000ed28c start + 36

1 ответов


вы не найдете этого ни в одной документации (т. е. у меня нет "доказательства"), но я могу сказать вам из болезненного, личного опыта, состоящего из многих дней (если не недель) отладки, что этот вид сбоя вызван добавлением/удалением наблюдателей для свойства внутри обработчика уведомлений KVO для этого свойства. (Присутствие NSKeyValuePopPendingNotificationPerThread в трассировке стека есть "дымящийся пистолет" в моем опыте.) Я также эмпирически заметил, что порядок, в котором наблюдатели данного свойства уведомление недетерминировано, поэтому даже если добавление или удаление наблюдателей внутри обработчиков уведомлений работает некоторые времени, он может не произвольно в разных обстоятельствах. (Я предполагаю, что где-то в кишках кво есть неупорядоченная структура данных, которая может быть перечислена в разных порядках, возможно, на основе числового значения указателя или чего-то произвольного.) В прошлом я работал над этим, немедленно разместив NSNotification до / после установки свойства, чтобы дать наблюдателям возможность добавлять / удалять себя. Это неуклюжий шаблон, но это лучше, чем сбой (и позволяет мне продолжать использовать другие вещи, которые полагаются на KVO, такие как привязки.)

кроме того, просто в стороне, я замечаю в коде, который вы опубликовали, что вы не используете контексты для идентификации своих наблюдений, и вы не вызываете super в своем observeValueForKeyPath:... реализация. Обе эти вещи могут привести к тонким, труднодиагностируемым ошибкам. Более пуленепробиваемый шаблон для кво выглядит так:

static void * const MyAdjustingFocusObservationContext = (void*)&MyAdjustingFocusObservationContext;
static void * const MyAdjustingExposureObservationContext = (void*)&MyAdjustingExposureObservationContext;

- (void)focusAtPoint
{
    // ... other stuff ...
    [device addObserver:self forKeyPath:@"adjustingFocus" options:NSKeyValueObservingOptionNew context:MyAdjustingFocusObservationContext];
    [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:MyAdjustingExposureObservationContext];
    // ... other stuff ...
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 
{
    if (context == MyAdjustingFocusObservationContext)
    {
        // Do stuff
    }
    else if (context == MyAdjustingExposureObservationContext)
    {
        // Do other stuff
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

EDIT: я хотел продолжить, чтобы узнать, могу ли я больше помочь в этой конкретной ситуации. Из кода и ваших комментариев я заключаю, что вы ищете, чтобы эти наблюдения были фактически одноразовыми. Я вижу два способа сделать это:

более простой и пуленепробиваемый подход был бы для этого объекта всегда наблюдайте за устройством захвата (т. е. addObserver:... когда вы init,removeObserver:... когда вы dealloc), но затем "ворота" поведение с помощью пары Ивар называется waitingForFocus и waitingForExposure. В -focusAtPoint где вы в настоящее время addObserver:... вместо этого установите ivars в YES. Потом в observeValueForKeyPath:... только принять меры, если эти ивары YES и removeObserver:... просто установите ivars в NO. Это должно иметь желаемый эффект без необходимости добавлять и удалять замечания каждый раз.

другой подход, о котором я думал, это позвонить removeObserver:... "далее" с помощью GCD. Таким образом, вы измените removeObserver:... такой:

    dispatch_async(dispatch_get_main_queue(), ^{ [device removeObserver:self forKeyPath:keyPath context:context]; });

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

EDIT 2: я просто не мог отпустить. :) Я понял, почему ты разбиваешься. Я наблюдаю за этой обстановкой exposureMode в то время как в обработчике KVO для adjustingExposure заканчивается тем, что вызывает другое уведомление для adjustingExposure, и поэтому стек взрывается, пока ваш процесс не будет убит. Я смог заставить его работать, обернув часть observeValueForKeyPath:..., который обрабатывает изменения adjustingExposure на dispatch_async(dispatch_get_main_queue(), ^{...}); (включая возможное removeObserver:... звонок). После этого он работал для меня, и определенно фиксировать экспозицию и фокус. Тем не менее, как я уже упоминал примерно выше, это, возможно, было бы лучше обработано с помощью ivars, чтобы предотвратить рекурсию, а не произвольно задержанную dispatch_async().

надеюсь, это поможет.