Welcome to the iOS Exploration series.

  • IOS explores the alloc process
  • IOS explores memory alignment &malloc source code
  • IOS explores ISA initialization & pointing analysis
  • IOS exploration class structure analysis
  • IOS explores cache_T analysis
  • IOS explores the nature of methods and the method finding process
  • IOS explores dynamic method resolution and message forwarding mechanisms
  • IOS explores the dyLD loading process briefly
  • The loading process of iOS explore classes
  • IOS explores the loading process of classification and class extension
  • IOS explores isa interview question analysis
  • IOS Explore Runtime interview question analysis
  • IOS explores KVC principles and customization
  • IOS explores KVO principles and customizations
  • IOS explores the principle of multithreading
  • IOS explores multi-threaded GCD applications
  • IOS explores multithreaded GCD underlying analysis
  • IOS explores NSOperation for multithreading
  • IOS explores multi-threaded interview question analysis
  • IOS Explore the locks in iOS
  • IOS explores the full range of blocks to read

Writing in the front

From KVC (key-value coding) to KVO (key-value observation), it may be used by readers, but do you really understand it? This paper will analyze the principle of KVO in all directions

1. Preliminary study on KVO

KVO (Key-value Observing) is a set of event notification mechanism provided by Apple. This mechanism allows you to notify objects of the changes of specific properties of other objects. IOS developers can use KVO to detect and respond quickly to changes in object properties, which can be a huge help in developing highly interactive, responsive applications and bidirectional binding of views and models.

In Documentation Archieve, it is mentioned that to understand KVO, you must first understand KVC, because key-value observation is based on key-value encoding

In order to understand key-value observing, You must first understand key-value coding. — key-value Observing Programming Guide

KVO and NSNotificatioCenter are both implementations of iOS observer mode. The difference between them is:

  • As opposed to the relationship between the observed and the observer,KVOIt’s one to one,NSNotificatioCenterIt’s one to many
  • KVOIt is non-intrusive to the monitored object and can be monitored without modifying its internal code

Two, KVO use and attention points

1. Basic use

KVO uses trilogy:

  • Registered observer
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
Copy the code
  • To implement the callback
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) NSLog(@ "% @", change);
}
Copy the code
  • Remove observer
[self.person removeObserver:self forKeyPath:@"name"];
Copy the code

2. The use of the context

The key-value Observing Programming Guide describes the context in this way

The context pointer in the message contains arbitrary data that is passed back to the observer in the corresponding change notification; You can specify NULL and rely entirely on the key path string to determine the source of the change notification, but this approach can cause problems by causing the object’s parent class to observe the same key path for different reasons; A more secure and extensible approach is to use context to ensure that the notifications you receive are addressed to the observer and not to the superclass.

If the parent class has a name attribute and the subclass has a name attribute, and both register the observation of name, then it is no longer possible to distinguish which name has changed using keyPath alone. There are two solutions:

  • Add another layer of judgment — judgmentobjectObviously, it is not desirable to add logical judgment to meet business requirements
  • usecontextDelivering information is more secure and scalable

contextSummary of use:

  • Do not use context as an observation value
// Context is void * and should be NULL instead of nil
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
Copy the code
  • Use context to pass information
static void *PersonNameContext = &PersonNameContext;
static void *ChildNameContext = &ChildNameContext;

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    if (context == PersonNameContext) {
        NSLog(@ "% @", change);
    } else if (context == ChildNameContext) {
        NSLog(@ "% @", change); }}Copy the code

3. The need to remove notifications

You may not think it matters whether or not you remove notifications in the course of daily development, but not removing notifications has potential pitfalls

Here is a piece of code that does not remove the observer, and it works fine before and after the page push and the key change

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.child = [FXChild new];
    self.child.name = @"Feng";
    
    [self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:ChildNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) NSLog(@"% @", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.child.name = [NSString stringWithFormat:@"% @ +", self.child.name];
}
Copy the code

But when FXChild is created as a singleton, pop goes back to the previous page and pushes in again and the program crashes

This is because if the observation is not removed, the singleton will still exist and the wild pointer error will be reported on re-entry

