This article, the ninth in the Objective-C series, focuses on the underlying implementation of KVO, the use of KVC, and the call process in KVC.

  • Objective C (7) Object memory analysis
  • Objective C (8) The nature and classification of objects
  • Objective-c (9) KVC and KVO
  • Objective-c (10) Category
  • Objective-c load and Initialize
  • Objective-c (12) Associative objects

An overview,

KVO, the full name of Key Value Observing, is a set of event notification mechanism provided by Apple. Allows an object to listen for changes to specific properties of another object and receive events when they change. Because of the implementation mechanism of KVO, which only works on properties, objects that inherit from NSObject support KVO by default.

Both KVO and NSNotificationCenter are implementations of the Observer pattern in iOS. The difference is that KVO is one-to-one, rather than one-to-many, with respect to the relationship between observed and observer. KVO is non-intrusive to the monitored object and can be monitored without modifying its internal code.

KVO can listen for changes in individual properties as well as collection objects. The proxy object is obtained through methods such as mutableArrayValueForKey: of KVC. When the internal object of the proxy object changes, the method monitored by KVO is called back. The collection object contains NSArray and NSSet.

2. Basic use of KVO

Project code kVO-01-Usage

2.1 Registered Observer

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[BFPerson alloc] init];
    self.person1.age = 28;
    self.person1.name = @"weng";
    [self addObserver];
}
- (void)addObserver
{
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
    [self.person1 addObserver:self forKeyPath:@"name" options:option context:@"name change"];
}
Copy the code

2.2 Listening for callbacks

/** The observer listens to the callback method @param keyPath listens to the keyPath @param object listens to the object @param change the contents of the field changed @param context the address value passed in when registering */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@" listen to %@ change %@ - %@ - %@, object, keyPath, change, context);
}
Copy the code

2.3 call

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

2.3.1 Other Invocation methods

   // All of the following calls can start KVO
    self.person1.age = 29;
    [self.person1 setAge:29];
    [self.person1 setValue:@(29) forKey:@"age"];
    [self.person1 setValue:@(29) forKeyPath:@"age"];
Copy the code

2.3.2 Manual Invocation

KVO is automatically called when a property changes. If you want to manually control the timing of the call, or if you want to implement the KVO property call yourself, you can call the method provided by KVO.

Take the age attribute as an example:

2.3.2.1 Disabling automatic invocation

// Age does not need to be called automatically, except for age attributes (including name)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"age"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}
Copy the code

The above method is also equivalent to the following two methods:

+ (BOOL)automaticallyNotifiesObserversOfAge
{
    return NO;
}
Copy the code

For each attribute, the KVO will generate a * * * * ‘+ (BOOL) automaticallyNotifiesObserversOfXXX’ method, if return can automatically call KVO

If we implement the above method, we will find that KVO cannot be triggered by changing the value of the age attribute, and we need to implement a manual call to trigger KVO.

2.3.2.2 Manual Invocation implementation

- (void)setAge:(NSInteger)age
{
    if(_age ! = age) { [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"]; }}Copy the code

The implementation of (1) disable automatic call (2) manual call implementation of two steps, the age attribute manual call is realized, and at this time can be the same as automatic call, trigger KVO.

2.4 Removing the Observer

- (void)dealloc
{
    [self removeObserver];
}

- (void)removeObserver
{
    [self.person1 removeObserver:self forKeyPath:@"age"];
    [self.person1 removeObserver:self forKeyPath:@"name"];
}
Copy the code

2.5 Crash

If KVO is used improperly, it is very easy to cause Crash. The relevant test code is in KVO- 02-CRASH

2.5.1 Observer does not implement the listening method

If the observer object * * – observeValueForKeyPath: ofObject: change: context: * * unrealized, will Crash

Crash: Terminating app due to uncaught exception ‘NSInternalInconsistencyException, reason:’ < ViewController: 0x7f9943d06710>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

2.5.2 Observer was not removed in time

Thread 1: EXC_BAD_ACCESS (code=1, address= 0x105e0fee02C0)

// Observer ObserverPersonChage
@interface ObserverPersonChage : NSObject
  // Implement observeValueForKeyPath: ofObject: change: context:
@end

//ViewController
- (void)addObserver
{
    self.observerPersonChange = [[ObserverPersonChage alloc] init];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"age" options:option context:@"age chage"];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"name" options:option context:@"name change"];
}

// Click the button to set the observer to nil, i.e. destroy
- (IBAction)clearObserverPersonChange:(id)sender {
    self.observerPersonChange = nil;
}

// Click change person1 property value
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 29;
    self.person1.name = @"hengcong";
}
Copy the code
  1. If an observer is registered in the current ViewController, click on the screen to change the property value of the observed object Person1.

  2. Click on the corresponding button, destruction of the observer, the self. The observerPersonChange is nil.

  3. Click the screen again, now Crash;

