IOS martial arts esoteric article summary

Writing in the front

When it comes to KVC (key-value coding) and KVO (key-value observation), you probably use the yo-yo, but do you really understand it? This paper will analyze the principle of KVO in all directions

A possible secret Demo for this section

1. Preliminary study on KVO

Key-value Observing (KVO) is a set of event notification mechanism provided by Apple, which allows iOS developers to notify objects of specific property changes of other objects. IOS developers can use KVO to detect object property changes and quickly respond to them. This can help us a lot when developing strongly interactive, responsive applications and implementing bi-directional 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

In iOS daily development, KVO is often used to monitor the change of object properties and make timely response. That is, when the properties of the specified observed object are modified, KVO will automatically notify the corresponding observer. So what is the difference between KVO and NSNotificatioCenter?

  • The same
    • 1. The realization principle of both isObserver model, are used for listening in
    • 2, all canImplement one-to-manyThe operation of the
  • The difference between
    • 1.KVOCan only be used to listen for changes in object attributes, and attribute names are passedNSStringTo find, the compiler will not help you to check and complete, pure hand typing will be more prone to error
    • 2,NSNotificationSend listening (postWe can control the operation ofKVOControlled by system
    • 3,KVOCan recordThe old and new valueschange

Two, KVO use and attention points

①. 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 canSpecify the NULLAnd rely entirely on the key path string to determine the source of the change notification, but this approach may cause the object’s parent class to observe the same key path for different reasons, which may cause problems; 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.

The assumption here is that 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 tell 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

Generally speaking, context is mainly used to distinguish the properties of different objects with the same name. Therefore, context can be directly used in KVO callback methods to distinguish, which can greatly improve performance and code readability

Context uses summary:

  • 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

    / / defines the context
    static void *PersonNameContext = &PersonNameContext;
    static void *StudentNameContext = &StudentNameContext;
    // Register the observer
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
    [self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext];
    / / KVO callback
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
        if (context == PersonNameContext) {
            NSLog(@ "% @", change);
        } else if (context == StudentNameContext) {
            NSLog(@ "% @", change); }}Copy the code

③ Necessity of removing notification

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.student = [TCJStudent new];
    
    [self.student 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.student.name = @Sunflower Treasure Book;
}

Copy the code

But when theTCJStudentOnce created as a singleton, click on the screen in push toTCJDetailViewControllerClick on the screen and pop back to the previous page. Click on the screen again and the program crashes

This is because without removing the observation, the singleton still exists, and the next time you click on the screen, the wild pointer error will be reported

This does not happen when the observer is removedRemoving the observer is necessary

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

④ manually trigger key value 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 return to NO be observer automaticallyNotifiesObserversForKey (can only for a certain attribute set), automatic switch, return to the NO, we won’t be able to listen, returns YES, said listening in

    + (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

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

⑤. Observe one-to-many key values

One-to-many in KVO observation, which means you can listen for changes to multiple properties by registering a SINGLE KVO observer

For example, if there is a requirement of download task, the current downloadProgress can be obtained according to total download data and current download writtenData. This requirement can be realized in two ways:

  • Look at total downloads separatelytotalDataAnd current downloadswrittenDataTwo properties, one of which evaluates the current download progress when it changesdownloadProgress
  • implementationkeyPathsForValuesAffectingValueForKeyMethods and observationdownloadProgressattribute

KeyPaths =downloadProgress can be used for listening callbacks whenever the total number of downloads (totalData) or the current number of downloads (writtenData) changes

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

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

- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f".1.0f*self.writtenData/self.totalData];
}
Copy the code

⑥.KVO looks at mutable arrays

TCJPerson: there is a variable array dataArray under TCJPerson, now observe it, ask if the screen is printed?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [TCJPerson 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:@ "1"];
}

Copy the code

Answer: Will not analyze:

  • KVOIs based onKVCWhile mutable array direct addition is not calledSettermethods
  • An array variabledataArrayAn 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:@ "2"];

Copy the code

Three, KVO principle — ISa-Swizzling

①. Official explanation

