This article Demo portal: CMKVODemo

Abstract: This article first introduces the basic usage of KVO, then explores the implementation mechanism of KVO (key-value Observing), and uses Runtime to simulate the implementation mechanism of KVO: a Block callback, a Delegate callback. This article also summarizes the runtime related API usage during the KVO implementation.

1. Theoretical basis of KVO

1.1 Basic usage of KVO

steps

The user registers observers and implements listening

[self.person addObserver:self
              forKeyPath:@"age"
                 options:NSKeyValueObservingOptionNew
                 context:nil];
Copy the code

The suggestion is a callback method that handles changes to the attribute

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { //... Implement listening processing}Copy the code

To remove observers

[self removeObserver:self forKeyPath: @ "age"];
Copy the code

Comprehensive example

// Add observer _person = [[Person alloc] init]; [_person addObserver:selfforKeyPath:@"age"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:nil];
Copy the code
//KVO callback method - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { NSLog(@"The %@ attribute of the %@ object has been changed. The change dictionary is: %@",object,keyPath,change);
    NSLog(@"New value for attribute: %@",change[NSKeyValueChangeNewKey]);
    NSLog(@"Old value of attribute: %@",change[NSKeyValueChangeOldKey]);
}
Copy the code
// removeObserver - (void)dealloc {[self.person removeObserver:selfforKeyPath:@"age"];
}
Copy the code

Using KVO to realize the key value of the third party framework

AFNetworking, MJRresh

1.2 The realization principle of KVO

KVO is an Objective-C implementation of the Observer Pattern. The observer object is notified when one of the properties of the observed object changes. Interestingly, you don’t need to add any extra code to the object being observed to use KVO. How does this work?

The KVO implementation also relies on objective-C’s powerful Runtime. Apple’s documentation briefly mentions the implementation of KVO. The only useful information from Apple’s documentation is that the ISA pointer to the observed object points to an intermediate class, not the original real class. Apple doesn’t want to reveal too much about the implementation details of the KVO.

However, if you use the methods provided by Runtime to dig deeper, all the hidden details will be revealed. Mike Ash did this research back in 2009, see more here.

KVO implementation:

When you look at an object, a new class is created dynamically. This class inherits from the object’s original class and overwrites the setter methods for the observed property. Naturally, the overridden setter method is responsible for notifying all observed object values of changes before and after the original setter method is called. Finally, by pointing the object’s ISA pointer (which tells the Runtime system what the object’s class is) to the newly created subclass, the object magically becomes an instance of the newly created subclass.

This intermediate class, it inherits from the original class. Not only that, but Apple overwrote the -class method in an attempt to trick us into thinking that the class was the same as it was. For more detailed information, check out the code in Mike Ash’s article, which will not be repeated here.

1.3 Shortage of KVO

KVO is powerful, yes. Knowing its internal implementation might help you use it better or make it easier to debug if it goes wrong. But the API provided by the official implementation of KVO is not very good.

For example, you can only through the rewrite – observeValueForKeyPath: ofObject: change: context: method to get a notice. If you want to provide a custom selector, no; There’s no way to pass a block. And you have to deal with the case of the parent class – the parent class also listens to the same property of the same object. But sometimes, you don’t know if the superclass is interested in the message. Although the context of this parameter is doing this, can also solve the problem – in – addObserver: forKeyPath: options: context: pass in the context of a parent don’t know. But I always feel that the box in the API design, the code is very awkward. At the very least, there should be support for blocks.

A lot of people think the official KVO is not working. Mike Ash’s key-value Observing Done Right, as well as KVO Considered Harmful, which gets a lot of sharing, have come up against KVO. So in actual development, KVO is not used in many scenarios, more often using a Delegate or NotificationCenter.

2. Block implements KVO

2.1 Simulation Implementation

Note: the following are written in the same file: NSObject+ block_kvo.m

  • Import the header file and define two static variables
#import "NSObject+Block_KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

//as prefix string of kvo class
static NSString * const kCMkvoClassPrefix_for_Block = @"CMObserver_";
static NSString * const kCMkvoAssiociateObserver_for_Block = @"CMAssiociateObserver";
Copy the code
  • Expose the caller to add the KVO method for the observed object
