Key-Value Coding

Key-value coding is an easy-to-use tool for us in our programming life. one of the usages is to change between model and JSON which we use the third party libraries instead generally. But what's under the hood? Let's explore it.

First of all, let's have a look at the very easy demo and the model we will use:

  1. LGPerson:
// LGPerson.h
#import <Foundation/Foundation.h>
#import "LGStudent.h"

NS_ASSUME_NONNULL_BEGIN

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface LGPerson : NSObject{
   @public
   NSString *myName;
}

@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, strong) NSArray           *array;
@property (nonatomic, strong) NSMutableArray    *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic)         ThreeFloats       threeFloats;
@property (nonatomic, strong) LGStudent         *student;

@end

// LGPerson.m
NS_ASSUME_NONNULL_END

#import "LGPerson.h"

@implementation LGPerson

@end
  1. LGStudent:
// LGStudent.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGStudent : NSObject
@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, copy)   NSString          *subject;
@property (nonatomic, copy)   NSString          *nick;
@property (nonatomic, assign) int               age;
@property (nonatomic, assign) int               stature;
@property (nonatomic, strong) NSMutableArray    *penArr;
@end

NS_ASSUME_NONNULL_END

// LGStudent.m
#import "LGStudent.h"

@implementation LGStudent

@end

ViewController:

    LGPerson *person = [[LGPerson alloc] init];
    person.name      = @"Justin";

What's the essence of person.name = @"Ray"? Yes, it's the setter created by compiler during the compiling time (no setter created by yourself). So we can access the setter which stored in ro by llvm.

When we set a breakpoint on line 3. (with objc source code) We'll find the code will come here:

void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}

tatic inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

This is the truth the code come here, but who called this function which encapsulat all the possible setter? Let's check another old friend llvm. We can find some info here:

// CGObjcGNU.cpp
SetPropertyNonAtomicCopy.init(&CGM, "objc_setProperty_nonatomic_copy",
                                    VoidTy, IdTy, SelectorTy, IdTy, PtrDiffTy);

// CGObjcMac.cpp
llvm::FunctionCallee getOptimizedSetPropertyFn(bool atomic, bool copy) {
  CodeGen::CodeGenTypes &Types = CGM.getTypes();
  ASTContext &Ctx = CGM.getContext();
  // void objc_setProperty_atomic(id self, SEL _cmd,
  //                              id newValue, ptrdiff_t offset);
  // void objc_setProperty_nonatomic(id self, SEL _cmd,
  //                                 id newValue, ptrdiff_t offset);
  // void objc_setProperty_atomic_copy(id self, SEL _cmd,
  //                                   id newValue, ptrdiff_t offset);
  // void objc_setProperty_nonatomic_copy(id self, SEL _cmd,
  //                                      id newValue, ptrdiff_t offset);

  SmallVector<CanQualType,4> Params;
  CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
  CanQualType SelType = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
  Params.push_back(IdType);
  Params.push_back(SelType);
  Params.push_back(IdType);
  Params.push_back(Ctx.getPointerDiffType()->getCanonicalTypeUnqualified());
  llvm::FunctionType *FTy =
      Types.GetFunctionType(
        Types.arrangeBuiltinFunctionDeclaration(Ctx.VoidTy, Params));
  const char *name;
  if (atomic && copy)
    name = "objc_setProperty_atomic_copy";
  else if (atomic && !copy)
    name = "objc_setProperty_atomic";
  else if (!atomic && copy)
    name = "objc_setProperty_nonatomic_copy";
  else
    name = "objc_setProperty_nonatomic";

  return CGM.CreateRuntimeFunction(FTy, name);
}       

llvm::FunctionCallee GetOptimizedPropertySetFunction(bool atomic,
                                                     bool copy) override {
  return ObjCTypes.getOptimizedSetPropertyFn(atomic, copy);
}

void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
                                        const ObjCPropertyImplDecl *propImpl,
                                        llvm::Constant *AtomicHelperFn) {
           
           ...
   case PropertyImplStrategy::SetPropertyAndExpressionGet: {

    llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
    llvm::FunctionCallee setPropertyFn = nullptr;
    if (UseOptimizedSetter(CGM)) {
      // 10.8 and iOS 6.0 code and GC is off
      setOptimizedPropertyFn =
          CGM.getObjCRuntime().GetOptimizedPropertySetFunction(
              strategy.isAtomic(), strategy.isCopy());
      if (!setOptimizedPropertyFn) {
        CGM.ErrorUnsupported(propImpl, "Obj-C optimized setter - NYI");
        return;
      }
    }
    ...
    }                                     
                ...

}                              

