Perfect support for viewing primitive types, pointer types, and structure properties. Automatically releases the observer object. Supports automatic and manual triggering of observers.

The principle of interpretation

KVO stands for Key Value observing, which is called Key Value observing. KVO is an implementation of the observer pattern that allows other objects to observe changes in an object’s specified properties.

So how does it work?

Apple’s official documentation reads:

Key-Value Observing Implementation Details

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

KVO is implemented using isA-Swizzling technology. The ISA pointer, as the name implies, points to the class of the object that maintains the dispatch table. The dispatch table essentially contains Pointers to methods implemented by the class, as well as other data. When an observer registers an object’s properties, the isa pointer to the observed object is modified to point to an intermediate class instead of the real class. Therefore, the value of the ISA pointer does not necessarily reflect the actual class of the instance. You should not rely on isa Pointers to determine class members. Instead, you should use class methods to determine the class of an object instance.

See here, apple changed the isa pointer of the observed object’s class to point to a derived class, observe the derived class of the same name attribute changes to implement KVO.

But in fact, when we print the class of the class being observed, it turns out that things aren’t that simple.

Prepare a Person demo class with the following structure:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) int age;

@property (nonatomic, assign) float money;

@property (nonatomic, assign) CGSize size;

@property (nonatomic, assign) double dd;

@property (nonatomic, assign) float ff;

@property (nonatomic, assign) CGFloat cgf;

@end
Copy the code

To add an observer to the Person object, look at the following code:

Person *p = [Person new];
self.p = p;
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    
NSLog(@"person class= %@",[p class]);
Copy the code

Print the following:

2021-07-28 17:59:10.791388+0800 KVODemo[7948:395941] person class= Person

What’s going on? I thought it was pointing to an intermediate class? Why print Person? OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC: OBJC

- (Class)class {
    return object_getClass(self);
}
Copy the code

Did the system override the implementation of the – (Class) Class method? Let’s try using object_getClassName directly:

Person *p = [Person new];
self.p = p;
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    
NSLog(@"person class= %@",[p class]);
NSLog(@"person class= %s",object_getClassName(p));
Copy the code

Print the following:

2021-07-28 18:09:58.412275+0800 KVODemo[8166:406263] person class= Person

2021-07-28 18:09:58.412488+0800 KVODemo[8166:406263] person class= NSKVONotifying_Person

See that it does point to a class named NSKVONotifying_Person. Let’s print out what methods it contains.

NSLog(@"==============> begin");
    Class cls = object_getClass(obj);
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    for (unsigned int i = 0; i < count; i++) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        NSLog(@"%@",NSStringFromSelector(sel));
    }
    NSLog(@"==============> end");
Copy the code

The intermediate class has the following methods:

2021-07-28 18:13:16.646938+0800 KVODemo[8220:409815] ==============> begin

2021-07-28 18:13:16.647070+0800 KVODemo[8220:409815] setName:

2021-07-28 18:13:16.647177+0800 KVODemo[8220:409815] class

2021-07-28 18:13:16.647273+0800 KVODemo[8220:409815] dealloc

2021-07-28 18:13:16.647377+0800 KVODemo[8220:409815] _isKVOA

2021-07-28 18:13:16.647448+0800 KVODemo[8220:409815] ==============> end

Seeing the familiar class method, it looks like the middle class really overrides the class, so we return Person when we call Person’s class.

Implementation approach

In fact, the system description document has given the implementation idea, let’s subdivide:

  1. A derived class of the class that creates the listening object
  2. Override the value of the derived classclass,set[keyPath]Methods (mainlysetMethods)
  3. When rewritingsetMethod to pass a value to the parent class, which is the target property of the listening class
  4. After passing a value to the parent class attribute, it is called back to the observer

The main implementation logic of KVO is the above, as for the system to rewrite the class class is mainly to prevent developers from confusion, but also to hide the implementation details of KVO.

Dynamically creating classes

The Runtime provides us with three methods:

Create a derived Class based on aClass, either Class or metaClass. Superclass: the parent class, name: the name of the new class, extraBytes: the number of bytes allocated for variables when creating the class.

  • Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes)

The new Class needs to be registered before it can be used. This step inserts the new Class into an underlying NXMapTable, which is a hash table with a key of type and a value of Class.

  • void objc_registerClassPair(Class _Nonnull cls)

