An overview of the

On the implementation of KVC/KVO principle, there are a lot of related articles on the Internet, but most of them are abstract and difficult to truly understand, we directly below the code to field discussion.

Demo code address: https://github.com/Assuner-Lee/KVC-KVO-Test.git

KVC demo code

ASClassA.h
#import <Foundation/Foundation.h>

@interface ASModel : NSObject

@property (nonatomic, strong) NSString *_modelString;

@end

@interface ASClassA : NSObject

@property (nonatomic, strong) NSString *stringA;

@property (nonatomic, strong) ASModel *modelA;

@end
Copy the code
ASClassA.m
#import "ASClassA.h"

@implementation ASModel

- (void)set_modelString:(NSString *)_modelString {
    __modelString = _modelString;
    NSLog(@"Execute setter _modelString");
}

- (void)setModelString:(NSString *)modelString {
    NSLog(@"Execute setter modelString");
}

- (void)setNoExist1:(NSString *)noExist {
    NSLog(@"Do setter noExist1");
}

@end


@implementation ASClassA

- (void)setStringA:(NSString *)stringA {
     NSLog(@"Execute setter stringA");
    _stringA = stringA;

- (instancetype)init {
    if (self = [super init]) {
        self.modelA = [[ASModel alloc] init];
    }
    return self;
}

@end

Copy the code
main.m
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "ASClassA.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ASClassA *objectA = [[ASClassA alloc] init];
        objectA.stringA = @"stringA setter"; / / setter (1) [objectAsetValue:@"stringA KVC" forKey:@"stringA"]; / / KVC (2) [objectAsetValue:@"_stringA KVC" forKey:@"_stringA"];       // kvc _
        
        NSLog(@"ObjectA. StringA value: %@", objectA.stringA);
        
        NSLog(@"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --"); (3) [objectAsetValue:@"_modelString kvc" forKeyPath:@"modelA._modelString"]; / / setter (4) [objectAsetValue:@"modelString kvc" forKeyPath:@"modelA.modelString"]; // KVC does not exist ⑤ [objectAsetValue:@"__modelString kvc" forKeyPath:@"modelA.__modelString"]; / / KVC _ 6 [objectAsetValue:@"noExist1" forKeyPath:@"modelA.noExist1"]; // KVC nonexistent property NSLog(@"ObjectA. ModelA. _modelString value: % @", objectA.modelA._modelString);
        
        NSLog(@"-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
        
       ⑦ NSString *s1 = [objectA valueForKeyPath:@"modelA._modelString"];
       ⑧ NSString *s2 = [objectA valueForKeyPath:@"modelA.modelString"];
       ⑨ NSString *s3 = [objectA valueForKeyPath:@"modelA.__modelString"];
}
    return 0;
}

Copy the code

The results

①->⑨ All executions are successful. Where ①③④⑥ executes setter methods, ⑦⑧ executes getter methods, ②⑤⑨ directly accesses instance variables.

why

Actually, if you go to valueForKey or setValueForKey, the help documentation is pretty clear

