iOS之KVC原理&自定義KVC

前言

開發過程中,很多人都會注意到KVO,以及自定義KVO,實際上KVC的作用也是十分強大的,不僅僅是簡單的字典轉模型,有關使用技巧可以看上篇文章,這篇文章要根據上篇的總結來進行自定義KVC操作;

相關代碼:KVCCode(上篇代碼也在這裏)

KVC原理

實際在自定義過程中主要要注意的2大點:1.KVC設置過程,2.KVC取值過程,

1.KVC賦值過程

1:非空判斷一下

2:找到相關方法set<Key>,_set<Key>,_setIs<Key>實例方法進行賦值

3:判斷是否能夠直接賦值實例變量判斷,即accessInstanceVariablesDirectly,且返回值爲YES;

        3.1:找相關實例變量進行賦值

             3.1.1 定義一個收集實例變量的可變數組

             3.1.2 獲取相應的 ivar

             3.1.3 對相應的 ivar 設置值

4.如果找不到相關實例setValue:forUndefinedKey報出異常

- (void)xz_setValue:(nullable id)value forKey:(NSString *)key{
    
    // 1:非空判斷一下
    if (key == nil  || key.length == 0) return;
    
    // 2:找到相關方法 set<Key> _set<Key> setIs<Key>
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self xz_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self xz_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self xz_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3:判斷是否能夠直接賦值實例變量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相關實例變量進行賦值
    // 4.1 定義一個收集實例變量的可變數組
    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 獲取相應的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 對相應的 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:如果找不到相關實例
    @throw [NSException exceptionWithName:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
#pragma mark - 方法分發
- (BOOL)xz_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;
}

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

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

 

2.KVC取值過程(ValueForKey:)

1.對key 判斷非空

2.找到相關方法getKey, key, isKey, _key,

3:判斷是否能夠直接賦值實例變量是否實現類方法accessInstanceVariablesDirectly

4..按照 _key,_iskey,key,isKey 順序查詢實例變量

5. 拋出異常ValueForUndefinedKey 報錯

- (nullable id)xz_valueForKey:(NSString *)key{
    
    // 1:刷選key 判斷非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相關方法 getKey, key, isKey, _key
    // key 要大寫
    NSString *Key = key.capitalizedString;
    NSString *getKey = [NSString stringWithFormat:@"get%@:",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@:",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@:",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(isKey)]){
        return [self performSelector:NSSelectorFromString(isKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(_key)]){
        return [self performSelector:NSSelectorFromString(_key)];
    }
#pragma clang diagnostic pop


    // 3:判斷是否能夠直接賦值實例變量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
     // 4.按照 _key,_iskey,key,isKey 順序查詢實例變量
    NSMutableArray *mArray = [self getIvarListName];
    _key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    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);;
    }
    
    // 5.拋出異常
    @throw [NSException exceptionWithName:@"XZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: valueForUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];

    return @"";
}

取值過程的自定義也結束了,其實這裏也有不嚴謹的地方,比如取得屬性值返回的時候需要根據屬性值類型來判斷是否要轉換成 NSNumber 或 NSValue,以及對 NSArray 和 NSSet 類型的判斷。在KVCCode(中有個寫的比較牛逼的KVC代碼,有興趣可以下載下來看看)

KVC異常小技巧

下面代碼會使用XZPerson類

NS_ASSUME_NONNULL_BEGIN

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

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

@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;

@end

NS_ASSUME_NONNULL_END

1: KVC 自動轉換類型

看下面代碼我們在給age賦值的時候一般情況會不能直接複製int類型,會使用下面方式

    XZPerson *person = [[XZPerson alloc] init];

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

    // 上面那個表達 大家應該都會! 但是下面這樣操作可以?
    [person setValue:@"20" forKey:@"age"]; // int - string
    NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber

看一下輸出結果:

上面使用@18 輸出的是__NSCFNumber(類簇,屬於NSNumber的子類) 是可以理解的,但是 @“20”也是__NSCFnumber ,這說明在賦值過程會進行對應的類型轉換

同樣的類型轉換,在結構體中也會出現

    
XZPerson *person = [[XZPerson alloc] init];
    
[person setValue:@"20" forKey:@"sex"];
    
NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFBoolean

 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

這裏的輸出過爲:

bool 類型會轉換爲__NSCFBoolean(NSCFNumber) ,結構體會轉換爲NSConcreteValue (NSValue)類型

2: 設置空值(setNilValueForKey實現方法進行容錯提示)

我們可以對age,sex進行設置空置

    
XZPerson *person = [[XZPerson alloc] init];
NSLog(@"******2: 設置空值******");