Once the Class is created, we need to bind isa to Class using the following method

  • Class _Nullable object_setClass(id _Nullable obj, Class _Nonnull cls)

The complete code for creating the registration class is as follows:

- (void)demo_addObserver:(id)observer forKeyPath:(NSString *)keyPath callback:(demoKVOCallback)callback {
    if(! observer) {return;
    }
    if (keyPath.length == 0) {
        return;
    }
    if(! callback) {return;
    }
    
    Class cls = [self class];
    NSString *oldClassName = NSStringFromClass(cls);
    NSString *newClassName = [NSString stringWithFormat:@"IjfKVONotifying_%@",oldClassName];
    Class newCls = NSClassFromString(newClassName);
    if (newCls == nil) {
        // There is no such class
        newCls = objc_allocateClassPair(cls, newClassName.UTF8String, 0);
        if(! newCls) {@throw [NSException exceptionWithName:@"IJFCustomException" reason:@"the desired name is already in use" userInfo:nil];
        }
        NSLog(@" before registering a class --%@",objc_getClass(newClassName.UTF8String));
        objc_registerClassPair(newCls);
        NSLog(@" after class registration --%@",objc_getClass(newClassName.UTF8String));
    }
    // Change the isa pointer
    object_setClass(self, newCls);
}


/ / use
Person *p = [Person new];
    
    NSLog(@" Before adding observer -%@",p.class);
    
    [p demo_addObserver:self forKeyPath:@"name" callback:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
            
    }];
    
    NSLog(@" After adding observer -%@",p.class);
Copy the code

Print after run:

2021-07-29 16:34:14.331125+0800 KVODemo[6868:228989] Before adding observer -Person 2021-07-29 16:34:14.331403+0800 KVODemo[6868:228989] Register before class –(NULL) 2021-07-29 16:34:14.331567+0800 KVODemo[6868:228989] Register after class –IjfKVONotifying_Person 2021-07-29 16:34:14.331679+0800 KVODemo[68688:228989] after adding the observer -IjfKVONotifying_Person

As you can see, when a class is registered, it can be found, which means it can be used normally like any other class. And the Person object P points to our own derived class.

Dynamic addition method

Once the class is created, we need to add methods to the class. Let’s start by adding a -class instance method.

if (newCls == nil) { ... // override -selector classSEL = @selector(class); Method classMethod = class_getInstanceMethod(cls, classSEL); const char *classType = method_getTypeEncoding(classMethod); class_addMethod(newCls, classSEL, (IMP)ijf_kvo_class, classType); } static class ijf_kvo_class(id receiver, SEL SEL) {// Pointing to the parent of a derived class return class_getSuperclass(object_getClass(receiver)); }Copy the code

Print after run:

2021-07-30 12:45:19.960182+0800 KVODemo[2971:116085] -Person 2021-07-30 12:45:19.960477+0800 KVODemo[2971:116085] Register before class –(NULL) 2021-07-30 12:45:19.960644+0800 KVODemo[2971:116085] Register after class –IjfKVONotifying_Person 2021-07-30 12:45:19.960793+0800 KVODemo[2971:116085] after adding the observer -Person

No problem, calling the Class method on the Person object already returns the type we want. IMP Class ijF_kvo_class (ID receiver, SEL SEL) when I call class_addMethod.

This topic covers message sending in ObjectiveC. When we call a method such as [obj play], it will eventually be converted to objc_msgSend(id obj, SEL op…). . This function is defined in objc source code message.h. Therefore, I can define functions such as Class ijF_kvo_class (ID Receiver, SEL SEL) to accept the parameters passed in. OC -(Class) Class {… } is also perfectly fine.

Next we’ll modify the most important method in this article, the set method of an object.

We know that when we call p.name=@”ijinfeng”, we’re actually calling the setName: method of object P with an NSString argument. We override the derived method of the same name. Look directly at the code:

/ / rewrite the node-set SEL setterSEL = NSSelectorFromString ([nsstrings stringWithFormat: @ "set % @ :" keyPath. CapitalizedString]); Method setterMethod = class_getInstanceMethod(cls, setterSEL); const char *setterType = method_getTypeEncoding(setterMethod); class_addMethod(newCls, setterSEL, (IMP)ijf_setter_invoke, setterType); static void ijf_setter_invoke(id receiver, SEL setSEL, id newValue) { NSLog(@"setSEL= %@",NSStringFromSelector(setSEL)); NSLog(@"newValue= %@",newValue); }Copy the code

In this step, we have added the set method to the derived class, and since keyPath is name, the final method inserted is setName:. You can verify this by printing a list of derived class methods.

We find the right time to trigger custom KVO,

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.p.name = @"ijinfeng";
}
Copy the code