valueForKey:
The default implementation of this method does the following:
    1. Searches the class of the receiver for an accessor method whose name matches the pattern -get<Key>, -<key>, or -is<Key>, in that order. If such a method is found it is invoked. If the type of the method's result is an object pointer type the result is simply returned. If the type of the result is one of the scalar types supported by NSNumber conversion is done and an NSNumber is returned. Otherwise, Conversion is done and an NSValue is returned (New in Mac OS 10.5: results of arbitrary type are converted to NSValues, not just NSPoint, NRange, NSRect, 2 (introduced in Mac OS 10.7). Otherwise (no simple accessor method is found), searches the class of the receiver for methods whose names match the patterns -countOf
      
        and -indexIn
       
        OfObject: and -objectIn
        
         AtIndex: (corresponding to the primitive methods defined by the NSOrderedSet class) and also -
         
          AtIndexes: (corresponding to -[NSOrderedSet objectsAtIndexes:]). If a count method and an indexOf method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSOrderedSet methods is returned. Each NSOrderedSet message sent to the collection proxy object will result in some combination of -countOf
          
           , -indexIn
           
            OfObject:, -objectIn
            
             AtIndex:, and -
             
              AtIndexes: messages being sent to the original receiver of -valueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -get
              
               :range: that method will be used when appropriate for best performance. 3. Otherwise (no simple accessor method or set of ordered set access methods is found), searches the class of the receiver for methods whose names match the patterns -countOf
               
                 and -objectIn
                
                 AtIndex: (Corresponding to the Primitive methods defined by the NSArray class) and (introduced in Mac OS 10.4) also -
                 
                  AtIndexes: (corresponding to -[NSArray objectsAtIndexes:]). If a count method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSArray methods is returned. Each NSArray message sent to the collection proxy object will result in some combination of -countOf
                  
                   , -objectIn
                   
                    AtIndex:, and -
                    
                     AtIndexes: messages being sent to the original receiver of -valueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -get
                     
                      :range: That method will be used when appropriate for best performance. 4 (introduced in Mac OS 10.4). Otherwise (no simple accessor method or set of ordered set or array access methods is found), searches the class of the receiver for a threesome of methods whose names match the patterns -countOf
                      
                       , -enumeratorOf
                       
                        , and -memberOf
                        
                         : (corresponding to the primitive methods defined by the NSSet class). If all three such methods are found a collection proxy object that responds to all NSSet methods is returned. Each NSSet message sent to the collection proxy object will result in some combination of -countOf
                         
                          , -enumeratorOf
                          
                           , and -memberOf
                           
                            : messages being sent to the original receiver of -valueForKey:. 5. Otherwise (no simple accessor method or set of collection access methods is found), if the receiver'
                           
                          
                         
                        
                       
                      
                     
                    
                   
                  
                 
                
               
              
             
            
           
          
         
        
       
      s class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _
      
       , _is
       
        , 
        
         , or is
         
          , in that order. If such an instance variable is found, the value of the instance variable in the receiver is returned, with the same sort of conversion to NSNumber or NSValue as in step 1. 6. Otherwise (no simple accessor method, set of collection access methods, or instance variable is found), invokes -valueForUndefinedKey: and returns the result. The default implementation of -valueForUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.
         
        
       
      Copy the code

In short:

Accessor matching: first look for methods with the same name as key, isKey, getKey (and _key), and return the object type.

2. Instance variable matching: Find instance variables with the same name as key, _key, isKey, _isKey

setValueForKey:
The default implementation of this method does the following:
    1. Searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found the type of its parameter is checked. If the parameter type is not an object pointer type but the value is nil -setNilValueForKey: is invoked. The default implementation of -setNilValueForKey: raises an NSInvalidArgumentException, but you can override it in your application. Otherwise, if the type of the method's parameter is an object pointer type the method is simply invoked with the value as the argument. If the type of the method's parameter is some other type the inverse of the NSNumber/NSValue conversion done by -valueForKey: is performed before the method is invoked.
    2. Otherwise (no accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found and its type is an object pointer type the value is retained and the result is set in the instance variable, after the instance variable's old value is first released. If the instance variable's type is some other type its value is set after the same sort of conversion from NSNumber or NSValue as in step 1.
    3. Otherwise (no accessor method or instance variable is found), invokes -setValue:forUndefinedKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.
Copy the code

In short:

Accessor matching: look for a method with the same name as setKey, and the argument must be an object type

2. Instance variable matching: Find instance variables with the same name as key, _isKey, _key, and isKey, and directly assign values.

other

When we use id objectA = objectb.value2; Does that mean objectB has a value2 property? Well, not necessarily. The “.” operation is just looking for a method that matches the name and the parameter, and we’re used to referring to properties just because they happen to have getters, setters, The essence of an attribute is an instance variable plus an access method (some instance variables have no access methods, and some access methods have no corresponding instance variables)… For example, Object. class, NSObject has no class property, only a class method. KVC is an efficient way to get a set value regardless of whether the key is exposed or not.


KVO demo code

ASClassA.h

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface ASClassA : NSObject

@property (nonatomic, assign) NSUInteger value;

@property (nonatomic, assign) IMP imp;

@property (nonatomic, assign) IMP classImp;

@end
Copy the code

ASClassA.m

#import "ASClassA.h"

@implementation ASClassA

- (void)setValue:(NSUInteger)value {
    _value = value;
}

- (IMP)imp {
    return [self methodForSelector:@selector(setValue:)];
}

- (IMP)classImp {
    return [self methodForSelector:@selector(class)];
}

@end
Copy the code

ASClassB.h

#import <Foundation/Foundation.h>

@interface ASClassB : NSObject

- (NSString *)classssss;

- (void)setClassssss;

@end
Copy the code

ASClassB.m

#import "ASClassB.h"

@implementation ASClassB

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    NSLog(@"B receives the change");
}