[person setValue:nil forKey:@"age"]; // subject不會走 - 官方註釋裏面說只對 NSNumber - NSValue

我們可以在person類中實現setNilValueForKey方法進行監控(注:如果沒有實現這個方法會導致崩潰),進行容錯提示

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"你傻不傻: 設置 %@ 是空值",key);
}

在setNilValueForKey方法的註釋文檔中描述如下,描述了,可以監控到NSNumber,和NSValue ,是監聽不到NSString類型的

/* 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.
*/

3: 插入找不到的 key(setValue: forUndefinedKey 進行容錯)

如果我們給person中插入一個不存在的屬性

    NSLog(@"******3: 找不到的 key******");
    [person setValue:nil forKey:@"Alan"];

這個時候如果直接運行就會報錯,找不到這個key,可以添加setValue: forUndefinedKey:進行容錯提示

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{

    NSLog(@"你瞎啊: %@ 沒有這個key",key);

}

 4: 取值時 - 找不到 key(valueForUndefinedKey:)

取值時去一個不包含的屬性,進行容錯處理

    // 4: 取值時 - 找不到 key
    NSLog(@"******4: 取值時 - 找不到 key******");
    NSLog(@"%@",[person valueForKey:@"Alan"]);

需要person類中添加方法

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"你瞎啊: %@ 沒有這個key - 給你一個其他的吧,別奔潰了!",key);
    return @"Master 牛逼";
}

5: 鍵值驗證

這個在開發中用的相對來說較少,主要是封裝一些庫,可能不想讓上層瞭解具體屬性是怎麼進行操作的,纔會有這種操作:具體如下:給person職工插入names屬性進行驗證,如過有錯誤就報錯,如果沒有錯誤,輸出names和subject值

    NSLog(@"******5: 鍵值驗證******");
    NSError *error;
    NSString *name = @"Alan";
    if (![person validateValue:&name forKey:@"names" error:&error]) {
        NSLog(@"%@",error);
    }else{
        NSLog(@"%@",[person valueForKey:@"subject"]);
    }
    if (![person validateValue:&name forKey:@"alan" error:&error]) {
        NSLog(@"%@",error);
    }else{
        NSLog(@"%@",[person valueForKey:@"subject"]);
    }

如果要實現重定向就需要在person類中實現:

代碼邏輯爲,如果傳入的key值爲names的話,就傳入的值進行修改拼接了字符,並且存儲到subject屬性中,如果不是names屬性就直接拋出錯誤

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"names"]){
        [self setValue:[NSString stringWithFormat:@"裏面修改一下: %@",*ioValue] forKey:@"subject"];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的屬性",inKey,self] code:10088 userInfo:nil];
    return NO;
}

輸出日誌信息:

 

總結

KVC 探索完了,其實我們探索的大部分內容都是基於蘋果的官方文檔,我們在探索 iOS 底層的時候,文檔思維十分重要,有時候說不定在文檔的某個角落裏就隱藏着追尋的答案。KVC 用起來不難,理解起來也不難,但是這不意味着我們可以輕視它。在 iOS 13 之前,我們可以通過 KVC 去獲取和設置系統的私有屬性,但從 iOS 13 之後,這種方式被禁用掉了。建議對 KVC 理解還不透徹的讀者去多幾遍官方文檔,相信我,你會有新的收穫。最後,我們簡單總結一下本文的內容。

  • KVC 是一種 NSKeyValueCoding 隱式協議所提供的機制。
  • KVC 通過 valueForKey:valueForKeyPath: 來取值,不考慮集合類型的話具體的取值過程如下:
    • get<Key>, <key>, is<Key>, _<key> 的順序查找方法
    • 如果找不到方法,則通過類方法 accessInstanceVariablesDirectly 判斷是否能讀取成員變量來返回屬性值
    • _<key>, _is<Key>, <key>, is<Key> 的順序查找成員變量
  • KVC 通過 setValueForKey:setValueForKeyPath: 來取值,不考慮集合類型的話具體的設置值過程如下:
    • set<Key>, _set<Key>的順序查找方法
    • 如果找不到方法,則通過類方法 accessInstanceVariablesDirectly 判斷是否能通過成員變量來返回設置值
    • _<key>, _is<Key>, <key>, is<Key> 的順序查找成員變量

5種異常處理

  •  KVC 自動轉換類型

  •  設置空值容錯

  • 插入找不到的 key容錯

  •  取值時 - 找不到 key容錯

  • 鍵值驗證重定向

希望對大家有用處,歡迎大家點贊+評論,關注我的CSDN,我會定期做一些技術分享!未完待續。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章