You can see the print:

2021-07-30 12:50:29.427236+0800 KVODemo[3107:122373] setSEL= setName:

2021-07-30 12:50:32.668304+0800 KVODemo[3107:122373] newValue= ijinfeng

We’ve successfully received the new value from the outside, which is the core first step of KVO, but it’s not enough. What we need to change is the value of our parent class, because to the user, he’s modifying the name property of the Person object. It is not the name of the derived class we created, IjfKVONotifying_Person, so we need to pass the value to the parent class.

static void ijf_setter_invoke(id receiver, SEL setSEL, id newValue) { NSLog(@"setSEL= %@",NSStringFromSelector(setSEL)); NSLog(@"newValue= %@",newValue); SEL setterSel = setSEL; SEL getterSel = getterForSetter(setterSel); Class superCls = [receiver class]; /* struct objc_super { __unsafe_unretained _Nonnull id receiver; #if ! defined(__cplusplus) && ! __OBJC2__ __unsafe_unretained _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif }; */ struct objc_super s = { receiver, superCls }; id oldValue = ((id (*)(struct objc_super *, SEL))objc_msgSendSuper)(&s,getterSel); ((void (*)(struct objc_super *, SEL, id))objc_msgSendSuper)(&s, setterSel, newValue); NSLog(@"oldValue= %@",oldValue); }Copy the code

And we will print self.p.name = @”ijinfeng” after updating the attribute name of p; NSLog(@” new value: %@”,self.p.name); And let’s see how it turned out:

2021-07-30 13:13:34.094102+0800 KVODemo[3413:138388] setSEL= setName 2021-07-30 13:13:34.094205+0800 KVODemo[3413:138388] newValue= ijinfeng 2021-07-30 13:13:34.094317+0800 KVODemo[3413:138388] oldValue= (NULL) 2021-07-30 13:13:34.094424+0800 KVODemo[3413:138388] New value: ijinfeng

Update the print again:

2021-07-30 13:16:06.015924+0800 KVODemo[3413:138388] setSEL= setName: 2021-07-30 13:16:06.016080+0800 KVODemo[3413:138388] newValue= iJinfeng 2021-07-30 13:16:06.016219+0800 KVODemo[3413:138388] oldValue= iJinfeng 2021-07-30 13:16:06.016362+0800 KVODemo[3413:138388] New value: ijinfeng

You can see that our new value has been set and the old value is retrieved correctly. So is this the end of it? Are we done with the core of KVO?

Of course not. What if I change a value of a primitive type? (Remember to change the listening property to age.)

self.p.age = 10; NSLog(@" new value: %d",self.p.age);Copy the code

Let’s run it.

Crash! Thread 1: EXC_BAD_ACCESS (code=1, address= 0xA) is displayed. Normally this error is caused by accessing freed memory.

We cannot change the input parameter to int newValue. If we change the input parameter to int newValue, how do we accept other types of parameters?

Let’s try receiving with a universal pointer void * and see what happens.

static void ijf_setter_invoke(id receiver, SEL setSEL, void *newValue) {
    NSLog(@"setSEL= %@",NSStringFromSelector(setSEL));
    NSLog(@"newValue= %p",newValue);
}
Copy the code

Run print:

2021-07-30 14:23:10.270407+0800 KVODemo[3827:163075] setSEL= setAge: 2021-07-30 14:23:10.270559+0800 KVODemo[3827:163075] newValue= 0xA

Now there is no Crash, but there is a new problem, where is the 10 we passed in? If you look at this 0xa which is a hexadecimal value, if you convert it to base 10 is 10, it looks like 10 has been inserted into the pointer newValue. So the pointer used to store a list of addresses, but now it stores a value.

Now that we know why, it’s easy to evaluate. Look at the code.

int value = (int)newValue;
NSLog(@"get value= %d",value);
Copy the code

Print:

2021-07-30 14:40:55.352662+0800 KVODemo[4142:181244] get value= 10

So you can see that we did get it out, so let’s try something else, and I’ll change the name property. Receive newValue in the following form.

