[iOS] - KVO re-learning

KVO

KVO concept

KVO is a development mode. Its full name is Key-Value Observing (observer mode). It is a development mechanism provided under the Apple Fundation framework. Using KVO, you can easily observe a certain attribute of a specified object. When the attribute When a change occurs, notify the developer of the corresponding content between the old value and the new value of the property

Steps to use KVO

Register KVO monitoring

Register KVO through [addObserver:forKeyPath:options:context:]the method, so that you can receive the change event of the keyPath attribute;

  • observer: Observer, an object that monitors property changes. The object must implement observeValueForKeyPath:ofObject:change:context:the method.
  • keyPath: The property name to observe. Be consistent with the name of the attribute declaration.
  • options: In the callback method, the old value or new value of the attribute of the observed object is received, and the KVO mechanism is configured to modify the timing and content of the KVO notification
  • context: Pass in any type of object, which can be received in the code of "receive message callback", which is a value-passing method in KVO.

KVO monitoring implementation

[observeValueForKeyPath:ofObject:change:context:]Realize KVO monitoring through methods ;

  • keyPath: property of the observed object
  • object: the object being observed
  • change: Dictionary, store related values, return new value and old value according to the enumeration passed in by options
  • context: When registering the observer, the value passed by the context

Remove KVO monitoring

When there is no need to monitor, use the method [removeObserver:forKeyPath:]to remove the monitor;

Basic usage of KVO

We monitor the background color of the Button, click the button to change the background color of the button, and when the background color changes, print the background color before and after the change.

    //KVO最基本的使用
    self.view.backgroundColor = [UIColor whiteColor];
    self.kvoButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    self.kvoButton.frame = CGRectMake(100, 100, 100, 100);
    self.kvoButton.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:self.kvoButton];
    [self.kvoButton addTarget:self action:@selector(press) forControlEvents:UIControlEventTouchUpInside];
    //给所要监听的对象注册监听
    [self.kvoButton addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- (void)press {
    
    
    //改变被监听对象的值
    [self.kvoButton setValue:[UIColor colorWithRed:arc4random() % 255 / 255.0 green:arc4random() % 255 / 255.0 blue:arc4random() % 250 / 250.0 alpha:1] forKey:@"backgroundColor"];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    
    //打印监听结果
    if ([keyPath isEqual:@"backgroundColor"]) {
    
    
        NSLog(@"old value is: %@", [change objectForKey:@"old"]);
        NSLog(@"new value is: %@", [change objectForKey:@"new"]);
    }
}

We click the button once:
insert image description here

KVO pass value

KVO value transfer is also very simple. It can be understood that we monitor a certain property of the second viewController. When we jump to the first viewController, we can monitor the value change.

//FirstViewController
- (void)pressChuanZhi {
    
    
    SecondViewController *secondViewController = [[SecondViewController alloc] init];
    secondViewController.modalPresentationStyle = UIModalPresentationFullScreen;
    //为试图中的属性注册一个监听事件
    [secondViewController addObserver:self forKeyPath:@"content" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [self presentViewController:secondViewController animated:YES completion:nil];
}

//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    
    if ([keyPath isEqual:@"content"]) {
    
    
        id value = [change objectForKey:@"new"];
        self.chuanzhiLabel.text = value;
    }
}

//SecondViewController
- (void)viewDidLoad {
    
    
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    self.backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    self.backButton.frame = CGRectMake(100, 100, 100, 100);
    self.backButton.backgroundColor = [UIColor blueColor];
    [self.backButton addTarget:self action:@selector(pressBack) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.backButton];
    
    self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 250, 200, 50)];
    self.textField.keyboardType = UIKeyboardTypeDefault;
    self.textField.borderStyle = UITextBorderStyleRoundedRect;
    [self.view addSubview:self.textField];
}

- (void)pressBack {
    
    
    self.content = self.textField.text;
    [self dismissViewControllerAnimated:YES completion:nil];
}

Please add a picture description
Please add a picture description
Please add a picture description

Ways to ban KVO