2.5.3 Remove observer multiple times

Cannot remove an observer <ViewController 0x7fc6dc00c090> for the key path “age” from <BFPerson 0x6000014acd00> because it is not registered as an observer.

2.6 Disadvantages of keyPath strings

When registering Observe, pass in keyPath as a string. KeyPath is easy to miswrite.

[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
Copy the code

The optimized scheme is:

[self.person1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:option context:@"age change"];
Copy the code

2.7 Attribute Dependency

/** The observer will also be notified if the age changes */
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAge
{
    NSSet *set = [NSSet setWithObjects:@"name".nil];
    return set;
}
Copy the code

Third, the principle of

1. Discover intermediate objects

In order to distinguish the changes of objects and corresponding attribute setting methods after the addition of KVO, we conducted the following tests:

Observation method realization:

  • Before and after adding KVO,person1Point to theClass objectandYuan class object, as well assetAge:Have changed;
  • After adding KVO,person1In theisaPoints to theNSKVONotifying_BFPersonClass objects;
  • After adding KVO,setAge:The implementation of Foundation calls: Foundation_NSSetLongLongValueAndNotifyMethods;

2. Explore the invocation process

Rewrite project at kVO-03-princlipe.

(1) Override the following methods of BFPerson

  • – setAge:
  • -willChangeValueForKey:
  • -didChangeValueForKey:

(2) Debugging

After we’ve overridden the method, we run a print test

In combination with

  • Rewrite the printed log:

BFPerson willChangeValueForKey: – begin

BFPerson willChangeValueForKey: – end

BFPerson setAge: begin

BFPerson setAge: end

BFPerson didChangeValueForKey: – begin

– [ViewController observeValueForKeyPath: ofObject: change: context:] – listen to BFPerson age attribute value has changed – {

kind = 1;

new = 29;

old = 28;

} – age chage

BFPerson didChangeValueForKey: – end

  • Assembly call stack

  • Intermediate object — NSKVONotifying_BFPerson

Let’s put together a complete chain of methods:

  1. self.person1.age = 29;
  2. Foundation _NSSetLongLongValueAndNotify
    1. willChangeValueForKey:
    2. [BFPerson segAge:]
    3. didChangeValueForKey:
      1. -[ViewController observeValueForKeyPath:ofObject:change:context:]

The diagram below:

But these are not enough to reflect a truly complete KVO implementation.

3. Repeat KVO

Here’s a description of how it works, taken from the official documentation:

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.

Let’s look at the list of methods for the intermediate object after adding KVO and before adding KVO:

class List of methods (ignore methods related to the name attribute)
BFPerson test, .cxx_destruct, setAge:, age
NSKVONotify_BFPerson setAge:, class, dealloc, _isKVOA
  • Isa switching technology

    • After the exchange, any method that calls the BFPerson object will go through NSKVONotify_BFPerson, but different methods have different processing.

      • callMethod of setting properties for listening, such assetAge:, are called firstNSKVONotify_BFPersonCorresponding property setting method;
      • callMethod of setting non-listening properties, such astest, will passNSKVONotify_BFPersonthesuperclassTo findBFPersonClass object and call it[BFPerson test]methods
    • After swapping, isa points to an object that is not a true reflection of the class, and object_getClass returns an object that ISA points to, so it is also unreliable.

      For example, using KVO, object_getClass yields the generated intermediate object NSKVONotify_BFPerson instead of BFPerson.

    • To get the real object of the class, you need to get it through the class object method.

      If **[self.person1 class]** returns a BFPerson object.

  • **[self.person1 class]** still gets the BFPerson object, why?

    • NSKVONotify_BFPersonRewrite theclassObject method, which returnsBFPerson;
  • _isKVOA

    Returns whether KVO;

  • delloc

    Do some cleaning up

At this point, basically the NSKVONotifying_BFPerson class has been formed (the relevant code reference project), combined with the call process, we draw the following comparison diagram.

(1) No KVO object is used

(2) Use KVO — generate intermediate objects

(3) Use KVO — execution stream

Iv. Basic use of KVC

The project source is at: KvC-01-usage

(1) Common apis

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key; 
Copy the code

There are two methods to note:

  • Difference between valueForKey and objectForKey
valueForKey objectForKey
Processing without key None Key, crash, NSUndefinedKeyException None This key returns nil
source KVC main method The method of NSDictionary
symbol [super valueForKey:] [super valueForKey:] Key does not begin with the @ sign; they are the same
  • Difference between setValue and setObject
setValue setObject
value Value can be nil, and when value is nil, the removeObject: method is automatically called Value cannot be nil
source The main method of KVC NSMutabledictionary peculiar
The parameters of the key Can only be nsstrings SetObject: Can be of any type

There are other methods in the NSKeyValueCoding category, for example

If set
      
        is not found, members will be searched in the order _key, _iskey, Key, and iskey
      
+ (BOOL)accessInstanceVariablesDirectly;

//KVC provides a property value validation API, which can be used to check if the value of a set is correct, make a replacement value for an incorrect value, or reject a new value and return the cause of the error.
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// This is the set operation API, there are a series of such apis, if the property is an NSMutableArray, then you can use this method to return
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// If the Key does not exist and no KVC can find any fields or attributes related to the Key, this method is called. By default, an exception is thrown
- (nullable id)valueForUndefinedKey:(NSString *)key;

// Same as the previous method, but set the value.
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

// If you pass nil to Value during the SetValue method, this method is called
- (void)setNilValueForKey:(NSString *)key;

// Input a set of keys, return the values corresponding to the keys, and then return the dictionary, which is used to transfer the Model to the dictionary.
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
Copy the code

(2) Collection API

The corresponding method of ordered set is as follows:

-countOf<Key>// Must be implemented, corresponding to NSArray's base method count:2
    
-objectIn<Key>AtIndex:
-<key>AtIndexes:NSArray objectAtIndex: objectsAtIndexes:

-get<Key>:range:// This is not required, but can improve performance, and corresponds to the NSArray method getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes:NSMutableArray insertObject:atIndex: and insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes:NSMutableArray removeObjectAtIndex: and removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>:// Optionally, if there are performance issues with such operations, you need to consider implementing them
Copy the code

The corresponding method of unordered set is as follows:

-countOf<Key>// Must be implemented, corresponding to NSArray's basic method count:

-objectIn<Key>AtIndex:
-<key>AtIndexes:NSArray objectAtIndex: objectsAtIndexes:

-get<Key>:range:// This is not required, but can improve performance, and corresponds to the NSArray method getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes:NSMutableArray insertObject:atIndex: and insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes:NSMutableArray removeObjectAtIndex: and removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>:// Both of these are optional and should be considered if there are performance issues with such operations
Copy the code

(3) Usage scenarios

A. Dynamically value and set values

B. Access and modify private variables

C. Modell and dictionary conversion

D. Modify internal properties of some controls

Metagtext in UITextField for example

[textField setValue:[UIFont systemFontOfSize:25.0] forKeyPath:@"_placeholderLabel.font"];
Copy the code

How do I get internal properties of a control?

unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList([UITextField class], &count);
for (int i = 0; i < count; i++) {
    objc_property_t property = properties[i];
    const char *name = property_getName(property);
    NSLog(@"name:%s",name);
}
Copy the code

E. Higher order messaging

When using KVC on a container class, valueForKey: is passed to every object in the container, not the container itself. The result is added to the returned container so that the developer can easily manipulate the collection to return another collection.

NSArray *arr = @[@"ali".@"bob".@"cydia"];
NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
for (NSString *str  in arrCap) {
    NSLog(@ "% @",str);        //Ali\Bob\Cydia
}
Copy the code

Set of function operations in F.VC

  • Simple set operator

    • @avg
    • @count
    • @max
    • @min
    • @sum
    @interface Book : NSObject
    @property (nonatomic.assign)  CGFloat price;
    @end
    
    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
    Copy the code
  • Object operator

    • @distinctUnionOfObjects
    • @unionOfObjects
    // Get an array of all Book prices, and deduplicate them
    NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
    Copy the code
  • Array and Set operators (sets within sets)

    • @distinctUnionOfArrays
    • @unionOfArrays
    • @distinctUnionOfSets

Five, KVC principle

Project source in ** KvC-02-principle **

  • setValue:forKey

  • valueForKey

Six, extension,

1._NSSetLongLongValueAndNotify

// Get the Foundation library Check the Notify function $nm. / Foundation | grep LongValueAndNotify 22 __NSSetLongLongValueAndNotify 22 bc90a0 bc9290 t t __NSSetLongValueAndNotify 22bc93a0 t __NSSetUnsignedLongLongValueAndNotify 22bc9198 t __NSSetUnsignedLongValueAndNotify 22bc9d18 t ____NSSetLongLongValueAndNotify_block_invoke 22bc9ca8 t ____NSSetLongValueAndNotify_block_invoke 22bc9d4c t ____NSSetUnsignedLongLongValueAndNotify_block_invoke 22bc9ce0 t ____NSSetUnsignedLongValueAndNotify_block_invokeCopy the code

reference

link

  1. Key-Value Coding Programming Guide
  2. Key-Value Observing Programming Guide
  3. KVC and KVO
  4. Key-Value Observing

The sample code

  1. KVC-01-usage
  2. KVC-02-principle
  3. KVO-01-usage
  4. KVO-02-crash
  5. KVO-03-principle
  6. KVOLearnDemo

tool

KVOController is a KVO library from Facebook.