IOS Low-level exploration series

  • IOS low-level exploration – alloc & init
  • IOS underlayer exploration – Calloc and Isa
  • IOS Low-level exploration – classes
  • IOS Low-level exploration – cache_t
  • IOS Low-level exploration – Methods
  • IOS Low-level exploration – message lookup
  • IOS Low-level exploration – Message forwarding
  • IOS Low-level exploration – app loading
  • IOS low-level exploration – class loading
  • IOS Low-level exploration – classification loading
  • IOS low-level exploration – class extension and associated objects
  • IOS Low-level exploration – KVC
  • IOS Basics – KVO

IOS leak check and fill series

  • IOS leak fix – PerfromSelector
  • IOS bug fix – Threads
  • – RunLoop for iOS
  • IOS – LLVM & Clang

In Objective-C and Cocoa, there are many ways for events to communicate with each other, and each has varying degrees of form and coupling: NSNotification & NSNotificationCenter provides a central hub where any part of an application can notify or be notified of changes to other parts of the application. The only thing you need to know is what you’re looking for, mainly the name of the notice. UIApplicationDidReceiveMemoryWarningNotification is sent application, for example, a signal out of memory. By listening for changes on a particular Key path, special event introspection can be performed between specific object instances. For example, a ProgressView can look at the network request’s numberOfBytesRead to update its own Progress property. Delegate is a popular design pattern for passing events by defining a series of methods to be passed to a given processing object. For example, UIScrollView will send scrollViewDidScroll every time its scroll offset changes: Whether it’s a proxy Callbacks like the completionBlock in NSOperation (which fires when isFinished==YES), or a function pointer in C, Pass a function hooks like SCNetworkReachabilitySetCallback (3).

1. Preliminary study on KVO

According to the definition of Apple official documents, KVO (Key Value Observing) is based on KVC. Therefore, readers who are not familiar with KVC can check the last article about the underlying exploration of KVC.

I’m sure most developers will be familiar with KVO, but let’s review the KVO explanation on the website.

1.1 what isKVO?

KVO provides a mechanism for notifying observers when properties of other objects change. According to the definition on the official website, the classification of attributes can be divided into the following three types:

  • Attributes: Simple attributes, such as primitive data types, strings, and booleans, andNSNumberAnd other immutable types likeNSColorCan also be considered simple properties
  • To-one relationships: These are mutable object properties with their own properties. That is, the properties of an object can be changed without changing the object itself. For example, aAccountThe object may have oneownerProperty, which isPersonObject, andPersonThe object itself hasaddressProperties.ownerCan be changed, but does not need to beAccountHold theownerProperties. That is to say,AccountownerProperty is not changed, justaddressIt’s changed.
  • To-many relationshipsThese are collection object properties. Custom collection classes are commonly used, although they can also be usedNSArrayNSSetTo hold this collection.

KVO works for all three attributes. Here’s an example:

As shown above, the Person object has an Account attribute, and the Account object has balance and interestRate. And both properties are read and written to the Person object. If you want to implement a feature: when the balance or interest rate changes need to notify the user. In general, you can use polling, with the Person object periodically fetching balance and interestRate from the Account property. But this approach is inefficient and impractical, and a better approach is to use KVO, similar to how the Person object gets notified when the balance or interest rate changes.

The premise of realizing KVO is to ensure that the observed object conforms to the KVO mechanism. In general, objects that inherit from the NSObject root class and their properties automatically comply with KVO. Of course, you can also implement KVO compliance yourself. That is to say, the KVO mechanism is actually divided into automatic compliance and manual compliance.

Once you have determined that objects and attributes are KVO compliant, you need to go through three steps:

  • Observer registration

The Person object needs to register itself with one of the specific properties of the Account. This process is through the addObserver: forKeyPath: options: context: implementation, this method need to specify a listener who (the observer), listening (keypath), listening strategies (options), listening context (context)

  • Triggered callback by observer

Person object to receive the Account was listening property changes after the notice, need their own implementation observeValueForKeyPath: ofObject: change: context: method to receive the notice.

  • Unregister observer