NSString *value = (__bridge NSString *)newValue;
NSLog(@"get value= %@",value);
Copy the code

Print the following:

2021-07-30 14:44:25.305722+0800 KVODemo[4231:185283] setSEL= setName:

2021-07-30 14:44:25.305884+0800 KVODemo[4231:185283] newValue= 0x10d7c3360

2021-07-30 14:44:25.306019+0800 KVODemo[4231:185283] get value= ijinfeng

Ok, so it looks like primitive types and pointer types are working perfectly. But do you see a new problem here, which is how do we know what kind of argument newValue is passed in? After all, the function definition accepts void *. All of the above are strong turns with known parameter types.

How do I get parameter types

I don’t know if you’ve been around and used the NSMethodSignature class. This class is an encapsulation of our method and returns the method signature. This class is the perfect solution to our appeal.

Directly on the code:

NSMethodSignature *m = [receiver methodSignatureForSelector:setSEL]; Const char *type = [m getArgumentTypeAtIndex:2]; const char *type = [m getArgumentTypeAtIndex:2]; NSLog(@" get newValue parameter type: %s",type);Copy the code

2021-07-30 14:51:55.652546+0800 KVODemo[43:190749] Obtain the parameter type of newValue: @

See, it prints an at sign, and that represents the id type. The mapping table can be found under Runtime.h, which is posted directly here.

That is, we can do the following code:

NSMethodSignature *m = [receiver methodSignatureForSelector:setSEL]; const char *type = [m getArgumentTypeAtIndex:2]; NSLog(@" get newValue parameter type: %s",type); if (strcmp("@", type) == 0) { id value = (__bridge id)newValue; NSLog(@"get value= %@",value); } else if (strcmp("i", type) == 0) { int value = (int)newValue; NSLog(@"get value= %d",value); }Copy the code

See print:

2021-07-30 15:01:29.407734+0800 KVODemo[4543:200241] setSEL= setName: 2021-07-30 15:01:29.407938+0800 KVODemo[4543:200241] newValue= 0x105B533E0 2021-07-30 15:01:29.408124+0800 KVODemo[4543:200241] gets the parameter type of newValue: @2021-07-30 15:01:29.408271+0800 KVODemo[4543:200241] get value= ijinfeng 2021-07-30 15:01:29.408558+0800 KVODemo[4543:200241] setSEL= setAge: 2021-07-30 15:01:29.408677+0800 KVODemo[4543:200241] newValue= 0xA 2021-07-30 15:01:29.408829+0800 KVODemo[4543:200241] I 2021-07-30 15:01:29.408951+0800 KVODemo[4543:200241] get value= 10

Then the rest of the matter is simple, according to the above type mapping table, and then write down. But a new problem arises when we write about receiving doubles.

The compiler indicates that Pointers cannot be strongly converted to double. At the end of the time when I met a new barrier… It seems that this method is still unproven.

Next I’ll introduce another method called variable parameter parsing.

Va_list is a macro that handles when arguments are not specified, so we don’t need to convert them ourselves.

Get straight to the code demo:

static void ijf_setter_invoke(id receiver, SEL setSEL, ...) { NSMethodSignature *m = [receiver methodSignatureForSelector:setSEL]; const char *type = [m getArgumentTypeAtIndex:2]; va_list v; va_start(v, setSEL); if (strcmp(type, "@") == 0) { id actual = va_arg(v, id); NSLog(@"get value= %@",actual); } else if (strcmp(type, "i") == 0) { int actual = (int)va_arg(v, int); NSLog(@"get value= %d",actual); } else if (strcmp(type, "d") == 0) { double actual = (double)va_arg(v, double); NSLog(@"get value= %lf",actual); }}Copy the code

Print result:

2021-07-30 15:19:07.890083+0800 KVODemo[4835:214726] setSEL= setName: 2021-07-30 15:16:49.231051+0800 KVODemo[4800:212648] get value= iJinfeng 2021-07-30 15:19:07.890540+0800 KVODemo[4835:214726] setSEL= setAge: 2021-07-30 15:16:49.231416+0800 KVODemo[4800:212648] get value= 10 2021-07-30 15:19:07.890818+0800 KVODemo[4835-214726] SetSEL = setDd: 2021-07-30 15:16:49.231712+0800 KVODemo[4800:212648] get value= 12.300000

Finally, all that is left is to complete the type and call back the result. Look at this for the full Demo.