This does not happen once the observer is removed — it is necessary to remove the observer

Apple’s official recommendation is to use addObserver for init and removeObserver for Dealloc. This ensures that add and remove are paired, which is an ideal way to use them

4. Manually trigger key observation

Sometimes a business requirement needs to observe a property value, and then it needs to be observed, and then it doesn’t… Removing and adding the entire KVO trilogy would be a tedious and unnecessary task. Fortunately, there are two ways to manually trigger key observation in KVO:

  • Will be observedautomaticallyNotifiesObserversForKeyReturns NO (can be set for a property only)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
Copy the code
  • Override setter methods for observed properties with willChangeValueForKey, didChangeValueForKey

    These two methods are used to inform the system that the value of the key attribute is about to change or has changed

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}
Copy the code

The permutations and combinations of the two methods are as follows. How to use them

situation The callback number
normal 1
AutomaticallyNotifiesObserversForKey to NO 0
Add willChangeValueForKey, didChangeValueForKey automaticallyNotifiesObserversForKey to NO and 1
Add willChangeValueForKey, didChangeValueForKey automaticallyNotifiesObserversForKey to YES and 2

Recently found [self willChangeValueForKey: name] and [self willChangeValueForKey: “name”] two kinds of writing is the result of the different: rewrite the setter method takes attribute value operation will not send additional notice; Using “name” will send an additional notification

5. Observe many-to-one key values

For example, if there is a requirement of download task, the Current download progress Process can be obtained according to the Total downloads and Current downloads. This requirement can be realized in two ways:

  • Respectively to observeTotal downloadsandCurrent downloadsTwo properties, evaluated when one of them changesCurrent download progress Process
  • implementationkeyPathsForValuesAffectingValueForKeyMethods and observationprocessattribute

KeyPaths = Process can receive listening callbacks whenever the Total or Current download numbers change

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"process"]) {
        NSArray *affectingKeys = @[@"total".@"current"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
Copy the code

But that’s not enough — you can only listen for callbacks, but not complete Process assignment — you need to override the getter method

- (NSString *)process {
    if (self.total == 0) {
        return @ "0";
    }
    return [[NSString alloc] initWithFormat:@"%f".1.0f*self.current/self.total];
}
Copy the code

Mutable arrays

There is a variable array dataArray under FXPerson, now observe it and ask if clicking the screen prints.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [FXPerson new];
    [self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"dataArray"]) NSLog(@ "% @", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person.dataArray addObject:@"Felix"];
}
Copy the code

A: no

Analysis:

  • KVOIs based onKVCWhile mutable array direct addition is not calledA Setter method
  • Variable array dataArrayAn error will be reported if you add it without initialization
// Initialize the mutable array
self.person.dataArray = @[].mutableCopy;
// Call setter methods
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"Felix"];
Copy the code

Three, KVO principle — ISa-Swizzling

1. Official explanation

Key-Value Observing Programming Guide

  • KVO is to useisa-swizzlingTechnically realized
  • As the name implies, the ISA pointer points to the class that maintains the object of the allocation table, which essentially contains Pointers to the methods that the class implements, as well as other data
  • When registering an observer for an object’s properties, the ISA pointer to the observed object is modified to point to the intermediate class instead of the real class. The value of the ISA pointer does not necessarily reflect the actual class of the instance
  • You should never rely on isa Pointers to determine class membership. Instead, you should use the class method to determine the class of an object instance

2. Code exploration

This paragraph of words in the clouds, or knock code to see the real chapter

  • Before registering an observer: the class object isFXPersonThe instance object ISA points toFXPerson
  • After registering the observer: the class object isFXPersonThe instance object ISA points toNSKVONotifying_FXPerson

One conclusion can be drawn from these two figures: The FXPerson class does not change before and after observer registration, but the ISA direction of the instance object does

So what is the relationship between the dynamically generated middle class NSKVONotifying_FXPerson and FXPerson?

Call the print subclass method before and after registering the observer — NSKVONotifying_FXPerson is a subclass of FXPerson