- (NSString *)classssss {
    return @"classssss";
}

- (void)setClassssss {
}

@end
Copy the code

ASClassC.h


#import <Foundation/Foundation.h>

@interface ASClassC : NSObject

@end
Copy the code

ASClassC.m


#import "ASClassC.h"

@implementation ASClassC

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    NSLog(@"C receives the change");
}

@end
Copy the code

main.m

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"

#import <Foundation/Foundation.h>
#import "ASClassA.h"
#import "ASClassB.h"
#import "ASClassC.h"

NSArray<NSString *> *getProperties(Class aClass) {
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList(aClass, &count);
    NSMutableArray *mArray = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        const char *cName = property_getName(property);
        NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
        [mArray addObject:name];
    }
    return mArray.copy;
}

NSArray<NSString *> *getIvars(Class aClass) {
    unsigned int count;
    Ivar *ivars = class_copyIvarList(aClass, &count);
    NSMutableArray *mArray = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *cName = ivar_getName(ivar);
        NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
        [mArray addObject:name];
    }
    return mArray.copy;
}

NSArray<NSString *> *getMethods(Class aClass) {
    unsigned int count;
    Method *methods = class_copyMethodList(aClass, &count);
    NSMutableArray *mArray = [NSMutableArray array];
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        SEL selector = method_getName(method);
        NSString *selectorName = NSStringFromSelector(selector);
        [mArray addObject:selectorName];
    }
    return mArray.copy;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ASClassA *objectA = [[ASClassA alloc] init];
        ASClassB *objectB = [[ASClassB alloc] init];
        ASClassC *objectC = [[ASClassC alloc] init];
        NSString *bbb = objectB.classssss;
        //objectB.classssss = @"";
        
        Class classA1 = object_getClass(objectA);
        Class classA1C = [objectA class]; // objectA.class;
        NSLog(@"before objectA: %@", classA1);
        NSArray *propertiesA1 = getProperties(classA1);
        NSArray *ivarsA1 = getIvars(classA1);
        NSArray *methodsA1 = getMethods(classA1);
        IMP setterA1IMP = objectA.imp;
        IMP classA1IMP = objectA.classImp;
        
           Class classB1 = object_getClass(objectB);
           NSLog(@"before objectA: %@", classB1);
           NSArray *propertiesB1 = getProperties(classB1);
           NSArray *ivarsB1 = getIvars(classB1);
           NSArray *methodsB1 = getMethods(classB1);
        
        [objectA addObserver:objectB forKeyPath:@"value" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
        [objectA addObserver:objectC forKeyPath:@"value" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
       
        Class classA2 = object_getClass(objectA);
        Class classA2C = [objectA class];
        BOOL isSame = [objectA isEqual:[objectA self]];
        id xxxx = [[classA2 alloc] init];
        NSLog(@"after objectA: %@", classA2);
        NSArray *propertiesA2 = getProperties(classA2);
        NSArray *ivarsA2 = getIvars(classA2);
        NSArray *methodsA2 = getMethods(classA2);
        IMP setterA2IMP = objectA.imp;
        IMP classA2IMP = objectA.classImp;
        
          Class classB2 = object_getClass(objectB);
          NSLog(@"before objectA: %@", classB2);
          NSArray *propertiesB2 = getProperties(classB2);
          NSArray *ivarsB2 = getIvars(classB2);
          NSArray *methodsB2 = getMethods(classB2);
        
             NSObject *object = [[NSObject alloc] init];
             NSArray *propertiesObj = getProperties([object class]);
             NSArray *methodsObj = getMethods([object class]);
             NSArray *ivarsObj = getIvars([object class]);
        
        BOOL isSameClass = [classA1 isEqual:classA2];
        BOOL isSubClass = [classA2 isSubclassOfClass:classA1];
  
        objectA.value = 10;
        [objectA removeObserver:objectB forKeyPath:@"value"];
        [objectA removeObserver:objectC forKeyPath:@"value"];
        
        NSNumber *integerNumber = [NSNumber numberWithInteger:1];
        Class integerNumberClass = object_getClass(integerNumber);
        NSNumber *boolNumber = [NSNumber numberWithBool:YES];
        Class boolNumberClass = object_getClass(boolNumber);
    }
    return 0;
}