The process would be like in this work flow:

  1. generateObjCSetterBody
  2. GetOptimizedPropertySetFunction
  3. getOptimizedSetPropertyFn
  4. CreateRuntimeFunction(FTy, "objc_setProperty_nonatomic_copy")
  5. objc_setProperty_nonatomic_copy
  6. reallySetProperty

OK, that's the method to set name through setter. We can also do it like this:

[person setValue:@"Justin_1" forKey:@"name"];

So what's the theory of this piece of code? Exploring through assembly is a tough job. Let's make it easy to use the Official Archieve File.

Hello, KVC

Acorrding to the file, We know that Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties.

Basic visit

When we want to set the corner radius and some properties of layer, we can set them through keyPath in Storyboard like this;

img_1.png

Collection visit

Knowing that the KVO cann't observer the mutable collection, we will ask the KVC for help. See this:

person.array = @[@"1",@"2",@"3"];

what can I do to change the first element in the array. You might want to do it like this:

person.array[0] = @"100";

Then you will get the error:

Expected method to write array element not found on object of type 'NSArray *'

The right way is do it like this:

NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);

Another way to do this is like:

NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);

You will get the result:

(
    100,
    2,
    3
)

(
    200,
    2,
    3
)

Collection Operators

  1. Dictionary:
    NSDictionary* dict = @{
                           @"name":@"Justin",
                           @"nick":@"J",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"stature":@180
                           };
    LGStudent *p = [[LGStudent alloc] init];
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"%@",p);
    NSArray *array = @[@"name",@"age"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);

Result:

<LGStudent: 0x600002e3b930>
{
    age = 18;
    name = Justin;
}
  1. Array:
    NSArray *array = @[@"Ray",@"Justin",@"Jack",@"Peter"];
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"%@",lenStr);
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"%@",lowStr);

Result:

(
    3,
    6,
    4,
    5
)
(
    ray,
    justin,
    jack,
    peter
)
  1. Aggregation Operator:
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent new];
        NSDictionary* dict = @{
                               @"name": @"Tom",
                               @"age": @(18+i),
                               @"nick": @"Cat",
                               @"stature": @(175 + 2 * arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"stature"]);
    
    float avg = [[personArray valueForKeyPath:@"@avg.stature"] floatValue];
    NSLog(@"%f", avg);
    
    int count = [[personArray valueForKeyPath:@"@count.stature"] intValue];
    NSLog(@"%d", count);
    
    int sum = [[personArray valueForKeyPath:@"@sum.stature"] intValue];
    NSLog(@"%d", sum);
    
    int max = [[personArray valueForKeyPath:@"@max.stature"] intValue];
    NSLog(@"%d", max);
    
    int min = [[personArray valueForKeyPath:@"@min.stature"] intValue];
    NSLog(@"%d", min);

Result:

 178.333328
 6
 1070
 181
 175
  1. Array Operator:
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"stature":@(175 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"stature"]);
    NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.stature"];
    NSLog(@"arr1 = %@", arr1);
    NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.stature"];
    NSLog(@"arr2 = %@", arr2);

Result:

(
    181,
    183,
    181,
    181,
    179,
    179
)
arr1 = (
    181,
    183,
    181,
    181,
    179,
    179
)
arr2 = (
    181,
    183,
    179
)
  1. Array Nesting:
    NSMutableArray *personArray1 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *student = [LGStudent new];
        NSDictionary* dict = @{
                               @"name": @"Tom",
                               @"age": @(18+i),
                               @"nick": @"Cat",
                               @"stature": @(175 + 2*arc4random_uniform(6)),
                               };
        [student setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:student];
    }
    
    NSMutableArray *personArray2 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name": @"Tom",
                               @"age": @(18+i),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray2 addObject:person];
    }
    
    NSArray* nestArr = @[personArray1, personArray2];
    
    NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.name"];
    NSLog(@"arr = %@", arr);
    
    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.age"];
    NSLog(@"arr1 = %@", arr1);

Result is:

arr = (
    Tom
)
arr1 = (
    18,
    19,
    20,
    21,
    22,
    23,
    18,
    19,
    20,
    21,
    22,
    23
)
  1. Nest:
    NSMutableSet *personSet1 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LGStudent *person = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18 + i),
                               @"nick":@"Cat",
                               @"stature":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personSet1 addObject:person];
    }
    NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"age"]);
    
    NSMutableSet *personSet2 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18 + i * 2),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personSet2 addObject:person];
    }
    NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"age"]);

    NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];

    NSSet* set1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.age"];
    NSLog(@"set1 = %@", set1);
    NSLog(@"arr1 = %@", [set1 allObjects]);

