Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Crash with removeObserver:forKeyPath: in Foundation

I having some problems with the following crash logs retrieved from the "Crashes" section in Xcode. Only few devices are affected by this crash report.

I have analyzed the problem but I guess it's a bug on Apple framework. But I cannot find a way to replicate it.

Here a similar discussion: Help with crash in removeObserver:forKeyPath:.

Any hints?

Thread 0 name: Thread 0 Crashed:

0 Foundation
0x23507591 _NSKeyValueReplaceObservationInfoForObject + 69 (NSKeyValueObserving.m:1166)

1 Foundation
0x23506fe7 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:] + 327 (NSKeyValueObserving.m:1552)

2 Foundation
0x23506b03 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:] + 163 (NSKeyValueObserving.m:1696)

3 Foundation
0x235069a7 -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:context:] + 219 (NSKeyValueObserving.m:1663)

4 ApplicationName 0x0002e233 -[Supervisor removeObjectObserver:forKeyPath:] + 115 (Supervisor.m:344)

where removeObjectObserver:forKeyPath: is

- (void) removeObjectObserver:(id)object forKeyPath:(NSString *)keyPath { 

    @try {        
        [object removeObserver:self forKeyPath:keyPath context:PrivateKVOContext];
    
    } @catch (NSException *exception) { }
}
like image 702
Lorenzo B Avatar asked May 11 '15 10:05

Lorenzo B


3 Answers

Observers in Objective-C must be used with extra attention: don't add the same observer multiples time to the same object's property, and wrap the removal if there is one :

  if ([self observationInfo]) {
        @try {
            [self removeObserver:self forKeyPath:keyPath];
        }
        @catch (NSException *exception) {}
    }

You are experiencing crashes because you try to remove twice the observer, or you are removing a non-existant observer.

You should add observers this way :

[yourObject addObserver:self forKeyPath:keypath options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil/yourContext];

EDIT: You may remove an observer on an already deallocate object, resulting in this crash.

  if (object && [self observationInfo]) {
    @try {
                [self removeObserver:self forKeyPath:keyPath];
            }
            @catch (NSException *exception) {}
}
like image 182
Loegic Avatar answered Nov 05 '22 00:11

Loegic


Normally you have an ivar to be able to know whether you object's keypath observing at the moment or not. Like @property(...) BOOL textFieldTextObserving; And your add/remove-observing methods should check this property before adding/removing to avoid of adding/removing observer twice. You also can use NSDictionary if there are many observing objects and keypaths (to keep @(BOOL) as objects and -identifiers as keys).

Anyway, doing things using @try-exception is not a recommended Objective-C way. Apple docs says:

"You should not use a try-catch block in place of standard programming checks for Objective-C methods. In the case of an NSArray, for example, you should always check the array’s count to determine the number of items before trying to access an object at a given index. The objectAtIndex: method throws an exception if you make an out-of-bounds request so that you can find the bug in your code early in the development cycle—you should avoid throwing exceptions in an app that you ship to users." https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/ErrorHandling/ErrorHandling.html

like image 5
Vladlex Avatar answered Nov 04 '22 23:11

Vladlex


It's too late to give an answer, but I'm faced with the same problem. So i decided to write this for other people.

Note: The main reason of crash is that you try to remove an observer before add.

I have created some extensions that will help you safely remove the observer. Swift 5.

You can now remove it before adding it, without crashing. Be sure you also delete an observer in deinit.

USAGE:

objectToObserve.safeRemoveObserver(self, keyPath: "myDate", context: &myContext)

EXTENSIONS:

extension NSRegularExpression {

convenience init(_ pattern: String) {
    do {
        try self.init(pattern: pattern)
    } catch {
        preconditionFailure("Illegal regular expression: \(pattern).")
    }
}

func matches(_ string: String) -> Bool {
    let range = NSRange(location: 0, length: string.utf16.count)
    return firstMatch(in: string, options: [], range: range) != nil
   }
 }

extension NSObject {

func safeRemoveObserver(_ observer: NSObject, keyPath: String, context: inout Int) {
    let result = checkIfAlreadyAdded(keyPath: keyPath, context: &context)

    if result {
        removeObserver(observer, forKeyPath: keyPath, context: &context)
    }
}

fileprivate func address(_ o: UnsafeRawPointer) -> Int {
    return Int(bitPattern: o)
}

fileprivate func checkIfAlreadyAdded(keyPath: String, context: inout Int) -> Bool {

    guard self.observationInfo != nil else { return false }

    let info = Unmanaged<AnyObject>
           .fromOpaque(self.observationInfo!)
           .takeUnretainedValue()

    let contextStr = NSString(format: "%p", address(&context))
    let infoStr = info.description ?? ""

    let regex = NSRegularExpression("\(keyPath).*[a-z].*\(contextStr)")
    let result = regex.matches(infoStr)

    return result
  }
}
like image 1
isHidden Avatar answered Nov 04 '22 23:11

isHidden