KVO principle

For the principle of KVO, many people are quite clear. It goes something like this:

Assume that our class is the Object and its Object obj, when obj send addObserverForKeypath: keypath message, the system will do three things:

  1. Dynamically create aObjectThe subclass of “, with a customizable name hypothetically calledObject_KVONotify.
  2. At the same time, subclasses add methods dynamicallysetKeypath:Dynamically added methods are bound to a C function.
  3. callobject_setClassFunction to set the class of obj toObject_KVONotify.

This would be equivalent to establishing the following structure:

//Object
@interface Object: NSObject
@property (nonatomic, copy) NSString *keypath;
@end

@implementation Object
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@" --- Object observeValueForKeyPath:%@ ofObject:%@ change:%@ context:%@", keyPath, object, change, context);
}

-(NSString *) description{
    return [NSString stringWithFormat: @"This is %@ instance keypath = %@", self.class, self.keypath]; } @end //Object_KVONotify @interface Object_KVONotify: Object @end static void dynamicSetKeyPath(id obj, SEL sel, id v){ ... . } @implementation Object_KVONotify -(void)setKeypath:(NSString *)keypath{
    dynamicSetKeyPath(self, @selector(setKeyPath:), keypath); } @end //obj Object *obj = [[Object alloc] init]; object_setClass(obj, Object_KVONotify.class); Object_KVONotify *obj = [[Object_KVONotify alloc] init]Copy the code

That way, when we call

obj.keypath = "hello world";
Copy the code

What we’re actually calling is

dynamicSetKeyPath(self, @selector(setKeypath:), keypath);
Copy the code

Now dynamicSetKeyPath is going to do two things.

  1. Calling the parent classsetKeyPath:Methods.
  2. callobserveValueForKeyPathMethod to trigger a callback.

So the dynamicSetKeyPath function should look something like this:

static void dynamicSetKeyPath(id obj, SEL sel, id v){
    Method superMethod = class_getInstanceMethod(Object.class, sel);
    ((void (*)(id, Method, id))method_invoke)(obj, superMethod, v);
    NSMutableDictionary * change = [[NSMutableDictionary alloc] init];
    change[@"new"] = v;
    [obj observeValueForKeyPath:@"keypath" ofObject:obj change:change context:nil];
}
Copy the code

Or it

static void dynamicSetKeyPath(id obj, SEL sel, id v){
    object_setClass(obj, Object.class);
    [obj setValue: v forKey: @"keyPath"];
    object_setClass(obj, Object_Notify.class);
    [(Object *)obj observeValueForKeyPath: @"keypath" ofObject: objChange:@{@"new":v} context: nil];
}
Copy the code

Add the test code to the Object class

+(void)test{
    Object *obj = [[Object alloc] init];
    obj.keypath = @"inited";
    NSLog(@"% @", obj);
    object_setClass(obj, Object_KVONotify.class);
    obj.keypath = @"hello world";
}
Copy the code

Calling the test code produces the following input

This is Object instance keypath = inited
Object observeValueForKeyPath:keypath ofObject:This is Object_KVONotify instance keypath = hello world change:{
    new = "hello world";
} context:(null)
Copy the code

The above process is KVO specific process and test code. The demo code can be found here.

KVO pain points

As you all know, system KVO is a bit difficult to use, mainly because of several points:

  1. addObserverAfter the object is released, it will not be automatically released, we can only release indeallocIn the manualremoveObserver. So in the case of negligence forgetremoveObserverIt could cause a crash. In addition, this restriction prevents us from adding listeners to other class objects in one class.
  2. If there is noaddObserverCan’t beremoveObserverCrash.
  3. Blocks are not supported.

Re-implement KVO

To re-implement KVO, according to KVO principles, we need to create a function that adds listeners and do so inside the function:

  1. Dynamically creates a subclass of the current class with a fixed suffix_NotifyKVO.
  2. At the same time, subclasses add methods dynamicallysetXXXX:Dynamically added methods are bound to a C function.
  3. callobject_setClassFunction to set the class of obj toXXXX_NotifyKVO.

First we create a class of NSObject, adding the create KVO method.

@implementation NSObject(BlockKVO) -(void) addObserverForKeyPath:(NSString *)keyPath option:(NSKeyValueObservingOptions)option block:((^)(id obj, NSDictionary<NSKeyValueChangeKey,id> *change))block{// self.blockkvo is bound to NSObject by associate [self. BlockKVO addObserver:selfforKeyPath:keyPath option:option block:block]; } // This overwrites the system KVO listener, which only calls the block that added the listener // this allows the system KVO listener to receive the events added by blockKVO. -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ BlockKVOItem *item = [self.blockKVO itemWithKeyPath:keyPath];if(item.block) {
        item.block(self, keyPath, change);
    }
}
@end
Copy the code

Since we have a lot of parameters and states to store, it’s cumbersome to keep attributes in the OC category. So we’ll create a new class to handle all the binding logic, passing all the parameters and the object itself into this class object.

