KVC

For some tips on how to use KVC, please refer to the previous brief note: tips on how to use KVC in iOS

KVO

KVO is a key value observation mechanism based on KVC. Leaving aside the basic use of KVO, let’s see how adding KVO affects Person:

The code in this article is based on the definition of the Person class:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end
Copy the code
static void *kContextPersonKVO = &kContextPersonKVO; . Person *p = [[Person alloc] init]; [p addObserver:selfforKeyPath:@"age"options:NSKeyValueObservingOptionNew context:kContextPersonKVO]; p.age = 10; / / the breakpointCopy the code

View at breakpoint:

isa-swizzling

The implementation mechanism of KVO is ISA-Swizzling.

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.

After KVO does isa-Swizzling, it dynamically creates a subclass that inherits from the original Class. If the original class is Person, then dynamically create an NSKVONotifying_Person inherited from the Person class. For an instance object P of the Person class, after adding KVO to one of its attributes, Use the object_getClass(self) method to view its ISA as NSKVONotifying_Person, Use class_getSuperclass(object_getClass(self)) to check that its super class is Person. So the key to ISa-Swizzling is to point the ISA pointer of the observed object (instance object P) to the newly created class NSKVONotifying_Person. Using [self class], you still get Person, and [self superClass] gets NSObject. The isa, super_class, and setter methods for detecting properties of the observed object (instance object P) are changed after KVO is added. For example, if we call the setter for the age property, we’ll actually go to NSKVONotifying_Person and find the setAge: method that we overwrote, Use willChangeForValue: and didChangeForValue: to implement listening on setter methods. If you assign to the instance variable _age directly instead of using setter methods, KVO’s response methods will not be triggered.

The removeObserver method points isa back to the original Person class.

Usage scenarios of KVO

Listen for property changes on an object

This is the most basic usage of KVO, so I won’t go into it

Observer model

The other most common use of KVO is the Observer mode. For example, KVO monitoring of the age attribute of the instance variable P of Person can obtain the changes of age at any time and make corresponding responses.

Two-way binding

KVO allows bidirectional binding to encapsulate a responsive framework. This is something that RAC and RxSwift are worth investigating.

Notes for KVO

Not all attributes can be listened on.

If you use KVO to listen on the frame property of UIView and change its center property, KVO will not be triggered. Because changing center does not call the setter methods for the frame property, you can use willChangeValueFor: and didChangeValueFor: in the setter methods for center to trigger the KVO for the frame property.

- (void)setCenter:(CGPoint)center
{
    [aView willChangeValueForKey:@"center"]; // Calculate new frame CGRect newFrame = XXX according to center; aView.frame = newFrame; [aView didChangeValueForKey:@"center"];
}
Copy the code

Manually listen for properties of NSOperation

By default, these three properties of NSOperation are read-only,

@interface NSOperation : NSObject

@property (readonly, getter=isCancelled) BOOL cancelled;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;

@end
Copy the code

What if we want to assign values to these three properties, so we can control the NSOperation state ourselves? You can use the following methods:

@interface CSDownloadOperation: NSOperation @end @interface CSDownloadOperation () // Because these properties arereadonly@property (assign, Nonatomic, getter = isExecuting) BOOL created; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (assign, nonatomic, getter = isCancelled) BOOL cancelled; @end @implementation CSDownloadOperation @synthesize executing = _executing; @synthesize finished = _finished; @synthesize cancelled = _cancelled; // MARK: - setter - (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"]; } /** finished Sets the isFinished state. This command cannot be executed before start, otherwise crash will occur. */ - (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setCancelled:(BOOL)cancelled
{
    [self willChangeValueForKey:@"isCancelled"];
    _cancelled = cancelled;
    [self didChangeValueForKey:@"isCancelled"];
}

@end
Copy the code

When a new value needs to be set, KVO is triggered manually and the corresponding instance variable is assigned. This can be done by customizing an NSOperation to perform asynchronously, such as a download operation encapsulated with NSURLSession.

Monitor mutable collections

