KVO, short for key-value-Observer, uses the Observer mode. The underlying implementation mechanism is isa-swizzing, which is to call the object_setClass function at the bottom and secretly change the Class to which the object’s ISA points.

In the observer mode, the target object (the observed object) manages all observer objects that depend on it and actively notifies the observer object when its own state changes. An implementation that actively notifies the observer typically calls the interface provided by the observer. This allows for loose coupling between the target object and the observer object.

In iOS, it’s even easier to implement, and you can notify the observer object by using respondto Selector to determine if the observer implements the specified method.

The implementation of KVO depends on the Runtime. It needs to dynamically obtain the class, dynamically modify the class, dynamically determine whether certain methods are implemented, and so on.

When an instance object of a Class is observed for the first time, a subclass of the Class is dynamically created, the isa of the object is changed to the Class of the new subclass, the set method of the observed attribute is overridden, and the interface of the observer is called to notify the observer before and after modifying the attribute.

1. KVO implementation in GNUstep

GNUstep is the predecessor of most implementations in Objective-C. Although OC has made many updates and optimizations based on GNUstep, much of the basic logic is the same. And KVO source code is not open source, so we can only first from the implementation of GNUstep to reference one or two.

[GNUstep Core Base] (wwwmain.gnustep.org/resources/d… There are implementations of the Foundation framework in. While it may not be quite the same as the OC implementation, the general idea is the same.

We [in the download open source project base/Headers/Foundation/NSKeyValueObserving. H] can be seen in KVO header files.

The API exposed in NSKeyValueObserving. H is basically the same as the API in objective-C foundation NSKeyValueObserving.

NSObjet added several categories, including key value observation methods and API methods such as Add observer and remove observer to be implemented by KVO.

We can be in the base/Source/Foundation/KVO 】 directory to find NSKeyValueObserving. M.

1.1 – addObserver: forKeyPath: options: context

Take a look at the source code first. Since it is the open source framework of GNUstep, some of the types are still GS prefixes. I have added some comments to make it easier to understand.

- (void) addObserver: (NSObject*)anObserver
          forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext { GSKVOInfo *info; GSKVOReplacement *r; NSKeyValueObservationForwarder *forwarder; NSRange dot; // 1. Initialize some global variables setup(); // 2. Use recursive lock to ensure thread safety [kvoLock lock]; Class r = replacementForClass([self Class]); Info = (GSKVOInfo*)[self observationInfo]; // 5. Create an observer information object instance if it does not exist.if(info == nil) { info = [[GSKVOInfo alloc] initWithInstance: self]; // 5.1 Save to global NSMapTable. [selfsetObservationInfo: info]; // change the isa of the observed object to a new KVO subclass Class object_setClass(self, [r replacement]); // Change the ISA of the observed object to a new KVO subclass Class object_setClass(self, [r replacement]); } // 6. Call the info instance method to observe dot = [aPath rangeOfString:@"."];
    if(dot.location ! = NSNotFound) { forwarder = [[NSKeyValueObservationForwarder alloc] initWithKeyPath: aPath ofObject: self withTarget: anObserver context: aContext]; [info addObserver: anObserverforKeyPath: aPath
                  options: options
                  context: forwarder];
    }
    else
    {
        [r overrideSetterFor: aPath];
        [info addObserver: anObserver
               forKeyPath: aPath options: options context: aContext]; } // 7. [kvoLock unlock]; }Copy the code

setup()

Setup () is used to initialize global variables, including recursive locks, and determine whether global variables are empty.

NSRecursiveLock *kvoLock = nil; static NSMapTable *classTable = 0; static NSMapTable *infoTable = 0; static NSMapTable *dependentKeyTable; static Class baseClass; static id null; // this is defined in GSLock NSRecursiveLock *gnustep_global_lock = nil; static inline voidsetup()
{
  if (nil == kvoLock)
    {
      [gnustep_global_lock lock];
      if (nil == kvoLock)
	{
	  kvoLock = [NSRecursiveLock new];
	  null = [[NSNull null] retain];
	  classTable = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
	    NSNonOwnedPointerMapValueCallBacks, 128);
	  infoTable = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
	    NSNonOwnedPointerMapValueCallBacks, 1024);
	  dependentKeyTable = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
	      NSOwnedPointerMapValueCallBacks, 128);
	  baseClass = NSClassFromString(@"GSKVOBase"); } [gnustep_global_lock unlock]; }}Copy the code

The above source code is basically at the top of NSKeyValueobserving.m.

NSMapTable is a new container class in iOS 6 that functions like NSDictionary. A normal dictionary will hold keys and values, resulting in the object’s reference count increasing. However, NSMapTable can set the holding status of the key and value respectively. If the key and value are weak references, the corresponding data in NSMapTable will be deleted after the key and value are released and destroyed.