#pragma clang diagnostic pop

Copy the code

The results

Analyze the above results

By fetching objectA class types, attribute lists, variable lists and method lists before and after being observed by objectB and objectC, we get:

(1) class: Before being observed, objectA was of type ASClassA. After being observed, objectA is of type NSKVONotifying_ASClassA, and this class isa subclass of ASClassA. Object_getClass (objectA) is equivalent to the objectA->isa method.

② Attribute, instance variable: no change.

NSKVONotifying_ASClassA has four new methods,

We can notice that observed value setValue: the implementation of the methods by [[ASClassA setValue:] at ASClassA. M) into (Foundation_NSSetUnsignedLongLongValueAndNotify). The overridden setter method inserts [self willChangeValueForKey: @” name “] before and after the original implementation; [super setValue:newName forKey:@ “name”]; [the self didChangeValueForKey: @ “name”); And so on to trigger a response from the observer. Then the class method changes from (libobjc.a.dylib -[NSObject class]) to (Foundation_NSKVOClass),

This also explains why the [objectA class] method is used before and after being observed. The implementation of the -(class)class method is object_getClass, but the class method and the objectA class result are different after being observed. The fact that the class method is overwritten, the class method always gets an ASClassA

The dealloc method changes the class back to ASClassA(via ISA) after the observation is removed. _isKVO: determines whether the observed is also observing other objects

In fact,

Apple Developer Documentation

In short, Apple uses an ISA swapping technique. When objectA is observed, the ISA pointer to the objectA object is pointed to a new subclass of ASClassA, NSKVONotifying_ASClassA, And this subclass overrides the observed setter and class methods, dealloc and _isKVO methods, and then makes the objectA object’s ISA pointer point to the new class, and objectA actually becomes an instance object of NSKVONotifying_ASClassA. The execution method is found in the class’s list of methods. (Apple also warns us that using ISA to get the type of a class is unreliable and that using the class method will always get the correct class.) .

For more information on OC objects, classes, ISAs, properties, variables, and methods please refer to my other blog postThe Runtime profile

thinking

Due to the limited research method, after does not know the value of the observed change, in what way to notify the observer, and make it perform implement corresponding method, we can guess, perhaps apple idiomatic, like maintenance of the object reference count, and the existence of a weak modified object, to build a hash table to correspond to the observer, The address of the observed or otherwise. I don’t know yet.

Thank you for watching

Welcome to point out the mistakes in the article!

Demo code address: https://github.com/Assuner-Lee/KVC-KVO-Test.git

Please correct any intellectual property rights, copyright issues or theoretical errors.

Please indicate the original author and above information.