- (void)CM_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CM_ObservingHandler)observedHandler
{
    //step 1 get setter method, if not, throw exception
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if(! setterMethod) { @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %@", self] userInfo: nil];
        return; } Class observedClass = object_getClass(self); NSString * className = NSStringFromClass(observedClass); // If the listener does not have CMObserver_, then determine whether to create a new classif(! [className hasPrefix: KCMkvoClassPrefix_for_Block]) {/ / observedClass code (1) 】 【 = [self createKVOClassWithOriginalClassName: the className]; //【API remarks ①】 object_setClass(self, observedClass); } //add kvo setter methodif its class(or superclass)hasn't implement setter if (! [self hasSelector: setterSelector]) { const char * types = method_getTypeEncoding(setterMethod); Class_addMethod (observedClass, setterSelector, (IMP)KVO_setter, types); } //add this observation info to saved new observer //【 code ③】 CM_ObserverInfo_for_Block * newInfo = [[CM_ObserverInfo_for_Block alloc] initWithObserver: observer forKey: key observeHandler: observedHandler]; NSMutableArray * Observers = objc_getAssociatedObject(self, (__bridge void *)kCMkvoAssiociateObserver_for_Block); if (! observers) { observers = [NSMutableArray array]; objc_setAssociatedObject(self, (__bridge void *)kCMkvoAssiociateObserver_for_Block, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [observers addObject: newInfo]; }Copy the code
  • If the observed class is the original class of the observed object, then create a new class based on the original classA subclassTo distinguish whether this subclass’s tag is withkCMkvoClassPrefix_for_BlockThe prefix. How do I create a new oneA subclass? The code looks like this:
- (Class)createKVOClassWithOriginalClassName: (NSString *)className
{
    NSString * kvoClassName = [kCMkvoClassPrefix stringByAppendingString: className];
    Class observedClass = NSClassFromString(kvoClassName);
    
    if (observedClass) { returnobservedClass; } Class originalClass = object_getClass(self); Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.utf8String, 0); Method classMethod = class_getInstanceMethod(originalClass, @selector(class)); const char * types = method_getTypeEncoding(classMethod); class_addMethod(kvoClass, @selector(class), (IMP)kvo_Class, types); objc_registerClassPair(kvoClass);return kvoClass;
}
Copy the code
  • In addition, [code ②] means to replace the original setter Method with a new setter (this is Runtime’s dark magic, Method Swizzling). So what’s the new setter method? As follows:
#pragma mark -- Override setter and getter Methods
static void KVO_setter(id self, SEL _cmd, id newValue)
{
    NSString * setterName = NSStringFromSelector(_cmd);
    NSString * getterName = getterForSetter(setterName);
    if(! getterName) { @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil];
        return; } id oldValue = [self valueForKey: getterName]; struct objc_super superClass = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; [self willChangeValueForKey: getterName]; void (*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper; objc_msgSendSuperKVO(&superClass, _cmd, newValue); [self didChangeValueForKey: getterName]; NSMutableArray * Observers = objc_getAssociatedObject(self, (__bridge const void *)kCMkvoAssiociateObserver_for_Block);for (CM_ObserverInfo_for_Block * info in observers) {
        if([info.key isEqualToString: getterName]) { dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ info.handler(self, getterName, oldValue, newValue); }); }}}Copy the code
  • [code ③] is a new observer class. The implementation of this class is written in the same class, equivalent to importing a class: CM_ObserverInfo_for_Block. This class acts as an observer and is responsible for the Block callback passed by the caller during initialization. As follows,self.handler = handler;That is, responsible for callbacks.
@interface CM_ObserverInfo_for_Block : NSObject

@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * key;
@property (nonatomic, copy) CM_ObservingHandler handler;

@end

@implementation CM_ObserverInfo_for_Block

- (instancetype)initWithObserver: (NSObject *)observer forKey: (NSString *)key observeHandler: (CM_ObservingHandler)handler
{
    if (self = [super init]) {
        
        _observer = observer;
        self.key = key;
        self.handler = handler;
    }
    return self;
}

@end
Copy the code
  • [code ④] is used for static variables with known “property names” of type NSStringkCMkvoAssiociateObserver_for_BlockTo get the array of “property” observersObserver arraySuch a property). Among them, about(__bridge void *)We’ll come back to that later.

Callers: Add KVO for the observed using the above API

  • VC call API
#import "NSObject+Block_KVO.h"/ /... - (void)viewDidLoad { [super viewDidLoad]; ObservedObject * object = [ObservedObject new]; object.observedNum = @8;#pragma mark - Observed By Block
    [object CM_addObserver: self forKey: @"observedNum" withBlock: ^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
        NSLog(@"Value had changed yet with observing Block");
        NSLog(@"oldValue---%@",oldValue);
        NSLog(@"newValue---%@",newValue);
    }];
    
    object.observedNum = @10;
}
Copy the code

2.2 Runtime Key API parsing

【API note ①】 : object_setClass

We can create new classes at run time, a feature we don’t use much, but it’s actually quite powerful. It allows you to create new subclasses and add new methods.

But what’s the use of such a subclass? Don’t forget a key point about Objective-C: An object has a variable inside it called isa that points to its class. This variable can be changed without having to be recreated. Then you can add new ivars and methods. You can change the class of an object by using the following command

object_setClass(myObject, [MySubclass class]);
Copy the code

This can be used for Key Value Observing. When you start observing Object, Cocoa will create the subclass of this object’s class, and then point the object’s ISA to the newly created subclass.

【API note ②】 : objc_allocateClassPair

objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, 
                       size_t extraBytes) 