replacementForClass()

This is a global static function that gets a KVO subclass of a class that has been created from the global classTable.

static GSKVOReplacement * replacementForClass(Class c) { GSKVOReplacement *r; // 0. The mode global is not initialized with setup(); // 1. Lock [kvoLock] with recursive lock; R = (GSKVOReplacement*)NSMapGet(classTable, (void*)c); // 3. If it does not exist, create one and save it in the global classTableif(r == nil) { r = [[GSKVOReplacement alloc] initWithClass: c]; NSMapInsert(classTable, (void*)c, (void*)r); } // 4. Unlock [kvoLock unlock];return r;
}
Copy the code

GSKVOReplacement actually stores the original Class as well as the updated Class of the object and the observed keys.

@interface	GSKVOReplacement : NSObject
{
  Class         original;       /* The original class */
  Class         replacement;    /* The replacement class */
  NSMutableSet  *keys;          /* The observed setter keys */
}
- (id) initWithClass: (Class)aClass;
- (void) overrideSetterFor: (NSString*)aKey;
- (Class) replacement;
@end
Copy the code

GSKVOInfo

The global infoTable stores instance objects of this type.

@interface	GSKVOInfo : NSObject
{
  NSObject	        *instance;	// Not retained.
  NSRecursiveLock	        *iLock;
  NSMapTable	        *paths;
}
- (GSKVOPathInfo *) lockReturningPathInfoForKey: (NSString *)key;
- (void*) contextForObserver: (NSObject*)anObserver ofKeyPath: (NSString*)aPath;
- (id) initWithInstance: (NSObject*)i;
- (NSObject*) instance;
- (BOOL) isUnobserved;
- (void) unlock;

@end
Copy the code

– observationInfo and – setObservationInfo:

These two functions are mainly used to access objects from the global infoTable, so I won’t go into details here.

- (void*) observationInfo
{
  void	*info;

  setup();
  [kvoLock lock];
  info = NSMapGet(infoTable, (void*)self);
  IF_NO_GC(AUTORELEASE(RETAIN((id)info));)
  [kvoLock unlock];
  return info;
}

- (void) setObservationInfo: (void*)observationInfo
{
  setup();
  [kvoLock lock];
  if (observationInfo == 0)
    {
      NSMapRemove(infoTable, (void*)self);
    }
  else
    {
      NSMapInsert(infoTable, (void*)self, observationInfo);
    }
  [kvoLock unlock];
}
Copy the code

object_setClass(self, [r replacement])

Here [R replacement] is really just getting the value of the replacement member variable in GSKVOReplacement. The process of generating replacement is in the init function.

- (id) initWithClass: (Class)aClass
{
    original = aClass;
    /*
     * Create subclass of the original, and override some methods
     * with implementations from our abstract base class.
     */
    NSString *superName = NSStringFromClass(original);
    NSString *name = [@"GSKVO"stringByAppendingString: superName]; NSValue *template = GSObjCMakeClass(name, superName, nil); // Register the new GSKVOXXX class with GSObjCAddClasses([NSArray arrayWithObject: template]); replacement = NSClassFromString(name); // Add the API from baseClass(GSKVOBase) to replacement. GSObjCAddClassBehavior(replacement, baseClass); keys = [NSMutableSet new];return self;
}
Copy the code

The most important thing is GSObjCAddClassBehavior(replacement, baseClass), because there are few apis in GSKVOBase, which mainly implement the following apis:

- (void) dealloc
{
  // Turn off KVO for self ... then call the real dealloc implementation.
  [self setObservationInfo: nil];
  object_setClass(self, [self class]);
  [self dealloc];
  GSNOSUPERDEALLOC;
}