Read the comments in the code carefully.

@implementation BlockKVO // obj is the object that needs kVO. This function is very important. It does two things. The advantage of pointing obJ's class to XXX_NotifyKVO is that AOP is implemented. The original class remains unchanged, obj still has access to all property methods of the original class, and obj can add functionality by extending XXX_NotifyKVO. You can also modify the behavior of the original class without affecting the structure of the original class. -(void) initKVOClassWithObj:(id) obj{if(self.srcClass == nil){ self.srcClass = [obj class]; // Add subclass NSString *dynamicClassName = [NSString stringWithFormat:@"%@_NotifyKVO", NSStringFromClass(self.srcClass)];
        Class dynamicClass = NSClassFromString(dynamicClassName);
        if(! dynamicClass) { dynamicClass = objc_allocateClassPair(self.srcClass, dynamicClassName.UTF8String, 0); objc_registerClassPair(dynamicClass); } self.dynamicClass = dynamicClass; // change the obj class to the newly created subclass, otherwise the dynamicSetKeyPath object_setClass(obj, dynamicClass) will not be adjusted; }} // This method takes arguments from the original class. It does two things: //1. Once you receive the parameters, save them in observers dictionary. //2. Add setter methods according to keyPath. -(void) addObserver: (id) objforKeyPath:(NSString *)keyPath option:(NSKeyValueObservingOptions)option block:(void (^)(id obj, NSString *keyPath, NSDictionary<NSKeyValueChangeKey,id> *change))block{
    [self initKVOClassWithObj:obj];
    
    if(self.observers == nil){
        self.observers = [[NSMutableDictionary alloc] init];
    }
    
    if(self.observers[keyPath] ! = nil){return; } // add methodSel methodSel = getSetSelector(keyPath); class_addMethod(self.dynamicClass, methodSel, (IMP)dynamicSetKeyPath,"v@:@"); // Save BlockKVOItem *item = [[BlockKVOItem alloc] init]; item.obj = obj; item.keyPath = keyPath; item.options = option; item.block = block; self.observers[keyPath] = item; } @endCopy the code

You’ll notice that the class_addMethod method has a strange string as its last argument. This string is used to represent the type of the method being added, including the return value type and any parameter types.

This thing is also called TypeEncoding, why is there such a thing?

As we know, OC is a dynamic language, and it sends messages to find the function through SEL. Once we find the function, we call it, and it is not a dynamic call, but a static call.

The number and type of static call parameters is important. A mismatch between the number of arguments and the type of arguments will cause the program to fail.

For the class_addMethod function, TypeEncoding marks the return value type, the number of arguments, and the type of each argument for the added method.

The “v@:@” above represents the added function pointer, which returns void and takes 3 arguments. The first argument is id, the second argument is SEL, and the third argument is id. Very simple.

OC class properties can be of many types, not just id. So if you want to call class_addMethod for different types, you write different TypeEncoding.

List the commonly used TypeEncoding :(see TypeEncoding here for more details)

  • “v@:q” => setKeyPath:(long long)
  • “v@:c” => setKeyPath:(char)
  • “v@:{CGSize=dd}” => setKeypPath:(CGSize)

Using the above code, when our object calls setKeyPath:, it actually calls the dynamicSetKeyPath function. Let’s look at its implementation:

// This function is defined in accordance with our typeEncoding:"v@:@"static void dynamicSetKeyPath(id obj, SEL sel, id value){ BlockKVO *blockKVO = [obj blockKVO]; // This is definitely not emptyif(blockKVO ! = nil) {// getKeyPath from SEL. NSString * keyPath = getKeyPath(SEL); // Get the parameters passed in when registering KVO, including block. BlockKVOItem *item = [blockKVO itemWithKeyPath:keypath]; Object_setClass (obj, blockKVO. SrcClass); Id oldValue = [obj valueForKey:keypath]; // Set the new value [obj]setValue:value forKey: keypath]; // Subclass object_setClass(obj, blockKVO. DynamicClass); / / will oldValue and newValue through observerValueForKeyPath: ofObject: change: method of notification to the caller NSMutableDictionary * change = (call the block) [[NSMutableDictionary alloc] init];if (item.options & NSKeyValueObservingOptionNew){
            change[@"old"] = oldValue;
        }
        if (item.options & NSKeyValueObservingOptionOld) {
            change[@"new"] = value; } [obj observeValueForKeyPath:keypath ofObject:obj change:change context:nil]; }}Copy the code

This way, every time we call setKeyPath:, the previously registered KVO listening block will be called. The entire KVO process is complete.

Of course, if you implement full KVO, the above code is not enough. You also need to address the following issues:

  1. Different types of attributes are supported
  2. setValue:forKey:The weak variable can be handled by this function.
  3. Thread-safe (not necessary if you only use it in the main thread)
  4. Release of dynamically created classes
  5. Other problems that may arise

All the code mentioned in this article has been submitted to Github, see the full demo here.

You can also click here to see all my Repos on Github.