Last article we analyzed the principle of KVO, so can we customize our own KVO? Main ideas:

  1. addObserver, create an intermediate class,isaReplacement.
  2. The middle classSetter implementationIncluding callbacks, superclassessetterMethod calls.
  3. removeOperation,Isa reduction.

1. Create an intermediate class

We create a custom KVO classification based on NSObject

@interface NSObject (KBKVO)

/ / add

- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// Observe the callback- (void)kb_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change  context:(void *)context;
/ / remove

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

@end
Copy the code

1.1 Determination of setter methods

Our KVO is based on listening to setter methods, so we add observers first by making sure that the keypath implements the setter method.

 [self judgeMethodIsExist:keyPath];// Whether the setter method is implemented- (void)judgeMethodIsExist:(NSString*)keyPath

{

    Class class = object_getClass(self);

    NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];

    SEL sel = NSSelectorFromString(setKey);

    Method method = class_getInstanceMethod(class.sel);

    if(! method) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Kvo cannot be used without implementing the setter method for %@.",keyPath] userInfo:nil]; }}Copy the code

Let’s start by observing KBPerson member variable nickName, which does not implement setter method, and enter error logic

1.2 Intermediate class implementation

-(Class)createSubClassWithKeyPath:(NSString*)keyPath

{

    NSString *superClassName = NSStringFromClass(self.class);

    NSString *associateClassName = [NSString stringWithFormat:@"% @ % @",kKBKVOProfix,superClassName];

    Class associateClass = NSClassFromString(associateClassName);

    if (associateClass) {

        return  associateClass;//1. If it exists, return it directly without creating it

    }

    associateClass = objc_allocateClassPair(self.class, associateClassName.UTF8String, 0);/ / 2.1 application class

    

    objc_registerClassPair(associateClass);/ / 2.2 registered

    // 2.3.1: add a class: the class points to KBPerson

    SEL classSEL = NSSelectorFromString(@"class");

    Method classMethod = class_getInstanceMethod([self class].classSEL);

    const char *classTypes = method_getTypeEncoding(classMethod);

    class_addMethod(associateClass, classSEL, (IMP)kb_class, classTypes);
    
    // 2.3.2 Setter method added

    NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];

    SEL sel = NSSelectorFromString(setKey);/ / the method name

    

    Method method = class_getInstanceMethod(self.class, sel);

    const char *type = method_getTypeEncoding(method);// Method type

    

    class_addMethod(associateClass,sel, (IMP)kb_setter, type);// Add the method

    

    

    return associateClass;

    

    

}
Class kb_class(id self,SEL _cmd){

    return class_getSuperclass(object_getClass(self));

}
static void kb_setter(id self,SEL _cmd,id newValue){

    

    NSLog(@"Here comes the customization :%@",newValue);

}
Copy the code
  1. Let’s judge firstDoes it already exist?, does not exist to create.
  2. To apply forAssociation class, and thenregisteredAssociation class.
  3. Add to the associated classsetterMethod and implement it.

1.3 Intermediate classes point to instances

- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{

    

    //1. Interpret the setter method implementation

    [self judgeMethodIsExist:keyPath];

    

    //2. Implement the intermediate class

    Class newClass = [self createSubClassWithKeyPath:keyPath];

    

    

    //3. Isa refers to the association class

    

    object_setClass(self, newClass);

       

}
Copy the code

We’re looking at the property name, which goes into the setter method of the intermediate class.

2. Setter method implementation

2.1 Normal Logic

It is understood that we will send a noticeThe system has changedProperty value, while calling the parent classsetterMethod, print as follows:When the system is called to send messages, an error will be reportedToo many arguments to function call, expected 0, have 3Let’s modify it according to the picture.Can also beobjc_msgSendSuperFunction is a function that returns no arguments, so you need to put itForced conversiontype

void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;

// objc_msgSendSuper(&superStruct,_cmd,newValue);

    kb_msgSendSuper(&superStruct,_cmd,newValue);
Copy the code

After the notification observer callback, we need to send a message to the observer. So first we save the observer

