The statement

Some people say that this is the so-called black magic, I declare here: this item does not have any black magic, no harm to the original code, just the registration method encapsulation.

The problem

As we all know, when using KVO mode to listen on a property, the Observer needs to be removed at the necessary moment, otherwise the App will inevitably Crash. This problem is a bit annoying because occasionally you forget to write code to remove the Observer…

I’ve always wanted an effect that just listens and handles the listening method. Remove the Observer without distraction and allow it to automatically process when appropriate.

Fortunately, it works. Here’s a preview:

@interface NSObject (SJObserverHelper)



- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;



@end



@interface SJObserverHelper : NSObject

@property (nonatomic, unsafe_unretained) id target;

@property (nonatomic, unsafe_unretained) id observer;

@property (nonatomic, strong) NSString *keyPath;

@property (nonatomic, weak) SJObserverHelper *factor;

@end



@implementation SJObserverHelper

- (void)dealloc {

    if ( _factor ) {

        [_target removeObserver:_observer forKeyPath:_keyPath];

    }

}

@end



@implementation NSObject (ObserverHelper)



- (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {

    

    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];

    

    SJObserverHelper *helper = [SJObserverHelper new];

    SJObserverHelper *sub = [SJObserverHelper new];

    

    sub.target = helper.target = self;

    sub.observer = helper.observer = observer;

    sub.keyPath = helper.keyPath = keyPath;

    helper.factor = sub;

    sub.factor = helper;

    

    const char *helpeKey = [NSString stringWithFormat:@"%zd", [observer hash]].UTF8String;

    objc_setAssociatedObject(self, helpeKey, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    objc_setAssociatedObject(observer, helpeKey, sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}



@end



Copy the code

Program source code

Step by step implementation:

Preliminary idea realization

As we all know, before an object is released, the dealloc method is called and the instance variables it holds are also released.

I would like to associate a temporary object with self and Observer while listening on registration. When both are releasing instance variables, I use this opportunity to remove the Observer from the temporary object’s dealloc method.

That’s a good idea, but you can’t add a temporary object property to every class. So how do you associate a temporary object with an existing class without changing it?

The attribute

There are a lot of categories in the system framework, and there are a lot of associated properties, as shown in line 180 of UIView header:


Following the figure above, let’s look at an example of adding a Category to NSObject and adding a property that implements its setter and getter methods in dot m.

#import <objc/message.h>



@interface NSObject (Associate)

@property (nonatomic, strong) id tmpObj;

@end

@implementation NSObject (Associate)



static const char *testKey = "TestKey";

- (void)setTmpObj:(id)tmpObj {

    // objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

    objc_setAssociatedObject(self, testKey, tmpObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}



- (id)tmpObj {

    // objc_getAssociatedObject(id object, const void *key)

    return objc_getAssociatedObject(self, testKey);

}

@end

Copy the code

Explicitly, objc_setAssociatedObject is the setter method for the association property, and objc_getAssociatedObject is the getter method for the association property. The setter method is the one we need to focus on, because we’re going to use it to add the associated property object.

Preliminary Thinking exploration

Preliminary attempt: Now that properties can be associated with objc_setAssociatedObject at any time, I’ll try associating a temporary object with self first, in its dealloc, removing the Observer.

@interface SJObserverHelper : NSObject

@property (nonatomic, weak) id target;

@property (nonatomic, weak) id observer;

@property (nonatomic, strong) NSString *keyPath;

@end



@implementation SJObserverHelper

- (void)dealloc {

    [_target removeObserver:_observer forKeyPath:_keyPath];

}

@end



- (void)addObserver {

    NSString *keyPath = @"name";

    [_xiaoM addObserver:_observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];

    

    SJObserverHelper *helper_obj = [SJObserverHelper new];

    helper_obj.target = _xiaoM;

    helper_obj.observer = _observer;

    helper_obj.keyPath = keyPath;



    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;

/ / associated

    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

Copy the code

When _xiaoM is set to nil, bang App Crash……

reason: 'An instance 0x12cd1c370 of class Person was deallocated while key value observers were still registered with it.
Copy the code

Analysis: The temporary object’s dealloc did indeed run. Why registered? So I tried to print the instance variable target in the temporary object’s dealloc and found that it was nil. Well, that’s why Crash is a problem!


Try unsafe_unretained

From the above we know that self will release the associated property it holds before being freed. During the destruction, we can be sure that self still exists, not completely freed, but in the temporary object, target becomes nil. So how do we keep it from being nil?

Let’s take a look at the two modifiers of OC: weak and unsafe_unretained:

  • Weak: The holder’s instance variable will not retain the target and will be empty when the target is destroyed
  • Unsafe_unretained: The owner’s instance variable does not retain the target. After the target is freed, the owner’s instance variable still points to the previous memory space (wild pointer).

As mentioned above, unsafe_unretained solved our problem perfectly. So I made the following changes:

@interface SJObserverHelper : NSObject

@property (nonatomic, unsafe_unretained) id target;

@property (nonatomic, unsafe_unretained) id observer;

@property (nonatomic, strong) NSString *keyPath;

@end

Copy the code

Run the program again, ok, observer removed.


Finally realize

There are still problems

So far, we have only implemented how to remove the Observer from self when self is released. But what if the Observer were released early? However, with the addition of the association attribute, the two can not hold the temporary object at the same time, otherwise the temporary object will not be released in time.


Well, since one doesn’t work, let’s associate one of each:

- (void)addObserver {

.

    

    SJObserverHelper *helper_obj = [SJObserverHelper new];

    SJObserverHelper *sub_obj = [SJObserverHelper new];



    sub_obj.target = helper_obj.target = _xiaoM;

    sub_obj.observer = helper_obj.observer = _observer;

    sub_obj.keyPath = helper_obj.keyPath = keyPath;



    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;

/ / associated

    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

/ / associated

    objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

Copy the code

As mentioned above, there is an obvious problem that two association properties are released simultaneously with two observation removal operations. To avoid this problem, I made the following modifications:

@interface SJObserverHelper : NSObject

@property (nonatomic, unsafe_unretained) id target;

@property (nonatomic, unsafe_unretained) id observer;

@property (nonatomic, strong) NSString *keyPath;

@property (nonatomic, weak) SJObserverHelper *factor; // 1. Add a weak variable

@end



@implementation SJObserverHelper

- (void)dealloc {

    if ( _factor ) {

        [_target removeObserver:_observer forKeyPath:_keyPath];

    }

}

@end



- (void)addObserver {

.

    

    SJObserverHelper *helper_obj = [SJObserverHelper new];

    SJObserverHelper *sub_obj = [SJObserverHelper new];



    sub_obj.target = helper_obj.target = _xiaoM;

    sub_obj.observer = helper_obj.observer = _observer;

    sub_obj.keyPath = helper_obj.keyPath = keyPath;

// 2. Weak references

    helper_obj.factor = sub_obj;  

    sub_obj.factor = helper_obj;



    const char *helpeKey = [NSString stringWithFormat:@"%zd", [_observer hash]].UTF8String;

/ / associated

    objc_setAssociatedObject(_xiaoM, helpeKey, helper_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

/ / associated

    objc_setAssociatedObject(_observer, helpeKey, sub_obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

Copy the code

In the previous operation, we know that the owner’s instance variable is automatically set to nil when the target is released. Therefore, in the dealloc method above, we only need to check whether the instance variable factor referenced by weak is null.

extracting

By doing so, we can solve the problem of occasionally forgetting to write code to remove an Observer. Now you just need to extract the implementation into a generic utility method:

I created a new Category for NSObject and added a method like this:

Then integrate the above implementation into.m:

After that, you can just call – (void)sj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; This method can be removed by the temporary variable itself.

Conclusion: can see here, old iron is true love, can help younger brother to point a Star. Over…