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:
- Dynamically create a
Object
The subclass of “, with a customizable name hypothetically calledObject_KVONotify
. - At the same time, subclasses add methods dynamically
setKeypath:
Dynamically added methods are bound to a C function. - call
object_setClass
Function 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.
- Calling the parent class
setKeyPath:
Methods. - call
observeValueForKeyPath
Method 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:
addObserver
After the object is released, it will not be automatically released, we can only release indealloc
In the manualremoveObserver
. So in the case of negligence forgetremoveObserver
It could cause a crash. In addition, this restriction prevents us from adding listeners to other class objects in one class.- If there is no
addObserver
Can’t beremoveObserver
Crash. - 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:
- Dynamically creates a subclass of the current class with a fixed suffix
_NotifyKVO
. - At the same time, subclasses add methods dynamically
setXXXX:
Dynamically added methods are bound to a C function. - call
object_setClass
Function 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:
- Different types of attributes are supported
setValue:forKey:
The weak variable can be handled by this function.- Thread-safe (not necessary if you only use it in the main thread)
- Release of dynamically created classes
- 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.