Unregister is required when the observer is no longer listening or when its life cycle ends. Specific implementation is an object to be observed by a removeObserver: forKeyPath: message.

The biggest benefit of the KVO mechanism is that you don’t need to implement a mechanism to know when an object’s properties change and the results of that change.

1.2 KVOAnalysis of three processes

1.2.1 Observer Registration

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context
Copy the code

Observer: Registers the object for KVO notifications. Observers must implement the key – value method of observing observeValueForKeyPath: ofObject: change: context:. KeyPath: The keyPath of the observed property, which cannot be nil relative to the receiver. The combination of options: NSKeyValueObservingOptions, it specifies the observation in the notice what contains the context: in observeValueForKeyPath: ofObject: change: context: The context of the parameter passed to the observer

The first two parameters are easy to understand, while the options and context parameters require extra attention.

The options represent NSKeyValueObservingOptions bitmask, need to pay attention to NSKeyValueObservingOptionNew & NSKeyValueObservingOptionOld, Because these are often want to use, you can skip NSKeyValueObservingOptionInitial & NSKeyValueObservingOptionPrior:.

NSKeyValueObservingOptionNew: shows that the change dictionary should provide the new attribute value of a notice, how it might be possible. NSKeyValueObservingOptionOld: that should notice the change in the dictionary contains old attribute values, how it might be possible. NSKeyValueObservingOptionInitial: this enumeration values is more special, if you specify the enumeration values, changes in properties after immediately notify observers, the process even before the observer to register. If at the time of register configuration NSKeyValueObservingOptionNew, then in the notification of changes in the dictionary also contain NSKeyValueChangeNewKey, but does not include NSKeyValueChangeOldKey. (The observed property value may be old in the initial notification, but new to the observer.) The simple fact is that the enumerated value triggers an observeValueForKeyPath callback before the property changes. NSKeyValueObservingOptionPrior: this enumeration values will successively set out two consecutive times observeValueForKeyPath callback. There is also a key-notificationisPrior Boolean value in the mutable dictionary in the callback to indicate whether the property value is before or after the change. If is change after the callback, the variable dictionary is only a new value, if at the same time established NSKeyValueObservingOptionNew. If you need to start manual KVO, you can specify the enumerated value and then observe the property value through the willChange instance method. It may be too late to invoke willChange after starting the observeValueForKeyPath callback.

These options allow an object to get its value before and after the change. In practice, it is not necessary, because from the current attribute values for the new value is generally available That means NSKeyValueObservingOptionInitial for when feedback KVO events to reduce code path, it is very good. For example, if you have a method, which can dynamically based on the text value to make a button, the NSKeyValueObservingOptionInitial can make the event as its initialization state trigger as soon as the observer is added.

How do you set a good context value? Here’s a suggestion:

static void * XXContext = &XXContext;
Copy the code

It’s as simple as that: a static variable holds its own pointer. This means that it has nothing of its own, making

even more perfect.

Let’s briefly test the effect of specifying a different enumeration value when registering an observer:

  • Only the specifiedNSKeyValueObservingOptionNew

  • Only the specifiedNSKeyValueObservingOptionOld

  • The specifiedNSKeyValueObservingOptionInitial

As you can see, only specifies the NSKeyValueObservingOptionInitial triggered after two callback, and a is in front of the property value changes, changes in property values. And there is no new value and returns the old value at the same time, we add a NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld:

After we add the enumeration of the new value and the old value, the new value is returned after two callbacks, but the first new value is actually the original property value, the second changed property value is the changed property value, and the old value is returned after the second real property value is changed.

  • The specifiedNSKeyValueObservingOptionPrior

As you can see, NSKeyValueObservingOptionPrior enumeration values is after the property value changes triggered the callback twice, at the same time also no new value and old value return, We add a NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld:

As you can see, there is no new value in the first callback, only the second, and the old value in both callbacks.


  • KeyPath string problem

When we register an observer, we require that the keyPath passed in be a string, and if we spell it wrong, the compiler can’t check it out, so the best practice is to use NSStringFromSelector(SEL aSelector), For example, if we want to observe the contentSize property of the tableView, we can use it like this:

