KVC底層原理--YYModel簡述

YYModel的作用就是字典轉模型,在瞭解YYModel前,我們先了解下KVC的知識。

KVC:也稱之鍵值編碼,是一種採用了NSKeyValueCoding協議的對象(直接或間接繼承NSObject時會爲基本方法提供默認實現)通過間接訪問其屬性的機制,也就是符合鍵值編碼,對象可通過字符串參數來簡單而統一的消息對其屬性進行尋址,例如valueForKey:setValue:forKey:。在一些特殊情況下,KVC還可以簡化代碼。官方文檔直通車

KVC底層原理

舉例:用鍵值編碼實現數據源方法

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}

或者我們常用的設置textFiled的placeholder顏色

[self.textFiled setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];

當調用協議的 getter(比如valueForKey:), 默認實現根據Accessor Search Patterns中描述的規則確定爲指定鍵提供值的特定訪問器方法或實例變量. 如果返回值不是對象,getter會使用這個值初始一個NSNumber對象或NSValue(對於結構體)對象替代。同樣setter(比如setValue:forKey:)通過特定鍵或訪問方法或實例變量時確定的數據類型,如果數據類型不是對象,setter首先會向傳入的值對象發送適當的 <type>Value 消息(intValue)提取基礎數據並存儲該數據。僅限Objective-C,因爲Swift的所有屬性都是對象。

自動包裝和解包不僅限於 NSPoint,NSRange,NSRect,和 NSSize. 也可以是 NSValue 對象。舉例:

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

使用KVC獲取myClass的threeFloats:默認調用threeFloats的getter,然後將返回值包裝在NSValue中返回。

NSValue* result = [myClass valueForKey:@"threeFloats"];

當然,我們也可以使用KVC來設置threeFloats的值

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
KVC取值和賦值的過程

首先,我們先根據官方文檔找到Accessor Search Patterns來查看根據輸入key的搜索流程。下面是官方文檔的部分說明,具體參考官方文檔

說的就是在getter的時候,先按照get<Key>, <key>, is<Key>, or _<key>的順序來查找,找到了就直接調用,然後判斷收到的屬性值是一個對象指針直接返回,該值是NSNumber支持的標量類型則用包裝NSNumber返回,否則包裝成 NSValue 對象返回。如果沒有找到則在實例中搜索countOf<Key>, objectIn<Key>AtIndex:(對應於NSArray基本方法)和<key>AtIndexes:(對應於NSArray的objectsAtIndexes),如果找到則創建一個響應所有NSArray方法的集合代理對象並返回,如果還是沒有找到則查找countOf<Key>, enumeratorOf<Key>和memberOf<Key>:(對應於 NSSet 類的基本方法),如果三個方法都找到,會創建一個響應所有NSSet方法的集合代理對象並返回該方法。如果還沒找到並且接收者對象的accessInstanceVariablesDirectly(是否開啓間接訪問)返回 YES,則會順序查找_<key>, _is<key>, <key>, is<key>,如果找到了則返回相應的值(對象指針、NSNumber或者NSValue),如果最後還是沒找到則會執行valueForUndefindKey:,默認拋出異常,NSObject子類可以重寫。

在setter的時候也是順序查找訪問方法名set<key>, _set<key>,找到了就則使用輸入值執行,沒找到且accessInstanceVariablesDirectly返回YES,則按照順序查找_<key>, _is<key>, <key>, is<key>,如果找到了就執行沒找到則執行setValue:forUndefinedKey:拋出未定義key的異常,同樣NSObject子類可以重寫。

當沒有對異常進行處理的話,不出意外會崩潰。

在賦值和取值過程中,當value爲空的時候,會執行setNilValueForKey,如果Key值不存在則執行setValue:forUndefinedKey
KVC也可以用於驗證key或key-path的方法,我們也可以爲屬性提供驗證方法。在響應validateValue:forKey:error:方法的時候,會查找valudate<Key>:error:是否實現,如果實現了則根據實現方法的自定義邏輯返回YES或者NO,如果沒實現則系統默認返回YES,NSError用來返回error信息。

KVC異常處理

那在實際運用的時候萬一出現意外,要怎麼規避呢?可以在NSObject的分類做下相應的處理(OC的對象幾乎都可以追溯到NSObject)。

// .h文件代碼
- (void)k_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)k_valueForKey:(NSString *)key;
// ------------------華麗的分割線----------------------
// .m文件代碼
- (void)k_setValue:(id)value forKey:(NSString *)key {
    NSError  *error;
    BOOL validate = [self validateValue:&value forKey:key error:&error];
    NSArray *arr = [self getIvarListName];
    if (validate) {
        if ([arr containsObject:key]) {
            [self setValue:value forKey:key];
        }else{
            NSLog(@"%@ 不存在變量",key);
        }
    }
}

- (nullable id)k_valueForKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        NSLog(@"key爲nil或者空值");
        return nil;
    }
    NSArray *arr = [self getIvarListName];
    if ([arr containsObject:key]) {
        return [self valueForKey:key];
    }
    NSLog(@"%@ 不存在變量",key);
    return nil;
}

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    if (*ioValue == nil || inKey == nil || inKey.length == 0) {
        NSLog(@"value 可能爲nil  或者key爲nil或者空值");
        return NO;
    }
    return YES;
}

