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

Я делаю некоторые обнаружения движения в области экрана. Перед началом обнаружения я хочу установить фокусировку и экспозицию и заблокировать их, чтобы они не вызывали ложного движения. Поэтому я отправляю 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");
       }
    }
  }

Если я закомментирую строку "device.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 в моём опыте - "дымящая пушка".) Я также эмпирически заметил, что порядок уведомления наблюдателей определенного свойства недетерминирован, поэтому даже если добавление или удаление наблюдателей в обработчиках уведомлений работает времени он может произвольно потерпеть неудачу при других обстоятельствах. (Я предполагаю, что в кишечнике KVO есть неупорядоченная структура данных, которая может быть перечислена в разных порядках, возможно, на основе числового значения указателя или чего-то подобного.) В прошлом я обходил это путем публикации 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];
    }
}

РЕДАКТИРОВАТЬ: Я хотел последовать, чтобы увидеть, могу ли я помочь больше в этой конкретной ситуации. Из кода и ваших комментариев я понял, что вы ищете, чтобы эти наблюдения были эффективными. Я вижу два способа сделать это:

Более простой и пуленепробиваемый подход заключается в том, чтобы этот объект всегда наблюдал устройство захвата (т.е. addObserver:... когда вы начинаете, removeObserver:... когда вы имеете дело с), но затем "гейт" поведение, используя пару иваров называется 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]; });

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

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

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

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