NSStringFromSelector(@selector(contentSize))
Copy the code

1.2.2 Observer receives notification

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
Copy the code

This method is where the observer receives the notification. Except for the change parameter, the other three parameters correspond to the three parameters passed in when the observer is registered.

  • Different objects listen for the samekeypath

By default, we in the addObserver: forKeyPath: options: context: Nil is the literal zero value of an object. We need a pointer, so NULL is passed. Context is void *.

But if different objects are listening for the same property, we need to pass a string pointer to the context that distinguishes the different objects:

static void *StudentNameContext = &StudentNameContext;
static void *PersonNameContext = &PersonNameContext;

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context{
  
  if (context == PersonNameContext) {
  
  } else if (context == StudentNameContext) {
    
  }
  
}

Copy the code
  • You have to deal with it yourselfsuperclassobserveThe transaction

In Objective-C, many times the Runtime system will automatically help with superclass methods. For example, for dealloc, assuming that Father inherits from NSObject and Son inherits from Father, create an instance of Son aSon. When aSon is released, the Runtime calls Son#dealloc first, Father#dealloc is then automatically called without having to explicitly do [super dealloc] in Son#dealloc; . KVO does not do this, so to ensure that the observe-transaction of the parent class (which may also have its own observe-transaction to process) can also be processed.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath >isEqualToString:@"contentSize"]) { [self configureView]; } else { [super observeValueForKeyPath:keyPath ofObject:object >change:change context:context]; }}Copy the code

1.2.3 Canceling Registration

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
Copy the code

There are two methods for deregistering, but it is recommended that you use the same method as the registration and notification process and use the method with the context parameter.

  • Deregistration is a one-to-one relationship with registration

Once a property on an object is registered for key-value observation, you can choose to unregister the property after receiving a change in its value, or before the end of the observer declaration cycle (for example: Dealloc method) unregister, if you forget to call the unregister method, the KVO mechanism will send a change callback message to a nonexistent object once the observer is destroyed, resulting in a wild pointer error.

  • You cannot cancel registration twice

Unregistration can also not be repeated for the same observer more than once. To avoid a crash, wrap the unregistration code in a try&catch block:

static void * ContentSizeContext = &ContentSizeContext;
    
- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    // 1. subscribe
    [_tableView addObserver:self
                 forKeyPath:NSStringFromSelector(@selector(contentSize))
                    options:NSKeyValueObservingOptionNew
                    context:ContentSizeContext];
}
    
// 2. responding
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if (context == ContentSizeContext) {
        // configure view
    } else{[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context]; }} - (void)dealloc {
    @try {
        // 3. unsubscribe
        [_tableView removeObserver:self
                        forKeyPath:NSStringFromSelector(@selector(contentSize))
                           context:ContentSizeContext];
    }
    @catch (NSException *exception) {
        
    }
}
Copy the code

1.3 “Automatic Transmission” and “Manual Transmission”

By default, we only need to follow the above three steps to observe the key value of the property, but this is “automatic”, what means? That is, the system controls the change of attribute values. We just tell the system what attribute to listen for, and then wait for the system to tell us. In fact, KVO also supports manual transmission.

To let the system know we want to open the manual need to modify the class methods automaticallyNotifiesObserversForKey: return values, if this method returns YES is automatic, return is NO manual. It can also be very precise, letting us choose which attributes are automatic and which are manual.

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
Copy the code

Similarly, as shown in the code, we use automaticallyNotifiesObserversForKey best practice still need to add the code that we need to manually or automatically ruled out to call after the superclass method to ensure that there would be no problem.

  • automaticKVOtriggered
// Call the accessor method.
[account setName:@"Savings"];
 
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
 
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
Copy the code

Automatic KVO is triggered as shown in the code above

  • manualKVOtriggered

In fact, manual KVO can help us combine changes to multiple attribute values into one, so that there is only one callback, while minimizing notification for application-specific reasons.

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
Copy the code