Result:

personSet1 = {(
    21,
    20,
    23,
    19,
    22,
    18
)}
personSet2 = {(
    28,
    24,
    20,
    26,
    22,
    18
)}
set1 = {(
    26,
    22,
    18,
    23,
    19,
    28,
    24,
    20,
    21
)}
arr1 = (
    26,
    22,
    18,
    23,
    19,
    28,
    24,
    20,
    21
)

Non-Object Type

How to set ThreeFloats ? Maybe you will try this:

ThreeFloats floats = {1., 2., 3.};
[person setValue:floats forKey:@"threeFloats"];

and get the error:

Sending 'ThreeFloats' to parameter of incompatible type 'id _Nullable'

We should do like this:

    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    
    ThreeFloats th;
    [reslut getValue:&th];
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);

Result:

{length = 12, bytes = 0x0000803f0000004000004040}
1.000000 - 2.000000 - 3.000000

Object Nesting

    LGStudent *student = [[LGStudent alloc] init];
    student.subject    = @"iOS";
    person.student     = student;
    [person setValue:@"KVC theory" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

Result:

KVC theory

setValue:forKey: in KVC

Prepare code:

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

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    @public
     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}
@end

NS_ASSUME_NONNULL_END

// LGPerson.m
#import "LGPerson.h"

@implementation LGPerson

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

in ViewController:

    LGPerson *person = [[LGPerson alloc] init];
    [person setValue:@"Justin" forKey:@"name"];

According to the official file, we know the process is like this:

  1. Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.

    Run the code and get the result:

    -[LGPerson setName:] - Justin
    

    comment this piece of code:

    //- (void)setName:(NSString *)name{
    //    NSLog(@"%s - %@",__func__,name);
    //}
    

    Run the code:

    -[LGPerson _setName:] - Justin
    

    comment this piece of code:

    //- (void)_setName:(NSString *)name{
    //    NSLog(@"%s - %@",__func__,name);
    //}
    

    Result:

    -[LGPerson setIsName:] - Justin
    

    comment this piece of code:

    //- (void)setIsName:(NSString *)name{
    //    NSLog(@"%s - %@",__func__,name);
    //}
    
  2. If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.

    So let's try this first:

    + (BOOL)accessInstanceVariablesDirectly{
        return NO;
    }
    

    Result is:

    *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x600003238dc0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'
    

    Oops! Change it back to YES and add code in ViewController:

        NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
    

    Run the code and result is:

    Justin-(null)-(null)-(null)
    

    comment the _name and change code in ViewController to:

    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
    

    Check the result:

    Justin-(null)-(null)
    

    comment the _isName and change code in ViewController to:

    NSLog(@"%@-%@",person->name,person->isName);
    

    Result:

    Justin-(null)
    

    comment the name and change code in ViewController to:

    NSLog(@"%@",person->isName);
    

    result is:

    Justin
    
  3. Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

    comment the isName, you will get the error:

    *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x6000037371e0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'
    

The design makes the KVC so great to fault-tolerant.

getValueForKey: in KVC

After setting the value to the member variable, we are going to get them. So first prepare the value for them:

     person->_name = @"_name";
     person->_isName = @"_isName";
     person->name = @"name";
     person->isName = @"isName";

Of course you should also uncomment the variable:

@interface LGPerson : NSObject{
    @public
     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

Then let's go through with the process from the official file in:

  1. Search the instance for the first accessor method found with a name like get<Key>, <key>,, is<Key>, or _<key>, in that order.

    Run code and check the result:

    get value:getName
    

    Comment:

    //- (NSString *)getName{
    //    return NSStringFromSelector(_cmd);
    //}
    

    Run:

    get value:name
    

    Comment:

    //- (NSString *)name{
    //    return NSStringFromSelector(_cmd);
    //}
    

    Run:

    get value:isName
    

    Comment:

    //- (NSString *)isName{
    //    return NSStringFromSelector(_cmd);
    //}
    

    Run:

    get value:_name
    

    Comment:

    //- (NSString *)_name{
    //    return NSStringFromSelector(_cmd);
    //}
    