The key-value Observing Programming Guide has a description of the underlying implementation principle

  • KVOIs the use ofisa-swizzlingTechnically realized
  • As the name suggests,isaPointers point 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 an observer is registered for an object’s properties, the observed object’sisaPointer to intermediate class instead of real class.isaThe value of a pointer does not necessarily reflect the actual class of the instance
  • You should never rely on itisaPointer to determine class membership. Instead, you should useclassMethod to determine the class of an object instance

②. Code exploration

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

  • Before registering an observer: the class object isTCJPerson, instance objectisaPoint to theTCJPerson

  • After registering the observer: the class object isTCJPerson, instance objectisaPoint to theNSKVONotifying_TCJPerson

One conclusion can be drawn from these two figures: the TCJPerson 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_TCJPerson and TCJPerson?

The print subclass method, discover, is called before and after the observer is registeredNSKVONotifying_TCJPersonisTCJPersonA subclass of

③ dynamic subclass exploration

You see what dynamic subclasses look at at first. Let’s look at the attribute variable nickName and the member variable name to find the difference

Both variables change at the same time, but onlyAttribute variablesListen for callbacks – indicates that dynamic subclasses observesettermethods

“Bluebeard’s bananas” by calling the Run-time API printed the methods of the dynamic subclass and the observation class

#pragmaMark - Traversal method -ivar-property
- (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

It can be seen from printing:

  • TCJPersonMethod in class does not change (IMP implementation address does not change)
  • NSKVONotifying_TCJPersonClass overrides the parent classTCJPersonthedeallocmethods
  • NSKVONotifying_TCJPersonClass overrides the base classNSObjecttheclassMethods and_isKVOAmethods
    • Rewrite theclassMethods can refer backTCJPersonclass
  • NSKVONotifying_TCJPersonClass overrides the parent classTCJPersonthesetNickNamemethods
    • Because subclasses only inherit, do not rewrite is not a method IMP, call method will ask the parent class to method implementation
    • And twosetNickNameThe address pointer is different
    • Every time you look at oneAttribute variablesI’ll just rewrite itsetterMethods (self-argumentation)

deallocafterisaPoint to who? — Refers to the original class

➍dealloc after dynamic subclasses are destroyed? –

The page pops and is pushed in again for printingTCJPersonClass, subclassNSKVONotifying_TCJPersonClass still exists

➎ automaticallyNotifiesObserversForKey will affect dynamic generate – can 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. Dynamic subclasses overwrite the observation propertysetterMethods,dealloc,class,_isKVOAmethods
    • setterThe key () method is used to observe key values
    • deallocMethod is used to release time pairsisaPoint to operate
    • 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 class of NSObject+TCJKVO, open register observer method

- (void)cj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(TCJKVOBlock)block;
Copy the code

①. Registered observer

1. Check whether the current observed value keypath exists or the 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

2. The judge observed attribute automaticallyNotifiesObserversForKey method returns the Boolean value

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

3. Dynamically generate subclasses and add a class method pointing 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:@ % @ % @ "", kTCJKVOPrefix, 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 refers to TCJPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)cj_class, classTypes);
    
    return newClass;
}

Copy the code

4. Isa repointing — Makes the value of an object’s ISA point to a dynamic subclass

object_setClass(self, newClass);
Copy the code

5. Save information Because multiple attribute values may be observed, it is stored in an array in the form of attribute values-model

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

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

@implementation TCJKVOInfo

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

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

Copy the code

②. 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)cj_setter, setterTypes);
    
    return newClass;
}

Copy the code

A concrete implementation of setter methods

static void cj_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 (*cj_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),}; cj_msgSendSuper(&superStruct,_cmd,newValue);// Information data callback
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey));
    
    for (FXKVOInfo *info in mArray) {
        if([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

③ 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)cj_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 cj_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

In this case, a wave of operations is performed based on the method exchange described in iOS ⑩: OC

  • Take the dealloc implementation of the base class NSObject and swap methods with CJ_dealloc
  • Isa means to go back and continue to call the real dealloc to release
  • You don’t swap in the +load method because it’s 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)cj_dealloc, deallocTypes);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self cj_methodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(cj_dealloc)];
    });
    
    return newClass;
}

- (void)cj_dealloc {
    Class superClass = [self class];
    object_setClass(self, superClass);
    [self cj_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

Study harmoniously without being impatient. I’m still me, a different color of fireworks.