As shown in the code above, the simplest way to use KVO manually is to send the willChangeValueForKey instance method to the observer before the property value changes, and didChangeValueForKey instance method to the observer after the property value changes, both taking the observed key. Of course, the above method is not optimal. For best performance, you can determine whether to execute will + DID in the setter of the property:

- (void)setBalance:(double)theBalance {
    if(theBalance ! = _balance) { [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"]; }}Copy the code

However, if a change to a property affects more than one key, the following action is required:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
Copy the code

For ordered one-to-many relationship properties, you must specify not only the changed key, but also the type of change and the index of the object involved. The type of change is NSKeyValueChange, it specifies NSKeyValueChangeInsertion, NSKeyValueChangeRemoval or NSKeyValueChangeReplacement, The index of the affected object is passed as an NSIndexSet:

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}
Copy the code

1.4 Register affiliationKVO

A dependency relationship is one in which the value of an attribute of an object depends on one or more attributes of another object. For different types of attributes, there are different ways to implement them.

  • One-to-one relationship

To trigger automatic KVO for one-to-one type attributes, there are two ways. Is a kind of rewriting keyPathsForValuesAffectingValueForKey method, one kind is to achieve an appropriate method.

- (NSString *)fullName {
    return [NSString stringWithFormat:@ % @ % @ "",firstName, lastName];
}
Copy the code

For example, in the code above, fullName consists of firstName and lastName, so override the getter method for the fullName attribute. This way, observers listening to the fullName attribute will be notified if firstName or lastName is changed.

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName".@"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
Copy the code

As shown in the code, through the implementation class methods keyPathsForValuesAffectingValueForKey to return a collection. It is important to note that there needs to be first keyPathsForValuesAffectingValueForKey messages sent to the parent class, so as not to interfere with this method in the parent class to rewrite.

There is also a convenient way, actually is keyPathsForValuesAffecting < Key >, the Key is the name of the attribute (need to capitalize the first letter). The effect of this method and keyPathsForValuesAffectingValueForKey is the same, but on a particular attribute.

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName".@"firstName".nil];
}
Copy the code

Relatively speaking, to use in the classification of keyPathsForValuesAffectingFullName more reasonable, because the overloading method is not allowed in the classification, So keyPathsForValuesAffectingValueForKey method certainly can’t be used in the classification.


  • One-to-many relationship

KeyPathsForValuesAffectingValueForKey: method does not support the Key Path contains a one-to-many relationship. For example, suppose you have a Department object that has a one-to-many relationship with Employee (the employees attribute), and Employee has a salary attribute. If you need to add a totalSalary attribute to the Department object that depends on the salary of all Employees in the relationship. For example, you cannot use keyPathsForValuesAffectingTotalSalary and return to the employees. The salary as the key to perform the operation.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if(totalSalary ! = newTotalSalary) { [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"]; }} - (NSNumber *)totalSalary {
    return _totalSalary;
}
Copy the code

As shown in the code above, register the Department instance object as an observer and then observe the object as the totalSalary property, but the setter method for the totalSalary property is manually called in the notification callback, And the value passed in is the sum of all the sum values in the set of employees by means of KVC’s set operator. Then in setter methods for the totalSalary property, the willChangeValueForKey: and didChangeValueForKey: methods are called accordingly.

If you’re using Core Data, you can also register the Department with the NSNotificationCenter as an observer of the managed object context. The Department should respond to relevant change notifications from Employee in a manner similar to observing key values.

2. Exploring the principle of KVO

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 The ISA pointer, as the name implies, points to the class to which the object belongs, which maintains a hash table. This hash table basically stores SEL and IMP key-value pairs of methods.

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.

When an observer registers an observation of an object’s property key, the isa pointer to the observed object changes, pointing to an intermediate class instead of the real class. This also results in the isa pointer not necessarily pointing to the real class to which the instance belongs.

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.

You should never rely on isa Pointers to determine class membership. Instead, you should use the class method to determine which class an object instance belongs to.

2.1 the middle class

According to the documentation on the official website, we preliminatively judge that there will be a so-called intermediate class generation in the KVO underlying implementation. This intermediate class changes the object’s ISA pointer. Let’s test this out:

As shown above, both the Person object and the personForTest object belong to the JHPerson class, and the Person object implements KVO, but you can see in the console print that both are JHPerson classes. Isn’t there going to be an intermediate class generated? Did the intermediate class generation get killed again? Let’s test the LLDB directly:

Bingo, the so-called intermediate class NSKVONotifying_JHPerson has been found. So we’ve overridden the middle class NSKVONotifying_JHPerson to make us think that the isa pointer to the object is always pointing to the JHPerson class. So how does this middle class relate to the original class? We can test this:

Where printClasses is implemented as follows:

- (void)printClasses:(Class)cls{
    int count = objc_getClassList(NULL.0);
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
Copy the code

The final output is as follows:

classes = (
    JHPerson
)
classes = (
    JHPerson,
    "NSKVONotifying_JHPerson"
)
Copy the code

The result is clear: the middle class NSKVONotifying_JHPerson is a role that is a subclass of the original true class JHPerson.

2.2 KVOWhat was observed?

What KVO cares about is changing the value of a property, which is essentially a member variable +getter+setter. The getter is used to get the value, and obviously there are only two ways to change the value of a property: setter and member variable assignment. Let’s test these two methods:

// JHPerson.h
@interface JHPerson : NSObject {
    @public
    NSString *_nickName;
}
@property (nonatomic.copy) NSString *name;
@end
Copy the code

As shown in the figure above, the change of the setter method to the property name was detected by KVO, but the change of the member variable _nickName was not detected, indicating that KVO was actually observing the setter method.

2.3 What methods do intermediate classes override?

We can verify this by printing a list of methods for the original and intermediate classes:

The printClassAllMethod is implemented as follows:

- (void)printClassAllMethod:(Class)cls{
    NSLog(@ "* * * * * * * * * * * * * * * * * * * * *");
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p".NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
Copy the code

As you can see in the figure above, both the primitive class and the intermediate class have setter methods. Based on the message-sending and forwarding process we explored earlier, the intermediate classes here should override the setName:, class, dealloc, and _isKVOA methods.

As we can see from the test results in the previous section, the class method overridden by the intermediate class still returns the original class. Obviously, the purpose of this system is to hide the existence of the intermediate class and make the results consistent when the caller calls the class method.

2.4 KVOWhen does the middle class go back?

We infer that the ISA pointer of the observed object will point to the middle class only when KVO registers the observer and removes the observer. Let’s test it with code:

As you can see from the above figure, after the observer is removed from the observer’s dealloc method, the object’s ISA pointer points back to the original class. If the intermediate class is destroyed, print all the subclasses of the original class:

It turns out that the intermediate class still exists, meaning that removing the observer does not cause the intermediate class to be destroyed, which is obviously better for adding and removing observers multiple times.

2.5 KVOCall to order

And we said earlier, there is the existence of a middle class, since to generate the middle class, it must be meaningful, we comb the KVO process, from the registered observers the correction notice to the viewer, since have correction notice, so it must be somewhere a callback, and as a result of the middle class is not compile, So for the parent of the middle class, the JHPerson class, let’s rewrite the corresponding setter method, so let’s test it:

// JHPerson.m
- (void)setName:(NSString *)name
{
    _name = name;
}

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}
Copy the code

The print result is as follows:

In other words, the call order of KVO is:

  • callwillChangeValueForKey:
  • Call the originalsetterimplementation
  • calldidChangeValueForKey:

That is didChangeValueForKey: internal necessarily call the observer observeValueForKeyPath: ofObject: change: context: method.

How to achieve custom KVO

Now that we’ve taken a look at the underlying principles of KVO, let’s try a simple implementation of KVO ourselves. We jump straight to the addObserver: forKeyPath: options: context: method statement:

As you can see, just like KVC, KVO loaded at the bottom is also in the form of classification, the classification is called NSKeyValueObserverRegistration. We might as well implement KVO customarily in this way.

// NSObject+JHKVO.h
@interface NSObject (JHKVO)
// Observer registration
- (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(nullable void *)context;
// The callback notifies the observer
- (void)jh_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey.id> *)change context:(nullable void *)context;
// Remove the observer
- (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
@end
Copy the code

A method prefix is added here to avoid conflicts with the system’s methods. Also, for observation policies, only new value and old value policies are declared to simplify implementation.

3.1 Custom observer registration

Before we start, let’s recall that the first step in customizing KVC is to determine key or keyPath. Does KVO need to do the same? In my actual tests, observing a nonexistent property of an object does not generate an error and does not bring the KVO callback method, so it is not necessary to determine whether keyPath exists or not. However, if we recall the underlying principles of KVO from the previous section, KVO focused on setter methods for properties, and determining whether an object belongs to a class that has such a setter is equivalent to determining whether keyPath exists at the same time. And then we need to dynamically subclass, which involves overriding setters and so on. Then we need to save the observer and keyPath information. We do this by using the associated object. We wrap the passed observer object, keyPath, and observation policy into a new object and store it in the associated object. Because the properties of the same object can be observed by different observers, they are essentially stored in an array of associated objects. Without further ado, go directly to the code:

// JHKVOInfo.h
typedef NS_OPTIONS(NSUInteger, JHKeyValueObservingOptions) {
    JHKeyValueObservingOptionNew = 0x01,
    JHKeyValueObservingOptionOld = 0x02};@interface JHKVOInfo : NSObject
@property (nonatomic.weak) NSObject  *observer;
@property (nonatomic.copy) NSString  *keyPath;
@property (nonatomic.assign) JHKeyValueObservingOptions options;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options;
@end

// JHKVOInfo.m
@implementation JHKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options
{
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _options = options;
    }
    return self;
}
@end
Copy the code

The code above is a custom JHKVOInfo object.

static NSString *const kJHKVOPrefix = @"JHKVONotifying_";
static NSString *const kJHKVOAssiociateKey = @"kJHKVO_AssiociateKey";
- (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JHKeyValueObservingOptions)options context:(void *)context
{
    // 1. Check whether the getter exists
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if(! setterMethod) {NSString *reason = [NSString stringWithFormat:@" Key %@ of object %@ has no setter implementation".self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    // 2. Create intermediate subclasses dynamically
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3. Point the object's ISA to the new middle subclass
    object_setClass(self, newClass);
    
    // 4. Save the observer
    JHKVOInfo *info = [[JHKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    
    if(! observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void* _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }}Copy the code

The code above is the complete process for adding an observer:

  • Determines whether the object belongs to a class that needs to be observedkeyPathThe correspondingsettermethods

The setterForGetter implementation here is as follows:

static NSString * setterForGetter(NSString *getter)
{
   // Determine if the getter is an empty string
   if (getter.length <= 0) {
       return nil;
   }
   // Take the first letter of the getter string and uppercase it
   NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
   // Fetch the rest of the string
   NSString *remainingLetters = [getter substringFromIndex:1];
   // Concatenate the uppercase letter with the rest of the letters to form a string in the 'set
       
        ' format
       
   NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
   return setter;
}
Copy the code
  • If there is a correspondingsetterMethod, then create an intermediate subclass with the corresponding prefix

Here createChildClassWithKeyPath implementation is as follows:

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    // Get the class name of the original class
    NSString *oldClassName = NSStringFromClass([self class]);
    // Get the middle subclass name by prefixing the original class name with the middle subclass name
    NSString *newClassName = [NSString stringWithFormat:@ % @ % @ "",kJHKVOPrefix,oldClassName];
    // The middle subclass name is used to determine whether it was created
    Class newClass = NSClassFromString(newClassName);
    // If an intermediate subclass was created, return it
    if (newClass) return newClass;
    The objc_allocateClassPair method has three parameters: 1. Parent class 2. name of new class 3. extra space needed to create new class
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // Register intermediate subclasses
    objc_registerClassPair(newClass);
    // Get the 'SEL' of the 'class' method from the parent class and the type code, then add a new subclass implementation 'jh_class' to the middle subclass
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)jh_class, classTypes);
    // Get the SEL of the getter method and the type code from the parent class, and add a new subclass implementation jh_setter on the middle subclass
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)jh_setter, setterTypes);
    return newClass;
}
Copy the code

Jh_class is implemented as follows:

Class jh_class(id self,SEL _cmd) {
   // Use class_getSuperclass to return the 'Class' of the parent Class, so that callers can hide the middle Class
   return class_getSuperclass(object_getClass(self));
}
Copy the code

The implementation of jh_setter is as follows:

static void jh_setter(id self,SEL _cmd,id newValue){
    // Because '_cmd' as the second argument to the method is actually the 'SEL' of the 'setter', here we get the corresponding 'getter' string form as the 'keyPath', and then we get the old property value through 'KVC'
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    
    // Because it overrides the 'setter' of the parent class, the following 'setter' methods need to be manually executed via message sending
    // Strong-cast 'objc_msgSendSuper' into 'jh_msgSendSuper', and since 'objc_msgSendSuper' has one more parent structure parameter than the usual 'objc_msgSend', So you need to manually build the superclass structure. The structure has two properties: the instance object and the parent of the instance object's class
    void (*jh_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... * /
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),};// Call 'jh_msgSendSuper' manually after the preparation is complete, because 'superStruct' is a structure type and the first argument to 'jh_msgSendSuper' is a null pointer object, so we need to add the address to assign the structure address to the pointer object
    jh_msgSendSuper(&superStruct, _cmd, newValue);
    // After calling the 'setter' of the parent class, fetch the array of objects that store the custom from the associated object
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    // Loop over the custom object
    for (JHKVOInfo *info in observerArr) {
    // If 'keyPath' matches, go to the next step
        if ([info.keyPath isEqualToString:keyPath]) {
            // For thread safety reasons, use the global queue of 'GCD' to perform the following operations asynchronously
            dispatch_async(dispatch_get_global_queue(0.0), ^ {// Initialize a notification dictionary
                NSMutableDictionary<NSKeyValueChangeKey.id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // Determine the stored observation policy and set the new value in the notification dictionary if it is a new value
                if (info.options & JHKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                // If the value is old, set the old value in the notification dictionary
                if (info.options & JHKeyValueObservingOptionOld) {
                    [change setObject:@ "" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey]; }}// get 'SEL' of the notification observer method
                SEL observerSEL = @selector(jh_observeValueForKeyPath:ofObject:change:context:);
                // Manually send the message through 'objc_msgSend', so that the observer receives the callback
                ((void(*) (id, SEL, id.id.NSMutableDictionary *, void *))objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL); }); }}}Copy the code

GetterForSetter is implemented as follows:

static NSString *getterForSetter(NSString *setter) {// Determine if the 'setter' string passed is longer than 0 and has a 'set' prefix and a ':' suffix
    if (setter.length <= 0| |! [setter hasPrefix:@"set") | |! [setter hasSuffix:@ ","]) { return nil; }// Get the getter string by excluding the 'set:' part of the 'setter' string
    NSRange range = NSMakeRange(3.setter.length4 -);
    NSString *getter = [setter substringWithRange:range];
    // Lowercase the first letter of the getter string
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0.1) withString:firstString];
}
Copy the code

