Key-Value Coding

2020-02-11  本文已影响0人  Justin_S_Wang

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;
}
- (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;
}
- (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];
}

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;
}
- (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.

上一篇 下一篇

猜你喜欢

热点阅读