- (NSMutableArray *)getIvarListName {
    NSMutableArray *mulArr = [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);
        [mulArr addObject:ivarName];
    }
    free(ivars);
    return mulArr;
}

記得添加#import <objc/runtime.h>,在調用k_setValue forKey或者k_valueForKey的時候會自動檢測value和key是否有效值,如果不是有效值會拋出

KVC使用
  • 字典的使用
NSDictionary* dict = @{
                           @"oneString":@"one",
                           @"num":@123,
                           @"list":@[@1, @2, @3]
                           };
    Person *p = [[Person alloc] init];
    // 字典轉模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"%@--%d---%@",p.oneString, p.num, p.list);
    // 鍵數組轉模型到字典
    NSArray *array = @[@"oneString",@"num"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);

打印結果如下:

one--123---(
    1,
    2,
    3
)

{
    num = 123;
    oneString = one;
}
  • KVC消息傳遞
    NSArray *array = @[@"One",@"Two",@"Three",@"HH"];
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"%@",lenStr);// 消息從array傳遞給了string
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"%@",lowStr);
    // 也支持聚合操作符
    int count = [[array valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"%d", count);

打印結果:

(
    3,
    3,
    5,
    2
)

(
    one,
    two,
    three,
    hh
)

 4
  • 補充
    當然了,聚合操作符還有很多,列舉一些可能用到的:
    @avg:通過right-key-path讀取集合中每個元素讀取屬性,並轉換爲double類型(nil用0代替),計算他們的平均值,然後返回NSNumber。
    @ sum:原理同 '@avg',返回所有元素的和。
    @ count:以NSNumber實例返回集合中所有對象的數量,如果有right-key-path忽略。
    @ max:通過right-key-path在集合中查找並返回最大的元素。查找會使用由Foundation類(例如 NSNumber) 定義的compare: 方法,所以right-key-path指定的屬性必須是一個對這個消息有意義的響應,查找忽略集合中的nil值。
    @min:原理同 '@max',返回最小的元素。
    下面的也可以對集合進行操作
    @distinctUnionOfObjects:返回對right-key-path指定屬性進行合併操作後的去重數組。
    @ unionOfObjects:和distinctUnionOfObjects行爲相似, 但是不會刪除重複對象。
    @ distinctUnionOfSets:和distinctUnionOfObjects行爲相似獲取交集。

鍵值模型--YYModel

字典轉模型的大概流程是分析對象,獲取到對象的所有ivar,並將ivar一一賦值。

首先,我們進入YYModel的字典轉模型入口yy_modelWithDictionarymodelWithJSON方法,部分代碼展示:

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {
    if (!dictionary || dictionary == (id)kCFNull) return nil;
    if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;
    
    Class cls = [self class];
    // 分析cls
    _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
    if (modelMeta->_hasCustomClassFromDictionary) {
        cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
    }
    
    NSObject *one = [cls new];
    if ([one yy_modelSetWithDictionary:dictionary]) return one;
    return nil;
}

metaWithClass:方法內部先用Core Foundation的CFMutableDictionaryRef進行緩存已經處理過的key-value,以備下次直接使用,並用dispatch_semaphore_t來確保字典的讀取安全。如果沒有該緩存或者不需要更新則進行創建_YYModelMeta對象,_YYModelMeta首先對黑名單和白名單進行處理,再次判斷modelContainerPropertyGenericClass是否需要對特殊屬性進行替換(按照不同的類型進行遍歷 ),然後開始遍歷自己和父類所有的屬性和方法,生成與數據源相對應的字典映射(具體可網上查找),接着處理_YYModelPropertyMeta,判斷是NSString、NSArray,一切準備好後開始執行yy_modelSetWithDictionary:方法響應CFArrayApplyFunction,再次執行ModelSetWithPropertyMetaArrayFunction,獲取到的value傳入ModelSetValueForProperty中,通過objc_msgSend發送meta->_setter來完成屬性值的設置。

switch (meta->_nsType) {
                case YYEncodingTypeNSString:
                case YYEncodingTypeNSMutableString: {
                    if ([value isKindOfClass:[NSString class]]) {
                        if (meta->_nsType == YYEncodingTypeNSString) {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
                        } else {
                            ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
                        }
                    } else if ([value isKindOfClass:[NSNumber class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSNumber *)value).stringValue :
                                                                       ((NSNumber *)value).stringValue.mutableCopy);
                    } else if ([value isKindOfClass:[NSData class]]) {
                        NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding];
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, string);
                    } else if ([value isKindOfClass:[NSURL class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSURL *)value).absoluteString :
                                                                       ((NSURL *)value).absoluteString.mutableCopy);
                    } else if ([value isKindOfClass:[NSAttributedString class]]) {
                        ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model,
                                                                       meta->_setter,
                                                                       (meta->_nsType == YYEncodingTypeNSString) ?
                                                                       ((NSAttributedString *)value).string :
                                                                       ((NSAttributedString *)value).string.mutableCopy);
                    }
                } break;

該文章爲記錄本人的學習路程,希望能夠幫助大家,知識共享,共同成長,共同進步!!!文章地址:https://www.jianshu.com/p/cce3a7b99c84

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