3.2 Custom Remove observer

First, we need to refer isa back to the original class. Then we need to remove the observer corresponding to the array of custom objects stored in the associated object.

- (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context
{
    // Fetch the array from the associated object
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    // If there is no content in the array, no observer has been added
    if (observerArr.count<=0) {
        return;
    }
    
    // Iterate over all custom objects retrieved
    for (JHKVOInfo *info in observerArr) {
        // If 'keyPath' matches, the response object is removed from the array and the latest array is stored on the associated object
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break; }}// To refer 'isa' back to the original class, the prerequisite is that the object under observation is no longer being observed by any observer, so we need to refer back
    if (observerArr.count<=0) {
        Class superClass = [self class];
        object_setClass(self, superClass); }}Copy the code

3.3 Implement automatic observer removal

Now our custom KVO can simply notify the observer of the change of the new value and the old value, but in fact, API users still have to strictly implement the matching operation of addObserver and removeObserver, it is difficult to avoid. Although it is generally convenient to manually call the removeObserver method in the observer’s dealloc method, this is still too cumbersome. Therefore, we can use the techniques of methodSwizzling to replace the implementation of the default dealloc method with code:

+ (BOOL)jh_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
    // Get the Class object
    Class cls = self;
    // Get the original method from 'SEL'
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    // Get the method to replace through 'SEL'
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    // If the method to replace does not exist, return NO
    if(! swiMethod) {return NO;
    }
    // If the original method does not exist, add the replaced method directly to the Class. Note that the added method is implemented as the replaced method, but the method 'SEL' is still the original method 'SEL'
    if(! oriMethod) { class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    // Check whether the server is successfully added
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
    // If it succeeds, the implementation of the method to be replaced already exists on the Class, so replace the original method implementation with the corresponding implementation of 'swizzledSEL'
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
    // If not, the original method already exists
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    return YES;
}