- (Class) class
{
  return class_getSuperclass(object_getClass(self));
}

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
  Class		c = [self class];
  void		(*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

- (void) takeStoredValue: (id)anObject forKey: (NSString*)aKey
{
  Class		c = [self class];
  void		(*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

- (void) takeValue: (id)anObject forKey: (NSString*)aKey
{
  Class		c = [self class];
  void		(*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

- (void) takeValue: (id)anObject forKeyPath: (NSString*)aKey
{
  Class		c = [self class];
  void		(*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

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

These functions are very simple to implement, and their main purpose is to make the developer unaware of the existence of the GSKVOxxx class, because when the developer uses these functions, the information is still the original class.

Next, there are two cases:

  • 1. If all you want to observe is the properties of the object, you simply override the set method.
  • 2. If you want to observe attributes of a member variable, you need to construct oneNSKeyValueObservationForwarderObject, and then callGSKVOInfoIn the- addObserver: forKeyPath: options: context:Function.

Case 1

The overrideSetterFor implementation in GSKVOReplacement, which splice setXxx: or _setXxx:, gets SEL, and finally assigns SEL values to setter IMP of various types in GSKVOSetter.

The implementation of set methods for various types of attributes has been concentrated in gSKvosetters. In addition, the assignment uses the following API: class_addMethod(replacement, sel, IMP, [SIG methodType]).

Finally, call the -addobServer: forKeyPath: options: Context: function in GSKVOInfo. This API is called for two purposes:

  • 1. Save keyPath information toGSKVOInfoPaths in the paths directory for later retrieval directly from memory.
  • 2. If the options set by KVO contains the initial value, return the initialized value to the observer.

Case 2

The implementation of this situation, in fact, is in the following function:

- (id) initWithKeyPath: (NSString *)keyPath
              ofObject: (id)object
            withTarget: (id)aTarget
               context: (void *)context
{
  NSString * remainingKeyPath;
  NSRange dot;

  target = aTarget;
  keyPathToForward = [keyPath copy];
  contextToForward = context;

  dot = [keyPath rangeOfString: @"."];
  if (dot.location == NSNotFound)
    {
      [NSException raise: NSInvalidArgumentException
        format: @"NSKeyValueObservationForwarder was not given a key path"];
    }
  keyForUpdate = [[keyPath substringToIndex: dot.location] copy];
  remainingKeyPath = [keyPath substringFromIndex: dot.location + 1];
  observedObjectForUpdate = object;
  [object addObserver: self
           forKeyPath: keyForUpdate
              options: NSKeyValueObservingOptionNew
                     | NSKeyValueObservingOptionOld
              context: target];
  dot = [remainingKeyPath rangeOfString: @"."];
  if(dot.location ! = NSNotFound) { child = [[NSKeyValueObservationForwarder alloc] initWithKeyPath: remainingKeyPath ofObject: [object valueForKey: keyForUpdate] withTarget: self context: NULL]; observedObjectForForwarding = nil; }else
    {
      keyForForwarding = [remainingKeyPath copy];
      observedObjectForForwarding = [object valueForKey: keyForUpdate];
      [observedObjectForForwarding addObserver: self
                                    forKeyPath: keyForForwarding
                                       options: NSKeyValueObservingOptionNew
                                              | NSKeyValueObservingOptionOld
                                       context: target];
      child = nil;
    }

  return self;
}
Copy the code

For example, if we look at the height property of the child member variable in Parent, keyPath is actually child.height. This method creates a KVO that listens for changes to its child property, and then executes the KVO of the child, listening for changes to the height member variable.

Here the child’s observer is NSKeyValueObservationForwarder object, Then internally – observeValueForKeyPath: ofObject: change: context: call the upper level of the object – observeValueForKeyPath: ofObject: change: context:, This allows changes to the properties to be listened for to be passed out level by level.

2. KVO implementation in Apple

Here I create an HLPerson class:

@interface HLPerson : NSObject

@property (nonatomic, assign) int height;

@end
Copy the code

Then initialize the person in the viewController’s viewDidLoad:

    self.person = [[HLPerson alloc] init];
    NSLog(@"Class:%@", object_getClass(self.person));
    NSLog(@"person.class:%@", self.person.class);
    [self.person addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"Class:%@", object_getClass(self.person));
    NSLog(@"person.class:%@", self.person.class); Class:HLPerson Class:NSKVONotifying_HLPerson Person.class :HLPersonCopy the code

The apple implementation constructs an NSKVONotifying_HLPerson, which is not quite the same as the GNUstep prefix, but the implementation logic should be similar.

3. Summary

Although the conclusion is speculative, it should be highly reliable. -addobServer: forKeyPath: options: context:

  • 1. If global variables required by KVO are not initialized, initialize these global variables first.
  • 2. Get the transformed GSKVOReplacement object from the global classTable, and if one does not exist, create one to save to the classTable.
  • 3. Obtain the observer information GSKVOInfo object from the global infoTable. If it does not exist, create one, save it to the global infoTable, and assign the dynamically created class in GSKVOReplacement to the object. Dynamically created classes are overwrittensetValue:forKeyEtc. Function. Insert the willChange and didChange methods before and after the actual assignment.
  • 4. Override the set method of an object and insert the willChange and didChange methods before and after the assignment.
  • 5. Call GSKVOInfo- observeValueForKeyPath:ofObject:change:context:.
  • 6. It can be called in willChange and didChange when an object’s properties are actually modified- observeValueForKeyPath:ofObject:change:context:The subject has been informed.