  2. If no simple accessor method is found, search the instance for methods whose names match the patterns countOf and objectInAtIndex: (corresponding to the primitive methods defined by the NSArray class) and AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).

    If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.

    The proxy object subsequently converts any NSArray messages it receives to some combination of countOf, objectInAtIndex:, and AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.

    add these two methods in LGPerson.m

    // count
    - (NSUInteger)countOfPens{
        NSLog(@"%s",__func__);
        return [self.arr count];
    }
    
    // item
    - (id)objectInPensAtIndex:(NSUInteger)index {
        NSLog(@"%s",__func__);
        return [NSString stringWithFormat:@"pens %lu", index];
    }
    

    in ViewController.m:

    NSLog(@"get value:%@",[person valueForKey:@"pens"]);
    

    You will get the result;

    get value:(
    )
    

    we can also test like this:

        person.arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
        NSArray *array = [person valueForKey:@"pens"];
        NSLog(@"%@",[array objectAtIndex:1]);
        NSLog(@"%d",[array containsObject:@"pen1"]);
    

    Result:

    -[LGPerson objectInPensAtIndex:]
    pens 1
    -[LGPerson countOfPens]
    -[LGPerson countOfPens]
    -[LGPerson objectInPensAtIndex:]
    -[LGPerson objectInPensAtIndex:]
    -[LGPerson objectInPensAtIndex:]
    -[LGPerson objectInPensAtIndex:]
    0
    
  3. If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf, enumeratorOf, and memberOf: (corresponding to the primitive methods defined by the NSSet class).

    If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.

    This proxy object subsequently converts any NSSet message it receives into some combination of countOf, enumeratorOf, and memberOf: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.

    comment the above methods

    //- (NSUInteger)countOfPens{
    //    NSLog(@"%s",__func__);
    //    return [self.arr count];
    //}
    
    //- (id)objectInPensAtIndex:(NSUInteger)index {
    //    NSLog(@"%s",__func__);
    //    return [NSString stringWithFormat:@"pens %lu", index];
    //}
    

    add the following code:

    - (NSUInteger)countOfBooks{
        NSLog(@"%s",__func__);
        return [self.set count];
    }
    
    - (id)memberOfBooks:(id)object {
        NSLog(@"%s",__func__);
        return [self.set containsObject:object] ? object : nil;
    }
    
    - (id)enumeratorOfBooks {
        // objectEnumerator
        NSLog(@"here enumerate");
        return [self.arr reverseObjectEnumerator];
    }
    

    in ViewController.m

    NSLog(@"get value:%@",[person valueForKey:@"books"]);
    

    get the result:

    get value:{(
    )}
    

    you can also try this:

    person.set = [NSSet setWithArray:person.arr];
    NSSet *set = [person valueForKey:@"books"];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSLog(@"set enumerate %@",obj);
    }];
    

    you'll get the result:

    2020-02-11 13:53:52.208905+0800 002-KVC取值&赋值过程[3488:199892] -[LGPerson countOfBooks]
    2020-02-11 13:53:52.209016+0800 002-KVC取值&赋值过程[3488:199892] -[LGPerson countOfBooks]
    2020-02-11 13:53:52.209110+0800 002-KVC取值&赋值过程[3488:199892] here enumerate
    2020-02-11 13:53:52.209229+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen3
    2020-02-11 13:53:52.209342+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen2
    2020-02-11 13:53:52.209550+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen1
    2020-02-11 13:53:52.209839+0800 002-KVC取值&赋值过程[3488:199892] set遍历 pen0
    
  4. If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<Key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.

    Still ensure accessInstanceVariablesDirectly is YES:

    + (BOOL)accessInstanceVariablesDirectly{
        return YES;
    }
    

    Run the code:

    get value:_name
    

    comment the:

    //     NSString *_name;
    and 
    //     person->_name = @"_name";
    

    Run:

    get value:_isName
    

    comment the:

    //     NSString *_isName;
    and
    //     person->_isName = @"_isName";
    

    Run:

    get value:name
    

    comment the:

    //     NSString *name;
    and
    //     person->name = @"name";
    

    Run:

    get value:isName
    

    Comment the:

    //     NSString *isName;
    and 
    //     person->isName = @"isName";
    
  5. If the retrieved property value is an object pointer, simply return the result. If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that. If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.

    Here is the LGStudent

    // LGStudent.h
    #import <Foundation/Foundation.h>
    typedef struct {
        float x, y, z;
    } ThreeFloats;
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGStudent : NSObject{
        int age;
        ThreeFloats threeFloats;
    }
    
    @end
      
    NS_ASSUME_NONNULL_END
      
    // LGStudent.m
    @implementation LGStudent
    @end
    

    in ViewController.m

            LGStudent *student = [[LGStudent alloc] init];
        student->age = 18;
        ThreeFloats th = {1.f, 2.f, 3.f};
        student->threeFloats = th;
      
        id retAge = [student valueForKey:@"age"];
        id retThreeFloats = [student valueForKey:@"threeFloats"];
        NSLog(@"get value:%@",retAge);
        NSLog(@"get value:%@",retThreeFloats);
    

    get the result:

    get value:18
    get value:{length = 12, bytes = 0x0000803f0000004000004040}
    
    img_2.png
  1. If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

    from the result of 4, run the code, you will get

    *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x6000029ccb10> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
    