+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self jh_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
    });
}

- (void)myDealloc{
    Class superClass = [self class];
    object_setClass(self, superClass);
    // There is no recursive loop reference here, because the method implementation of 'myDealloc' is really the original 'dealloc'
    [self myDealloc];
}
Copy the code

By implementing automatic observer removal, API users can safely use only addObserver to add observers and observeValueForKeyPath to receive callbacks.

3.4 Refactoring of functional programming ideas

We have implemented automatic observer removal, but from a functional programming perspective, the current design is not perfect, the observation of the same property of the code scattered in different places, if the business once increased, the readability and maintainability of a great deal of impact. So, we can refactor the current callback into a Block.

// NSObject+JHBlockKVO.h
typedef void(^JHKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface NSObject (JHBlockKVO)
- (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JHKVOBlock)block;
- (void)jh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end

// NSObject+JHBlockKVO.m
@interface JHBlockKVOInfo : NSObject
@property (nonatomic.weak) NSObject   *observer;
@property (nonatomic.copy) NSString   *keyPath;
@property (nonatomic.copy) JHKVOBlock  handleBlock;
@end

@implementation JHBlockKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(JHKVOBlock)block{
    if (self= [super init]) {
        _observer = observer;
        _keyPath  = keyPath;
        _handleBlock = block;
    }
    return self;
}
@end

@implementation NSObject (JHBlockKVO)
- (void)jh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JHKVOBlock)block{
    
    // 1. Check whether the getter exists
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if(! setterMethod) {NSString *reason = [NSString stringWithFormat:@" Key %@ of object %@ has no setter implementation".self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    // 2. Create intermediate subclasses dynamically
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3. Point the object's ISA to the new middle subclass
    object_setClass(self, newClass);
    
    // 4. Save the observer
    JHBlockKVOInfo *info = [[JHBlockKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    if(! mArray) { mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
} 
Copy the code

Here we’re going to classify a block directly by passing it in and storing it in the corresponding custom observation object, and then we’re going to have to make a change in our override setter method, where we used to send messages directly to implement the callback, and now we need to change it to a block callback

static void jh_setter(id self,SEL _cmd,id newValue){
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    void (*jh_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),}; jh_msgSendSuper(&superStruct,_cmd,newValue);NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJHKVOAssiociateKey));
    for (JHBlockKVOInfo *info in mArray) {
        if([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

Four,

After exploring the underlying layers of KVC and KVO, we can see that KVO is built on top of KVC. KVO as the observer design pattern in iOS concrete landing, its principle to the implementation of we have explored. In fact, we can see that in the early days of the API design, the native KVO didn’t work well, so libraries like FaceBook’s KVOController would be popular. Of course, this article’s custom KVO implementation is not rigorous, but interested readers can check out these two code libraries:

  • According to the originalKVCKVODisassembleDIS_KVC_KVO
  • Open sourceGNUSteplibs-base gnustep/libs-base

Stay tuned for the next chapter in our iOS Underground-exploration series on multithreading

The resources

Key-value Observing Programming Guide-Apple Official document

nil/Nil/Null/NSNull – NSHipster

Key-Value Observing – NSHipster