//返回NO禁止KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    
    
    if ([key isEqualToString:@"content"]) {
    
    
        return NO;
    } else {
    
    
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

Precautions for use

  • The call [removeObserver:forKeyPath:]needs to be made before the observer disappears, otherwise it will result Crash.
  • After calling addObserverthe method, KVO does not make a strong reference to the observer, so you need to pay attention to the life cycle of the observer, otherwise it will cause the observer to be released Crash.
  • The observer needs to implement observeValueForKeyPath:ofObject:change:context:the method, which will be called when the KVO event arrives, and will result if it is not implemented Crash.
  • addObserverThe sum of KVO removeObserveris paired, if it is repeated, removeit will cause NSRangeExceptionthe type Crash, if it is forgotten, removeit will receive the KVO callback again after the observer is released Crash.
  • When calling KVO, one needs to be passed in keyPath. Since keyPathit is in the form of a string, it is easy to cause that the string does not change after the corresponding attribute changes Crash. We can use the system's reflection mechanism to keyPathreflect it, so that the compiler can @selector()perform legality checks in it.

Principle of KVO

Before analyzing the internal implementation of KVO, let’s analyze the storage structure of KVO, which mainly uses the following classes:
GSKVOInfo
GSKVOPathInfo
GSKVOObservation

GSKVOInfo

KVO is an informal protocol implemented based on the NSObject category, so all classes inherited from NSObject can use KVO, and all KVO information associated with the object can be obtained through the - (void*) observationInfo method, and the return value is GSKVOInfo. The source code is as follows

@interface  GSKVOInfo : NSObject
{
    
    
  NSObject          *instance;  // Not retained.
  GSLazyRecursiveLock           *iLock;
  NSMapTable            *paths;
}
  • It saves an instance of an object. The key point is Not retainedthat is not held weak, so when it is released, the call will crash, and all observers need to be removed before the object is destroyed.
  • pathsUsed to hold the mapping keyPathfrom GSKVOPathInfoto :

GSKVOPathInfo

@interface  GSKVOPathInfo : NSObject
{
    
    
@public
  unsigned              recursion;
  unsigned              allOptions;
  NSMutableArray        *observations;
  NSMutableDictionary   *change;
}
  • It holds keypathall observers corresponding to a
  • observationsholds all observers ( GSKVOObservationtype)
  • allOptionsSave the observer's options collection
  • changeSave the content to be delivered when KVO triggers

GSKVOObservation

@interface  GSKVOObservation : NSObject
{
    
    
@public
  NSObject      *observer;      // Not retained (zeroing weak pointer)
  void          *context;
  int           options;
}
@end

It holds all the information for a single observation

  • observersave watcher note here tooNot retained
  • context optionsBoth are parameters passed in when adding observers

Please add a picture description

KVO is implemented through isa-swizzling technology (this sentence is the focus of the entire KVO implementation). Create an intermediate class based on the original class at runtime, this intermediate class is a subclass of the original class, and dynamically modify the isa of the current object to point to the intermediate class. And rewrite the class method to return the Class of the original class. Therefore, Apple suggests that you should not rely on the isa pointer during development, but use the class instance method to obtain the object type.

Why rewrite the class method?

If there is no rewriting classmethod, when the object calls classthe method, it will search for the method in its own method cache list, method list, parent class cache, and method list, because classthe method is NSObjecta method in the method, if it is not rewritten, it may eventually will return NSKVONotifying_Apple, and the class will be exposed.

To implement the isa-swizzling technology, it is mainly implemented through the following classes:
GSKVOReplacement
GSKVOBase
GSKVOSetter

GSKVOReplacement

@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

// 创建
- (id) initWithClass: (Class)aClass
{
    
    
  NSValue       *template;
  NSString      *superName;
  NSString      *name;

  original = aClass;

  /*
   * Create subclass of the original, and override some methods
   * with implementations from our abstract base class.
   */
  superName = NSStringFromClass(original);      // original == Temp
  name = [@"GSKVO" stringByAppendingString: superName];    // name = GSKVOTemp
  template = GSObjCMakeClass(name, superName, nil);   // template = GSKVOTemp
  GSObjCAddClasses([NSArray arrayWithObject: template]);
  replacement = NSClassFromString(name);
  GSObjCAddClassBehavior(replacement, baseClass);

  /* Create the set of setter methods overridden.
   */
  keys = [NSMutableSet new];

  return self;
}
  • This class saves the original class information of the observed objectoriginal
  • Create a subclass of the original class and name it GSKVO<原类名>, the iOS system uses the naming rules for NSKVONotifying_the original class name
  • Copy GSKVOBasea method from a class to a new class
  • The follow-up will pass object_setClass, and the observed object will isapoint to this new class, that is, isa-swizzlingtechnology, and isa saves the information of the class, that is to say, the observed object becomes an instance of the new class, and this new class is used to implement KVO notification mechanism

GSKVOBase

This class provides several methods by default, all of which are rewrites of NSObject methods, and from the above, these methods must be copied to the newly created replacement class. That is, the observed object will have the implementation of these methods

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

After the object is released, remove the KVO data and point the object back to the original class

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

This method is used to hide the information of the replacement class, and the information obtained by the application layer is still the information of the original class. Therefore, Apple recommends that you should not rely on the isa pointer during development, but use the class instance method to obtain the object type.

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

This method is the same as the class method

- (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);
    }
}

