You won't find this in any of the documentation (i.e. I don't have "proof"), but I can tell you from painful, personal experience consisting of many days (if not weeks) of debugging, that this kind of crash is caused by adding/removing observers for a property inside a KVO notification handler for that property. (The presence of NSKeyValuePopPendingNotificationPerThread
in the stack trace is the "smoking gun" in my experience.) I've also empirically observed that the order in which observers of a given property are notified is non-deterministic, so even if adding or removing observers inside notification handlers works some of the time, it can arbitrarily fail under different circumstances. (I'm assuming there's an unordered data structure down in the guts of KVO somewhere that can get enumerated in different orders, perhaps based on the numerical value of a pointer or something arbitrary like that.) In the past, I've worked around this by posting an NSNotification immediately before/after setting the property to give observers an opportunity to add/remove themselves. It's a clunky pattern, but it's better than crashing (and allows me to continue using other things that rely on KVO, like bindings.)
Also, just as an aside, I notice in the code you posted that you're not using contexts to identify your observations, and you're not calling super in your observeValueForKeyPath:...
implementation. Both of these things can lead to subtle, hard-to-diagnose bugs. A more bullet-proof pattern for KVO looks like this:
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: I wanted to follow up to see if I could help more with this specific situation. I gather from the code and from your comments that you're looking for these observations to effectively be one-shots. I see two ways to do this:
The more straightforward and bulletproof approach would be for this object to always observe the capture device (i.e. addObserver:...
when you init, removeObserver:...
when you dealloc) but then "gate" the behavior using a couple of ivars called waitingForFocus
and waitingForExposure
. In -focusAtPoint
where you currently addObserver:...
instead set the ivars to YES
. Then in observeValueForKeyPath:...
only take action if those ivars are YES
and then instead of removeObserver:...
just set the ivars to NO
. This should have the desired effect without requiring you to add and remove the observation each time.
The other approach I thought of would be to call removeObserver:...
"later" using GCD. So you would change the removeObserver:...
like this:
dispatch_async(dispatch_get_main_queue(), ^{ [device removeObserver:self forKeyPath:keyPath context:context]; });
This will cause that call to be made elsewhere in the run loop, after the notification process has finished. This is slightly less bulletproof because there's nothing that guarantees that the notification won't be fired a second time before the delayed removal call occurs. In that regard the first approach is more rigorously "correct" in achieving the desired one-shot behavior.
EDIT 2: I just couldn't let it go. :) I figured out why you're crashing. I observe that setting exposureMode
while in a KVO handler for adjustingExposure
ends up causing another notification for adjustingExposure
, and so the stack blows up until your process gets killed. I was able to get it working by wrapping the portion of observeValueForKeyPath:...
that handles changes to adjustingExposure
in a dispatch_async(dispatch_get_main_queue(), ^{...});
(including the eventual removeObserver:...
call). After this it worked for me, and was definitely locking exposure and focus. That said, like I mentioned about above, this would arguably be better handled with ivars to prevent the recursion and not a arbitrarily-delayed dispatch_async()
.
Hope that helps.