Copy the code
  • Everything seems simple enough, but creating a class at runtime requires only three steps: 1. Allocate space for a “class pair” (usingobjc_allocateClassPairAdd methods and members to the created classclass_addMethodAdded a method). 3. Register the class you created and make it availableobjc_registerClassPair).

Why does one and three both say pair? We know that pair in Chinese means a pair, so here is a pair of classes. So who is this pair of classes? They are Class, MetaClass.

  • The arguments to be configured are: 1. First argument: as a superclass of the new class, or Nil to create a new root class. 2, the second parameter: name of the new class 3, the third parameter: pass 0

【API annotation ③】 :(__bridge void *)

While ARC is in effect, id and void * can be converted to each other by (__bridge void *) converting id and void *. Why convert? That’s because the argument to objc_getAssociatedObject requires it. Let’s take a look at the API:

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
Copy the code

As you can see, the key of the “property name” must be a void * argument. So you need to convert. For this transformation, here’s an example of a transformation:

id obj = [[NSObject alloc] init];

void *p = (__bridge void *)obj;
id o = (__bridge id)p;
Copy the code

You can learn more about this conversion: ARC type conversion: Show the conversion ID and void *

Of course, if you use the API without a transformation, you need to use it like this:

  • Method 1:
objc_getAssociatedObject(self, @"AddClickedEvent");
Copy the code
  • Method 2:
static const void *registerNibArrayKey = &registerNibArrayKey;
Copy the code
NSMutableArray *array = objc_getAssociatedObject(self, registerNibArrayKey);
Copy the code
  • Method 3:
static const char MJErrorKey = '\ 0';
Copy the code
objc_getAssociatedObject(self, &MJErrorKey);
Copy the code
  • Method 4:
+ (instancetype)cachedPropertyWithProperty:(objc_property_t)property { MJProperty *propertyObj = objc_getAssociatedObject(self, property); / / omit}Copy the code

Objc_property_t is runtime

typedef struct objc_property *objc_property_t;
Copy the code

2.3 Runtime Other API parsing

The rest is runtime’s more common API, which will not be explained in the order of the above code. Let’s just categorize these apis according to runtime’s knowledge:

  • Runtime: Associated object related APIS
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
Copy the code
  • Runtime: Methods replace associated apis
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
object_getClass(id _Nullable obj) 
Method class_getInstanceMethod(Class cls, SEL name);
const char * method_getTypeEncoding(Method m);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
Copy the code
  • Runtime: Apis related to message mechanisms
objc_msgSendSuper
Copy the code
  • KVO
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
Copy the code

3. Delegate implementation of KVO

Note: the following are written in the same file: NSObject+ block_delegate.m

  • The observation class CM_ObserverInfo needs to change one property, changing the Block to a Delegate.
@interface CM_ObserverInfo : NSObject @property (nonatomic, weak) NSObject * observer; @property (nonatomic, copy) NSString * key; @property (nonatomic, assign) id <ObserverDelegate> ObserverDelegate; @endCopy the code
  • Similarly, when observing the initialization of the CM_ObserverInfo class, you need to initialize this new property accordingly.
@implementation CM_ObserverInfo

- (instancetype)initWithObserver: (NSObject *)observer forKey: (NSString *)key
{
    if(self = [super init]) { _observer = observer; self.key = key; // Modify here self.obServerDelegate = (id< observerDelegate >)observer; }return self;
}
@end

Copy the code
  • Expose the caller to add the KVO method for the observed object: no need to pass the Block.
#pragma mark -- NSObject Category(KVO Reconstruct)
@implementation NSObject (Block_KVO)

- (void)CM_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(CM_ObservingHandler)observedHandler { //... CM_ObserverInfo * newInfo = [[CM_ObserverInfo alloc] CM_ObserverInfo = [[CM_ObserverInfo alloc] initWithObserver: observerforKey: key]; / /... Omit}Copy the code

Callers: Add KVO for the observed using the above API

  • VC call API
#import "NSObject+Delegate_KVO.h"/ /... - (void)viewDidLoad { [super viewDidLoad]; ObservedObject * object = [ObservedObject new]; object.observedNum = @8;#pragma mark - Observed By Delegate
    [object CM_addObserver: self forKey: @"observedNum"];
    
    object.observedNum = @10;
}
Copy the code
  • VC proxy method implementation
#pragma mark - ObserverDelegate
-(void)CM_ObserveValueForKeyPath:(NSString *)keyPath ofObject:(id)object oldValue:(id)oldValue newValue:(id)newValue{
    NSLog(@"Value had changed yet with observing Delegate");
    NSLog(@"oldValue---%@",oldValue);
    NSLog(@"newValue---%@",newValue);
}
Copy the code

4. Runtime learn more

In addition, I wrote the principles and practices of Runtime. If you want to learn more about Runtime, you can read these articles:

  • IOS development · Runtime Principles and Practices: Message forwarding
  • IOS development · Runtime Principles and Practices: Associated Objects
  • IOS development · Runtime Principles and Practices: Methods interchange
  • IOS development · Runtime Principles and Practices: Basics