Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

Custom KVO multi-element observation and removal observation

Multielement observation

We have implemented the method of listening for the nickName. What if we need to listen for multiple properties? For example, we need to listen for both nickName and realName of Person;

@interface Person : NSObject
@property (nonatomic.copy) NSString *nickName;
@property (nonatomic.copy) NSString *realName;
@end
Copy the code

Now obviously the way we saved the observer is no longer appropriate;

objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCKKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Copy the code

At this point we need to collect the observer, for example we create a new class CKKVOInfo:

typedef NS_OPTIONS(NSUInteger, CKKeyValueObservingOptions) {
    CKKeyValueObservingOptionNew = 0x01,
    CKKeyValueObservingOptionOld = 0x02};@interface CKKVOInfo : NSObject
@property (nonatomic.copy) NSString *keyPath;
@property (nonatomic.weak) NSObject *observer; // The weak modifier needs to be used, otherwise circular references will occur
@property (nonatomic.assign) CKKeyValueObservingOptions options;

/// initialize method
/// @param observer observer description
/// @param keyPath keyPath description
/// @param options options description
- (instancetype)initWithObserver:(NSObject *)observer
                      forKeyPath:(NSString *)keyPath
                         options:(CKKeyValueObservingOptions)options;
@end


@implementation CKKVOInfo
- (instancetype)initWithObserver:(NSObject *)observer
                      forKeyPath:(NSString *)keyPath
                         options:(CKKeyValueObservingOptions)options {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath = keyPath;
        self.options = options;
    }
    return self;
}
@end
Copy the code

We use the CKKVOInfo class to hold keyPath and Observer data. So outside we need an array of CKKVOInfo to collect information;

The code is modified as follows:

Note the code here:

// Send a message to the observer
SEL observerSEL = @selector(ck_observeValueForKeyPath:ofObject:change:context:);
((void (*) (id, SEL, NSString *, id.NSMutableDictionary *, void(*))void *)objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL);
Copy the code

It used to be possible to modifyXcodeSet the itemThe code can be written as:

// Send a message to the observer
SEL observerSEL = @selector(ck_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer, observerSEL, keyPath, self, change, NULL);
Copy the code

Xcode13, however, will return an error:

Too many arguments to function call, expected 0, have 6

Apple doesn’t want developers to use too much of the underlying API;

Remove observer

  • Remove one at a timekeyPath, we need to go frommArrayRemove aCKKVOInfo;
  • From one anothermArrayRemove theCKKVOInfoAfter that, we all need to resetmArray;
  • object_setClassTo modify theisaPoint it to the originalThe parent class;

Self-destruct of custom KVO

In the previous custom KVO, we found that we needed to manually call the ck_removeObserver method in dealloc to destroy KVO

- (void)dealloc {
    [self.person ck_removeObserver:self forKeyPath:@"nickName"];
    [self.person ck_removeObserver:self forKeyPath:@"realName"];
    NSLog(@"%s", __func__);
}
Copy the code

Is there an easier way, instead of calling it manually, to have it call auto-destruct?

At this point we might think, listen to the dealloc method; Use method swaps to release listeners and remove KVO in the dealloc method we swapped.

Method oriMethod = class_getInstanceMethod([self class].NSSelectorFromString(@"dealloc"));
Method swiMethod = class_getInstanceMethod([self class].@selector(myDealloc));
method_exchangeImplementations(oriMethod, swiMethod);
Copy the code

So is that enough? If the parent class does not have a dealloc method, it may cause unnecessary problems. This is also a runtime pit. We should only deal with this class and not affect other classes;

We can add a dealloc method to the class the first time we dynamically subclass it:

SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)ck_dealloc, deallocTypes);
Copy the code

We put the dealloc method in a dynamic subclass, so that even if this class implements something else wrong with dealloc, we don’t affect the original logic;

Give ck_dealloc an implementation:

static void ck_dealloc(id self,SEL _cmd){
	Class superClass = [self class];
	object_setClass(self, superClass);
}
Copy the code

This ensures that the dealloc method is always present when the method is exchanged;

The final code is as follows:

Next, let’s verify:

We are inViewControllerthedeallocMethod is not executedck_removeObserverMethod is printed at this timeself.personWe found out that he was actually pointing toDynamic subclass; We continue to run breakpoints; We found thatck_deallocBefore the method points to,selfStill pointing to dynamic subclasses whenck_deallocAfter the execution,selfPoints to thePerson.isaRedirect to this class; So that’s itKVOAutomatic destruction of;