Customize KVC

Now we have learned the base process of the KVC, let's do it by ourselves (easy one) !

First of all, we should check the entry:

@interface NSObject(NSKeyValueCoding)

So here is the hint we can write our code in the category of NSObject, it's a good idea to decoupling (like you can do that of AppDelegate). Let's create a category:

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

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (LGKVC)

// custom KVC entry
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;

- (nullable id)lg_valueForKey:(NSString *)key;
@end

NS_ASSUME_NONNULL_END
// .m
#import "NSObject+LGKVC.h"
#import <objc/runtime.h>

@implementation NSObject (LGKVC)

- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
  
}

- (nullable id)lg_valueForKey:(NSString *)key{
  
}

then let's make a todo list for set:

  1. check the key is nil (return) or not.
  2. check the setter.
  3. check the accessInstanceVariablesDirectly - NO (throw exception)
  4. check whether the variable in the ivar list.
  5. set the variable (setIvar) if has otherwise throw exception.

Let's write the code:

- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}
  • put all the setter together
- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}
  • get iVar list.
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    
    // 1: check the nil
    if (key == nil  || key.length == 0) return;
    
    // 2: check the methods set<Key> _set<Key> setIs<Key>
    // key need Capitalized
    NSString *Key = key.capitalizedString;
    // connect the string
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3:check can set variable directly
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.get the variable to set
    // 4.1 get ivar list
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 get the corresponding ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 set value to ivar
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }

    // 5: ivars not found
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
  • The custom set value for key method.

let's make a todo list for getter:

  1. check the key nil (return) or not.
  2. check the getter (array, currently ignore the set for convenience).
  3. check the accessInstanceVariablesDirectly - NO (throw exception)
  4. check whether the variable in the ivar list.
  5. Return the variable (setIvar) if has otherwise return "".

Let's write the code:

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}
  • for getter.
- (nullable id)lg_valueForKey:(NSString *)key{
    
    // 1:check key nil or not
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:fins associate methods get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key need capitalized
    NSString *Key = key.capitalizedString;
    // connect the string
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 3:whether achieve the variable directly
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.return the ivar
    // 4.1 get the ivar list
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}

This is the base version of the KVC. There's a version quite detail which is written by other code, the project name is DIS_KVC_KVO. We can learn the thoughts there.

Conclude and Tips

We know that we usually set the Int value to the type Int, but in KVC, we can also use NSString :

@property (nonatomic, assign) int  age;     

LGPerson *person = [[LGPerson alloc] init];
[person setValue:@18 forKey:@"age"];
[person setValue:@"20" forKey:@"age"]; // int - string
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber

But this time the type of valueForKey: is __NSCFNumber. KVC can auto change the type for you.

Bool is the same:

@property (nonatomic, assign) BOOL sex;

[person setValue:@"20" forKey:@"sex"];
    NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber

struct is like this:

typedef struct {
    float x, y, z;
} ThreeFloats;
@property (nonatomic) ThreeFloats  threeFloats;

ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue

What about the nil

    [person setValue:nil forKey:@"age"]; // only for NSNumber - NSValue
    [person setValue:nil forKey:@"subject"]; // will not get into the next line of code

with
  
- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"Are u kidding me: set %@ to nil?",key);
}

the second line will not get into the following part of code.

/* Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism. The default implementation of this method raises an NSInvalidArgumentException. You can override it to map nil values to something meaningful in the context of your application.
*/
- (void)setNilValueForKey:(NSString *)key;

No key setter:

[person setValue:nil forKey:@"HH"];
// will get into:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"what are u doing: %@ has not this key",key);
}

No key getter:

NSLog(@"%@",[person valueForKey:@"HH"]);
// will get into:
- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"Hey: %@ has not this key - give u another one!",key);
    return @"Master";
}

Validate:

NSError *error;
NSString *name = @"LG_Cooci";
if (![person validateValue:&name forKey:@"names" error:&error]) {
    NSLog(@"%@",error);
}else{
    NSLog(@"%@",[person valueForKey:@"name"]);
}
// will get into:
- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"name"]){
        [self setValue:[NSString stringWithFormat:@"we change it here: %@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ is not the property of %@",inKey,self] code:10088 userInfo:nil];
    return NO;
}

looks like we can redirect the value to some key here or fault-tolerant or forward. For more detail info you can check the official file.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容