Mutable collections cannot be monitored using the common methods of KVO, Only through mutableArrayValueForKey: mutableSetValueForKey: mutableOrderedSetValueForKey: respectively to the NSMutableArray, NSMutableSet, NSMutableOrderedSet does the monitoring.

We would also like to point out that collections as such are not observable. KVO is about observing relationships rather than collections. We cannot observe an NSArray; We can only observe a property on an object — and that property may be an NSArray. As an example, if we have a ContactList object, we can observe its contacts property, but we cannot pass an NSArray to -addObserver:forKeyPath:… as the object to be observed.

mutableArrayValueForKey

For example, in the following code, we want to KVO monitor a mutable array of selectedMaterials to update the UI and code logic.

@property (nonatomic, strong) NSMutableArray *selectedMaterials;


[self addObserver:self
       forKeyPath:@"selectedMaterials"
          options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
          context:&ctxKVOSelectedMaterials];
       
              
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &ctxKVOSelectedMaterials) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self updateSelectedCount];
            
            [self.collectionView reloadData];
        });
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
       
[self removeObserver:self
          forKeyPath:@"selectedMaterials"
             context:&ctxKVOSelectedMaterials];
                 
Copy the code

Also, the code that triggers KVO is different. Make sure you get the mutable set [self mutableArrayValueForKey:@”selectedMaterials”]

// Each add triggers KVO [[self mutableArrayValueForKey:@"selectedMaterials"] addObject:material.number];
[[self mutableArrayValueForKey:@"selectedMaterials"] removeObject:material.number];
[[self mutableArrayValueForKey:@"selectedMaterials"] removeAllObjects];
Copy the code
/ / add triggers only once many times KVO NSIndexSet * indexSet = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange (self. SelectedMaterials. Count,  materials.count)]; [[self mutableArrayValueForKey:@"selectedMaterials"] insertObjects:materials atIndexes:indexSet];
Copy the code

Reference Documents:

In order to be key-value coding compliant for a mutable ordered to-many relationship you must implement the following methods: -insertObject:inAtIndex: or -insert:atIndexes:. At least one of these methods must be implemented. These are analogous to the NSMutableArray methods insertObject:atIndex: and insertObjects:atIndexes:. -removeObjectFromAtIndex: or -removeAtIndexes:. At least one of these methods must be implemented. These methods correspond to the NSMutableArray methods removeObjectAtIndex: and removeObjectsAtIndexes: respectively. -replaceObjectInAtIndex:withObject: or -replaceAtIndexes:with:. Optional. Implement if benchmarking indicates that performance is an issue. The -insertObject:inAtIndex: method is passed the object to insert, and an NSUInteger that specifies the index where it should be inserted. The -insert:atIndexes: method inserts an array of objects into the collection at the indices specified by the passed NSIndexSet. You are only required to implement one of these two methods.

The disadvantage of KVO

You can’t use blocks

To address this, there are third-party libraries that encapsulate KVO themselves, adding an API that can pass blocks, similar to some of the methods in NSNotificationCenter.

Removing an object that has been dealloc will crash

This is especially important. Generally, make sure that addObserver is paired with removeObserver.

Swift native classes do not support KVO

KVO is a feature of Runtime, so in Swift KVO is only valid for subclasses of NSObject, and you need to use the dynamic keyword for listening properties. However, attributes in Swift with the willSet and didSet methods are more practical than KVO.

The same thread

KVO’s observeValueForKeyPath method executes on the same thread as the code that executes the setter method for the listened property. To perform tasks for other threads in observeValueForKeyPath, use dispatch_async(XXX). This is similar to NSNotification. NSNotification cannot cross threads: Actions that respond to notifications are by default on the same thread as postNotification. If you want to execute a method that responds to notifications in a specified thread, you can either use the addObserver method with a block or use dispatch_async(XXX).

FBKVOController

FBKVOController is Facebook’s open source KVO encapsulation library, which is optimized for some of the shortcomings of KVO and easier to use.

reference

Key-Value Coding and Observing