NSObject *oldObserver =  objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    if(! oldObserver) { objc_setAssociatedObject(self, (__bridgeconst void * _Nonnull)(kKBKVOAssociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }
Copy the code

After the call, but also to record the change of value, record keypath is very troublesome to pass.

SEL observerSEL = @selector(kb_observeValueForKeyPath:ofObject:change:context:);

objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
Copy the code

2.2 Functional programming

Our daily iOS development is object-oriented development, based on MVC architecture pattern, some will use MVVM and MVP pattern to reduce the coupling. But sometimes modules have relationships that are difficult to decouple, and using functional programming can solve some of the coupling problems. For example, if f=x(), we can pass the entire function as an argument. F = x(x()). In iOS, we often use block as a parameter. I’ll have a chance to write about functional thinking and chained syntax.

  1. We write the notification callback directly in the add method, so it can be ourslogicMore closely,RAC frameworkFor suchblockCallbacks encapsulate many system functions, such as monitoringtextFieldDirect change inBlock the callback.buttonClick on theBlock the callbackAnd so on.
typedef void(^KBKVOBlock)(NSObject* observer,NSString*keyPath,id oldValue,id newValue); - (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block;
Copy the code

First of all we need to save the observer, keypath, and option, so we create a class that holds that information

// Copy the system

@interface KBKVOInfo : NSObject

@property (nonatomic, weak) NSObject  *observer;// Prevent circular references

@property (nonatomic, copy) NSString    *keyPath;

@property (nonatomic, copy) KBKVOBlock handleBlock;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block;

@end
@implementation KBKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block{

    if (self=[super init]) {

        _observer = observer;

        _keyPath  = keyPath;

        _handleBlock = block;

    }

    return self;

}
@end
Copy the code

We put the observer, keyPath and block in the INFO when we add observers and save the info

- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * link * * * * * * * * * * * * * * * * * * * * KBKVOInfo info = [[KBKVOInfo alloc]initWitObserver:observer forKeyPath:keyPath handBlock:block];//4.2 Save. For reusability, we put the info into an array for association

    NSMutableArray *arr  = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    if(! arr) { arr = [NSMutableArray arrayWithCapacity:1];/ / for the first time

        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    

    }

    [arr addObject:info];
  }
Copy the code

Implement the kb_setter:

static void kb_setter(id self,SEL _cmd,id newValue){

    //1. Calculate the old value

    NSString *keyPath =  getterForSetter( NSStringFromSelector(_cmd));

    id oldValue = [self valueForKey:keyPath];

    

    //1. Call the method of the superclass

    Class superClass = class_getSuperclass(object_getClass(self));

    struct objc_super superStruct = {

    .receiver = self,

    .super_class = superClass,

    };

    void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;

// objc_msgSendSuper(&superStruct,_cmd,newValue);

    kb_msgSendSuper(&superStruct,_cmd,newValue);

    NSLog(@"Here comes the customization :%@",newValue);

    //2. Tell the system about the change

   
    NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    for (KBKVOInfo *info in arr) {

        if(info.handleBlock && [info.keyPath isEqualToString:keyPath]) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

Fetch the associative array, find the info object based on the keyPath, and call the callback.

3. Remove the observer and restore ISA

Last step in the trilogy, remove. We know that removeObserver mainly does isa restore. Because the KBKVOInfo information is stored in the array of our custom associated objects, we need to remove it.

- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath

{

    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    if (observerArr.count<=0) {

        return;

    }

    

    for (KBKVOInfo *info in observerArr) {

        if ([info.keyPath isEqualToString:keyPath]) {

            [observerArr removeObject:info];

            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

            break; }}if (observerArr.count<=0) {

        // Return to the parent class

        Class superClass = [self class]; object_setClass(self, superClass); }}/ / call- (void)dealloc

{

    [self.person kb_removeObserver:self forKeyPath:@"name"];

}
Copy the code

4. Automatic removal

Since we need to manually remove the operation every time in dealloc, is there an automatic way to do it? We listen to the subclass’s dealloc method, and when dealloc is called by an observer, we call the remove operation. We’ve seen before that methd-Swizzing method swap, so we can swap dealloc subclasses.

+ (void)load

{
/ / exchange
    [self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)]; } - (void)customeDealloc

{

   //还原isa
    Class superClass = [self class];

    object_setClass(self, superClass);

}
// Method exchange, specific can understand methd-swizzing
+ (BOOL)kb_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {

    Class cls = self;

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);

    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

    

    if(! swiMethod) {return NO;

    }

    if(! oriMethod) { class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ })); } BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));if (didAddMethod) {

        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

    }else{

        method_exchangeImplementations(oriMethod, swiMethod);

    }

    return YES;

}
Copy the code

There are two problems with writing this way

  1. Our classification rewrite will result inAll the classesIt’s all switched.
  2. +loadThe code inside the method doesn’t necessarilyOnly go once.
+ (void)load

{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        // Execute only once to prevent swapping multiples of 2, swapping back.

        [self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)]; }); } - (void)customeDealloc

{
    Class superClass = [self class];

    object_setClass(self, superClass);

    [self customeDealloc];// After the imp swap, again call implementation of the original dealloc method.

}
Copy the code

Modified: 1. After the exchange of IMP, again call the implementation of the original dealloc method, does not affect the original method call. 2. To prevent active invocation, use the singleton form only to execute once, to prevent the exchange of 2 multiples, exchange back.

5. To summarize

The customization of KVO is roughly divided into three steps

  1. The middle classcreate(setterWhether the method is implemented, the implementation of the intermediate class,isaPointing to the intermediate class)
  2. setterMethod implementation (byblockFor processing)
  3. remove(Method exchange, automatic removal,Reduction of the isa)

In the process, we learn the idea of functional programming, but there are some disadvantages, such as automatic removal without removing the information stored in the associative array. Git has a custom implementation of KVO. For those interested, see KVOController.