3. Dynamic subclass exploration

What does a dynamic subclass observe? Let’s observe the difference between the attribute variable name and the member variable nickname

Both variables change at the same time, but only the property variable listens for callbacks — indicating that dynamic subclasses observe setter methods

② Print the methods of dynamic subclasses and observation classes via run-time API

- (void)printClassAllMethod:(Class)cls {
    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

  • FXPerson classThe imp implementation address has not changed.
  • NSKVONotifying_FXPerson classOverrides the parent class inFXPersonthedeallocmethods
  • NSKVONotifying_FXPerson classOverrides the base class inNSObjecttheclassMethods and_isKVOAmethods
    • Rewrite theclassMethods can refer backFXPerson class
  • NSKVONotifying_FXPerson classOverrides the parent class inFXPersonthesetNamemethods
    • Because subclasses only inherit, do not rewrite is not a method IMP, call method will ask the parent class to method implementation
    • And twosetNameThe address pointer is different
    • Every time you look at oneAttribute variablesI’ll just rewrite itsetterMethods (self-argumentation)

③ Who does Isa point to after dealloc? — Refers to the original class

④ Will dynamic subclasses be destroyed after dealloc? –

The page is pushed in again after pop to print the FXPerson class, and the subclass NSKVONotifying_FXPerson still exists

(5) automaticallyNotifiesObserversForKey generated will affect dynamic subclass –

Dynamic subclass will according to the observed properties of automaticallyNotifiesObserversForKey Boolean value to determine whether generation

4. To summarize

  1. automaticallyNotifiesObserversForKeyforYESDynamic subclasses are generated when observing properties are registeredNSKVONotifying_XXX
  2. Dynamic subclasses look atsettermethods
  3. The dynamic subclass overrides the observation propertysetterMethods,dealloc,class,_isKVOAmethods
    • setterThe key () method is used to observe key values
    • deallocMethod is used to operate on isa points at release time
    • classMethod is used to refer back to the parent of a dynamic subclass
    • _isKVOAA flag bit used to indicate whether it is in observer state
  4. deallocafterisaPointing to the metaclass
  5. deallocDynamic subclasses are not destroyed after that

4. Customize KVO

According to the official documentation of KVO and the conclusion above, we will customize KVO — the following customization will explain the use of Run-time API and interface design ideas, and the final customized KVO can meet the basic needs of use but still not perfect. The system’s KVO callbacks and auto-remove observers are layered with the registration logic, and custom KVO will use block callbacks and auto-release to optimize this

Create a new NSObject+FXKVO classification, open register observer method

-(void)fx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(FXKVOBlock)block;

1. Register observers

  1. judgeCurrent value keypathExists /setter method exists

The original idea is to determine whether a property exists. Although the parent class’s properties do not affect the child class, properties in the class do not have setter methods, but are added to the propertiList — eventually to determine setter methods

if (keyPath == nil || keyPath.length == 0) return;
// if (! [self isContainProperty:keyPath]) return;
if(! [self isContainSetterMethodFromKeyPath:keyPath]) return;

// Check whether the attribute exists
- (BOOL)isContainProperty:(NSString *)keyPath {
    unsigned int number;
    objc_property_t *propertiList = class_copyPropertyList([self class], &number);
    for (unsigned int i = 0; i < number; i++) {
        const char *propertyName = property_getName(propertiList[i]);
        NSString *propertyString = [NSString stringWithUTF8String:propertyName];
        
        if ([keyPath isEqualToString:propertyString]) return YES;
    }
    free(propertiList);
    return NO;
}

/// determine the setter method
- (BOOL)isContainSetterMethodFromKeyPath:(NSString *)keyPath {
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if(! setterMethod) {NSLog(@" No setter method for this property %@", keyPath);
        return NO;
    }
    return YES;
}
Copy the code
  1. Judging the properties of observationautomaticallyNotifiesObserversForKeyThe Boolean value returned by the
BOOL isAutomatically = [self fx_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
if(! isAutomatically)return;

// Call class methods dynamically
- (BOOL)fx_performSelectorWithMethodName:(NSString *)methodName keyPath:(id)keyPath {

    if ([[self class] respondsToSelector:NSSelectorFromString(methodName)]) {

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        BOOL i = [[self class] performSelector:NSSelectorFromString(methodName) withObject:keyPath];
        return i;
#pragma clang diagnostic pop
    }
    return NO;
}
Copy the code
  1. Dynamically generate subclasses, addclassMethod refers to the original class
// Dynamically generate subclasses
Class newClass = [self createChildClassWithKeyPath:keyPath];

- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@ % @ % @ "", kFXKVOPrefix, oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // Prevent repeated creation of new classes
    if (newClass) return newClass;
    
    / / application class
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    / / registered classes
    objc_registerClassPair(newClass);
    // Class points to FXPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)fx_class, classTypes);
    
    return newClass;
}
Copy the code
  1. Isa redirect – makes the object’sisaThe value points to a dynamic subclass
object_setClass(self, newClass);
Copy the code
  1. Save the information

Because multiple attribute values may be observed, they are stored in an array in the form of attribute values-model

typedef void(^FXKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface FXKVOInfo : NSObject
@property (nonatomic.weak) NSObject *observer;
@property (nonatomic.copy) NSString *keyPath;
@property (nonatomic.copy) FXKVOBlock handleBlock;
@end

@implementation FXKVOInfo

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

// Save the information
FXKVOInfo *info = [[FXKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
if(! mArray) { mArray = [NSMutableArray arrayWithCapacity:1];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
Copy the code

2. Add setter methods and call back

Add setter methods to dynamic subclasses

- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    ...
    / / add a setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)fx_setter, setterTypes);
    
    return newClass;
}
Copy the code

A concrete implementation of setter methods

static void fx_setter(id self,SEL _cmd,id newValue) {
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    
    // Change the value of the parent class - can be cast
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),}; lg_msgSendSuper(&superStruct,_cmd,newValue);// Information data callback
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
    
    for (FXKVOInfo *info in mArray) {
        if([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

3. Destroy the observer

Add the dealloc method to the dynamic subclass

- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    ...
    / / add dealloc
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)fx_dealloc, deallocTypes);
    
    return newClass;
}
Copy the code

Because when the page is freed, the holding object is called dealloc when the object is freed, adding an implementation to the dynamic subclass’s dealloc method name refers isa back so that it doesn’t go to the parent class for the method implementation when the page is freed

static void fx_dealloc(id self, SEL _cmd) {
    Class superClass = [self class];
    object_setClass(self, superClass);
}
Copy the code

But that’s not enough, just refer isa back, but the object doesn’t call the real dealloc method, the object doesn’t get released

For this reason, follow the iOS Discovery Runtime interview question analysis to exchange a wave of actions

  • Remove the base classNSObjectThe dealloc implementation withfx_deallocExchange methods
  • Isa means to go back and continue calling the realdeallocTo release
  • Reason for absence+loadMethod, because it is inefficient, and because it affects all classes
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    ...
    / / add dealloc
// SEL deallocSEL = NSSelectorFromString(@"dealloc");
// Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
// const char *deallocTypes = method_getTypeEncoding(deallocMethod);
// class_addMethod(newClass, deallocSEL, (IMP)fx_dealloc, deallocTypes);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self FXMethodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(fx_dealloc)];
    });
    
    return newClass;
}

- (void)fx_dealloc {
    Class superClass = [self class];
    object_setClass(self, superClass);
    [self fx_dealloc];
}
Copy the code

So I’m going to customize the KVO and I’m going to compose the KVO trilogy in block form in one step

Write in the back

This article is demo, SJKVOController and FBKVO written by J_Knight_ (I suggest taking a look at this mature custom KVO)

Recently saw a boiling point on the Nuggets – “a lot of people understand the principle, but not when it comes to actually coding”

Learning is like stepping on a pit to climb a pit, some pit seen others step on, you don’t know what is going on if you don’t try. You may be scratching your head and confused, but if you don’t solve the problem, it will always stand in the way of your growth

You have to quietly top, and then surprise everyone 🌺—————— with you