This method belongs to KVC. Rewrite this method to add [self willChangeValueForKey: aKey]and before and after the original class KVC call [self didChangeValueForKey: aKey], and these two methods are the key to triggering KVO notifications.
So KVO is based on KVC, and KVC is the entrance triggered by KVO.

GSKVOBase

@interface  GSKVOSetter : NSObject
- (void) setter: (void*)val;
- (void) setterChar: (unsigned char)val;
- (void) setterDouble: (double)val;
- (void) setterFloat: (float)val;
- (void) setterInt: (unsigned int)val;
- (void) setterLong: (unsigned long)val;
#ifdef  _C_LNG_LNG
- (void) setterLongLong: (unsigned long long)val;
#endif
- (void) setterShort: (unsigned short)val;
- (void) setterRange: (NSRange)val;
- (void) setterPoint: (NSPoint)val;
- (void) setterSize: (NSSize)val;
- (void) setterRect: (NSRect)rect;
@end

This class has the same principle as the rewriting KVC method above, and will replace the method implementation keypathof the observer in the future setter. will setterappend [self willChangeValueForKey: aKey]and[self didChangeValueForKey: aKey]

summary

So here, I have a general understanding of the implementation of KVO. Through isa-swizzlingtechnology, replace the observed class information, and hook the observed method, add and keyPath setterbefore and after the original method call , so as to achieve the function of monitoring attribute changes[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey]

Source code implementation

Next, view all the processes of KVO from the source code

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
    
    
  GSKVOInfo             *info;
  GSKVOReplacement      *r;
  NSKeyValueObservationForwarder *forwarder;
  NSRange               dot;

  setup();
  [kvoLock lock];

  // Use the original class
  r = replacementForClass([self class]);

  /*
   * Get the existing observation information, creating it (and changing
   * the receiver to start key-value-observing by switching its class)
   * if necessary.
   */
  info = (GSKVOInfo*)[self observationInfo];
  if (info == nil)
    {
    
    
      info = [[GSKVOInfo alloc] initWithInstance: self];
      [self setObservationInfo: info];
      object_setClass(self, [r replacement]);
    }

  /*
   * Now add the observer.
   */
  dot = [aPath rangeOfString:@"."];
  if (dot.location != NSNotFound)
    {
    
    
      forwarder = [[NSKeyValueObservationForwarder alloc]
        initWithKeyPath: aPath
           ofObject: self
         withTarget: anObserver
        context: aContext];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: forwarder];
    }
  else
    {
    
    
      [r overrideSetterFor: aPath];
      [info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
    }

  [kvoLock unlock];
}

The entry of KVO, adding the observer method, mainly does the following things:

  • 1. replacementForClassCreate a replacement class and add it to the global classTablefor future use
  • 2. Get the listener data of the object GSKVOInfo, if not, create a new one
  • 3. Next, there are two cases
    • If the dot syntax ( ) is used , sub-object monitoring self.keyPath.keyPathwill be created using recursion, and the sub-object will forward the monitored changes to the upper layer for specific analysis laterNSKeyValueObservationForwarder
    • By default (keyPath) directly listens to a property of the object, overrideSetterForthe method will be called, the setter method of the hook property, and the implementation of the setter method will be replaced by the GSKVOSettermethod implementation in the corresponding parameter type
  • 4. Then call [info addObserver: anObserver forKeyPath: aPath options: options context: aContext]; method to save the new listener.

Add method in GSKVOInfo

- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
    
    
  GSKVOPathInfo         *pathInfo;
  GSKVOObservation      *observation;
  unsigned              count;

  if ([anObserver respondsToSelector:
    @selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
    {
    
    
      return;
    }
  [iLock lock];
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
  if (pathInfo == nil)
    {
    
    
      pathInfo = [GSKVOPathInfo new];
      // use immutable object for map key
      aPath = [aPath copy];
      NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
      [pathInfo release];
      [aPath release];
    }

  observation = nil;
  pathInfo->allOptions = 0;
  count = [pathInfo->observations count];
  while (count-- > 0)
    {
    
    
      GSKVOObservation      *o;

      o = [pathInfo->observations objectAtIndex: count];
      if (o->observer == anObserver)
        {
    
    
          o->context = aContext;
          o->options = options;
          observation = o;
        }
      pathInfo->allOptions |= o->options;
    }
  if (observation == nil)
    {
    
    
      observation = [GSKVOObservation new];
      GSAssignZeroingWeakPointer((void**)&observation->observer,
    (void*)anObserver);
      observation->context = aContext;
      observation->options = options;
      [pathInfo->observations addObject: observation];
      [observation release];
      pathInfo->allOptions |= options;
    }

  if (options & NSKeyValueObservingOptionInitial)
    {
    
    
      /* If the NSKeyValueObservingOptionInitial option is set,
       * we must send an immediate notification containing the
       * existing value in the NSKeyValueChangeNewKey
       */
      [pathInfo->change setObject: [NSNumber numberWithInt: 1]
                           forKey:  NSKeyValueChangeKindKey];
      if (options & NSKeyValueObservingOptionNew)
        {
    
    
          id    value;

          value = [instance valueForKeyPath: aPath];
          if (value == nil)
            {
    
    
              value = null;
            }
          [pathInfo->change setObject: value
                               forKey: NSKeyValueChangeNewKey];
        }
      [anObserver observeValueForKeyPath: aPath
                                ofObject: instance
                                  change: pathInfo->change
                                 context: aContext];
    }
  [iLock unlock];
}

This method is mainly to save observer information

1. Query the corresponding GSKVOPathInfoGSKVOObservationif there is, update it, if not, create a new one and save it
2. If optionsit is included NSKeyValueObservingOptionInitial, call it immediately [anObserver observeValueForKeyPath: aPath ofObject: instance change: pathInfo->change context: aContext]; send a message to the observer
2. Get the current value through KVC [instance valueForKeyPath: aPath]; get

willChangeValueForKey didChangeValueForKey

These two methods are added before setterand KVCafter assignment to save changes in property values ​​and send messages to observers

  • willChangeValueForKey :
    The main record is oldValueto save pathInfo->changein , if optionsit is included NSKeyValueObservingOptionPrior, it will traverse all observers, and immediately send a message to the observer,
    NSKeyValueObservingOptionPriorindicating that they will receive notifications before and after the attribute value is modified
  • didChangeValueForKey
    Save the old and new values ​​of attributes according to options, traverse all observers, and send messages

remove observer

/*
 * removes the observer
 */
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
    
    
  GSKVOPathInfo *pathInfo;

  [iLock lock];
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
  if (pathInfo != nil)
    {
    
    
      unsigned  count = [pathInfo->observations count];

      pathInfo->allOptions = 0;
      while (count-- > 0)
        {
    
    
          GSKVOObservation      *o;

          o = [pathInfo->observations objectAtIndex: count];
          if (o->observer == anObserver || o->observer == nil)
            {
    
    
              [pathInfo->observations removeObjectAtIndex: count];
              if ([pathInfo->observations count] == 0)
                {
    
    
                  NSMapRemove(paths, (void*)aPath);
                }
            }
          else
            {
    
    
              pathInfo->allOptions |= o->options;
            }
    }
    }
  [iLock unlock];
}

This method is mainly used to remove the observer of the corresponding keyPath. The implementation of the method is very simple. It can be queried and removed according to the parameters passed in anObserverand aPathin the data structure introduced earlier.

Summarize

  • It is mainly used isa-swizzlingto modify the class information of the observer, and the hook settermethod settersends a message to all observers when the method is called
  • It can be seen from the source code above that the references to the observer and the observed are not retain, so the observer must be removed before the object is released.
  • The sending of the message is mainly triggered by [self willChangeValueForKey: key], [self didChangeValueForKey: key]and must appear in pairs. automaticallyNotifiesObserversForKeyThe method is used to control whether to add the above two methods. The default return value is YES. If it returns NO, it will not be added automatically, that is to say, the call of the setter and KVC modifications will not trigger notifications
  • + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
    This method is used to set dependencies. Sometimes it is required that the value of a certain property changes with the change of other properties of the same object. By registering such a dependency in the class in advance, even if the attribute value changes indirectly, a notification message will be sent, and the internal implementation of all the key collections that are rewritten by the observer class to return and the key depends on is also relatively simple
    . All dependencies are stored in the global dependentKeyTable, and then hook all dependent keymethods setter, when called [self willChangeValueForKey: key], [self didChangeValueForKey: key]all dependencies will be found, and then a message will be sent
  • KVC has been used many times inside KVO
    • rewritesetValue:forKey
    • Using valueForKey --- valueForKeyPathget attribute value, especially when using dot syntax, can only valueForKeyPathget deep attribute value.
      So KVO is implemented based on KVC.

Guess you like

Origin blog.csdn.net/m0_62386635/article/details/130383322