iOS - 閑聊KVC

KVC

KVC(Key-Value-Coding)是Cocoa框架為我們提供的非常強大的工具,簡譯為鍵值編碼。iOS的開發(fā)中,可以允許開發(fā)者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不需要調用明確的存取方法。這樣就可以在運行時動態(tài)地訪問和修改對象的屬性,而不是在編譯時確定,這也是iOS開發(fā)中的黑魔法之一。KVC依賴于RunTime,在Objective-C的動態(tài)性方面發(fā)揮了重要作用。很多高級的iOS開發(fā)技巧都是基于KVC實現(xiàn)的。

KVC的主要功能是直接通過變量名稱字符串來訪問成員變量,不管是私有的還是公有的,這也就是為什么對于Objective-C來說,沒有真正的私有變量。因為一是可以利用RunTime直接獲取所有成員變量,二是通過KVC對成員變量進行訪問讀寫。

基本內容

無論是Swift還是Objective-C,KVC的定義都是對NSObject的擴展來實現(xiàn)的(Objective-C中有個顯式的NSKeyValueCoding類別名,而Swift沒有,也不需要)。所以對于所有直接或者間接繼承了NSObject的類型,也就是幾乎所有的Objective-C對象都能使用KVC(一些純Swift類和結構體是不支持KVC的),下面是KVC重要的四個方法

- (id)valueForKey:(NSString *)key;  // 直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; // 通過Key來設值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通過KeyPath來設值

KVC中key的查找順序

設置值的查找順序

  • 1、程序優(yōu)先調用set<Key>:屬性值方法,代碼通過setter方法完成設置。注意,這里的<key>是指成員變量名

  • 2、如果沒有找到set<Key>:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會執(zhí)行setValue:forUndefinedKey:方法。在方法返回YES的情況下,緊接著就會查找_<key>,如果沒有_<key>成員變量,KVC機制會搜索_is<Key>的成員變量。

  • 3、如果該類既沒有set<Key>:方法,也沒有_<key>_is<Key>成員變量,KVC機制再會繼續(xù)搜索<key>is<Key>的成員變量。再給它們賦值。

  • 4、如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的setValue:forUndefinedKey:方法,默認是拋出異常,如果實現(xiàn)了該方法,那么不會拋出異常。

具體例子分析

創(chuàng)建一個Person類,實現(xiàn)如下

@interface Person : NSObject
@end

@implementation Person
{
    NSString *setName;
    NSString *isName;
    NSString *_name;
    NSString *_isName;
    NSString *name;
}

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

- (NSString *)getName {
    NSLog(@"getName");
    return setName;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"setValueForUndefinedKey: %@", key);
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"valueForUndefinedKey: %@", key);
    return nil;
}

@end

測試部分

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person = [[Person alloc]init];
    [person setValue:@"newName" forKey:@"name"];
    NSString *name = [person valueForKey:@"name"];
    NSLog(@"name: %@", name);
}

1、將accessInstanceVariablesDirectly注釋掉,運行程序,將執(zhí)行setNamegetName

setName: newName
getName
name: newName

也就是說先調用set<Key>:方法

2、接下來將setName方法注釋掉,并且修改getName方法,將返回值修改為_name

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

- (NSString *)getName {
    NSLog(@"getName:%@", _name);
    return _name;
}

運行程序,結果如下

getName:newName
name: newName

也就是滿足了第二步,如果沒有找到set<Key>:方法,就會查找_<key>為其賦值

3、注釋掉_name,并修改getName方法,返回_isName

@implementation Person
{
    NSString *setName;
    NSString *isName;
   // NSString *_name;
    NSString *_isName;
    NSString *name;
}

- (NSString *)getName {
    NSLog(@"getName:%@", _isName);
    return _isName;
}

運行程序,結果如下

getName:newName
name: newName

后面2步就不再驗證,有興趣可以自己嘗試。

接下來驗證accessInstanceVariablesDirectly屬性,代碼回復到最初狀態(tài),并且將setNamegetName注釋

@implementation Person
{
    NSString *setName;
    NSString *isName;
    NSString *_name;
    NSString *_isName;
    NSString *name;
}

//- (void)setName:(NSString *)name {
//    setName = name;
//    NSLog(@"setName: %@", name);
//}
//
//- (NSString *)getName {
//    NSLog(@"getName");
//    return setName;
//}

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"setValueForUndefinedKey: %@", key);
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"valueForUndefinedKey: %@", key);
    return nil;
}

@end

依然運行程序,結果如下

setValueForUndefinedKey: name
valueForUndefinedKey: name
name: (null)

說明再找不到setName:方法后,不再去找name系列成員變量,而是直接調用setValue:forUndefinedKey:方法

獲取值的查找順序

  • 1、首先按get<Key>,<key>,is<Key>的順序查找getter方法,找到直接調用。如果是BOOL、int等內建值類型,會做NSNumber的轉換。

  • 2、如果上面的getter沒有找到,KVC則會查找countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發(fā)送屬于NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes這幾個方法組合的形式調用。還有一個可選的get<Key>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。

  • 3、還沒找到,查找countOf<Key>、enumeratorOf<Key>、memberOf<Key>格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet所有方法的代理集合。

  • 4、還是沒找到,如果類方法accessInstanceVariablesDirectly返回YES。那么按 _<key>,_is<Key>,<key>,is<key>的順序搜索成員名。

  • 5、再沒找到,調用valueForUndefinedKey。

在KVC中使用keyPath

開發(fā)過程中,一個類的成員變量有可能是自定義類或其他的復雜數(shù)據(jù)類型,你可以先用KVC獲取該屬性,然后再次用KVC來獲取這個自定義類的屬性,但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath

- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通過KeyPath來設值

具體例子

@interface Account : NSObject

@property (nonatomic, copy) NSString *password;

@end

@implementation Account

@end

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *age;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, strong) Account *count;

@end

Example

Person *person = [[Person alloc]init];
Account *account = [[Account alloc] init];
account.password = @"xxxx";
person.account = account;

NSString *password1 = [person valueForKeyPath:@"account.password"];
[person setValue:@"yyyy" forKeyPath:@"account.password"];
NSString *password2 = [person valueForKeyPath:@"account.password"];

NSLog(@"password1 = %@, password2 = %@", password1, password2);
//  password1 = xxxx, password2 = yyyy    

對象關系映射

ORM(Object Relational Mapping,對象關系映射),說白了就是將JSON轉換為對象。在iOS開發(fā)最初的一段時間,還沒有特別好的第三方Model解析庫,那時候基本上是使用NSKeyValueCoding提供的方法

Example

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

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *age;
@property (nonatomic, copy) NSString *address;

@end

NS_ASSUME_NONNULL_END

// Person.m
#import "Person.h"

@implementation Person

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"UndefinedKey: %@", key);
}

@end

// ViewController.m

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSDictionary *dic = @{@"name": @"xiao",
                          @"age": @22,
                          @"address": @"China",
                          @"phone": @"186xxxxxxxx"
                          };
    Person *person = [[Person alloc]init];
    [person setValuesForKeysWithDictionary:dic];
}

@end

對私有屬性的訪問

在我們使用一些系統(tǒng)控件時,對一些內部屬性系統(tǒng)往往并沒有暴露給我們,需要我們使用KVC進行訪問。

例如,在使用UITextField時,對于設置placeholdertextColor時只能通過attributedPlaceholder這樣的NSAttributedString來設置,并且每次更改都需要這樣設置一遍,有些麻煩。這里可以通過KVC來獲取_placeholderLabel并對其賦值,修改顏色

UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(50, 100, 300, 44)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.placeholder = @"please input something....";

UILabel *placeLabel = [textField valueForKey:@"_placeholderLabel"];
placeLabel.textColor = [UIColor redColor];

[self.view addSubview:textField];

效果如下圖

屏幕快照 2019-03-22 下午4.23.50.png

但是需要注意

蘋果對一些系統(tǒng)控件的實現(xiàn)過程中,很多子控件使用了懶加載,即用到時才會去創(chuàng)建實例,所以使用KVC進行訪問時,需要注意訪問的時機。

例如上面,應該在placeholder設置值之后才訪問,否則_placeholderLabel獲取為nil,textColor設置無效。

另外一個需要注意的地方:雖然采用KVC訪問一些私有成員變量不屬于使用私有API,上線時不太會因此被拒絕,但是私有的成員變量可能會隨著iOS版本的不同而有所變化。

所以使用KVC訪問私有變量時需要謹慎。

控制是否觸發(fā)setter、getter方法

有些時候為了監(jiān)控某個屬性的值訪問情況會重寫settergetter方法,但只在特定的情況下觸發(fā),通過其他方式不觸發(fā)settergetter,我們可以通過KVC來做。例如:上面的Person類,重寫name屬性的setter方法,如下:

// Person.m
- (void)setName:(NSString *)name {
    _name = name;
    NSLog(@"setName: %@", name);
}

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    NSDictionary *dic = @{@"name": @"xiao",
                          @"age": @22,
                          @"address": @"China",
                          @"phone": @"186xxxxxxxx"
                          };
    Person *person = [[Person alloc]init];
    [person setValuesForKeysWithDictionary:dic];

    [person setValue:@"zhangsan" forKey:@"_name"];
}

運行輸出

undefinedKey: phone
setName: xiao

只有在 [person setValuesForKeysWithDictionary:dic];時觸發(fā)過一次setName:的方法,而通過KVC_name賦值并不會觸發(fā),如果也想觸發(fā),可以將“_name”改成“name”來實現(xiàn)。

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

這樣運行輸出

undefinedKey: phone
setName: xiao
setName: zhangsan

可以根據(jù)實際需求,選擇使用KVC的方法,出現(xiàn)上面的情況是因為KVC的查找成員變量的機制。

查找成員變量的機制

如果一個實例對象用KVC來訪問其成員變量,則會按照以下的順序來進行查找,例如:我們調用的方法是:

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

1、訪問setName方法
2、訪問_name成員變量
3、訪問_isName成員變量
4、訪問name成員變量
5、訪問isName成員變量

以上就是KVC查找的過程,只有在某一步找到才會不繼續(xù)向下查找,否則會按照上面的順序逐個查找,如果到最后一個也找不到,那就會調用)setValue:forUndefinedKey:方法。

值得注意的是,KVC的協(xié)議NSKeyValueCoding中的accessInstanceVariablesDirectly屬性:

@property (class, readonly) BOOL accessInstanceVariablesDirectly;

該屬性默認為YES,如果重寫返回NO,則下面這些方法都將不起作用

 -valueForKey:, -setValue:forKey:, -mutableArrayValueForKey:,
 -storedValueForKey:, -takeStoredValue:forKey:, and -takeValue:forKey:

也就是相當于禁止KVC的方法。但是我們在之前使用setValuesForKeysWithDictionary:方法仍然可以使用。

KVC進階用法

KVC的使用可不僅僅是訪問成員變量這么簡單,蘋果為KVC提供了一些高級的用法,方便開發(fā)者在代碼中的使用

1、keyPath訪問

對于keyPath訪問很多人應該不陌生,在一些ORM庫中經常會指向通過keyPath來映射賦值。

舉個例子:回到剛才的textFiled的場景,我們知道有一個“_placeholderLabel”成員變量,它是一個UILabel的實例,我們獲取到該UILabel,并對其進行textColor屬性賦值,達到了想要的效果。但實際上可以一步完成上面的操作

UITextField *textField = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 50)];
textField.placeholder = @"placeholder";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[self.view addSubview:textField];

2、集合類型的訪問

集合類型包括數(shù)組、字典和集合,其中,集合還分為有序和無序。對于我們的Person類,現(xiàn)在需要一個屬性friends:

@property(nonatomic, strong) NSMutableArray *friends;

然后在Person.m文件中會有提示下面等一系列方法

-(id)objectInFriendsAtIndex:(NSUInteger)index 
-(NSArray *)friendsAtIndexes:(NSIndexSet *)indexes 
-(NSUInteger)countOfFriends

這是KVC幫我們針對friends屬性添加的一系列方法中的幾個,表示對friends屬性支持KVC集合類型,目的是方便我們使用KVC時對其進行便捷式訪問。那么什么是便捷式訪問呢?

假設一下,如果我們要通過KVC獲取到Person對象的friends屬性,并添加一個friend,如果不使用KVC集合類型訪問,可能需要這樣寫:

//方式1
NSMutableArray *friends = [person valueForKey:@"friends"];
[friends addObject:[Person new]];

// 當然也可以使用該方法
[person.friends addObject:[Person new]];

通過KVC方式,我們可以簡化方式1的操作

[[person mutableArrayValueForKey:@"friends"] addObject:[Person new]];

除了簡化代碼這一點,實際上這種KVC集合類型還有一個好處,就是可以對于不可變的集合類型提供安全的可變訪問。

上面部分,我們的friends屬性是可變數(shù)組,如果改成不可變數(shù)組NSArray,那么再對其進行添加對象,使用上面的兩種方法就會不一樣了,前者會直接crash,而后者則會安全訪問,即使是不可變數(shù)組也可以增加數(shù)組元素。

但我們知道對于NSArray是不可變的,這在創(chuàng)建Person實例的時候就已經確定好了,對其添加元素,并不是不可變數(shù)組可以添加元素,而是在用KVC進行集合類型訪問時,如果是不可變數(shù)組,在添加元素時會重新創(chuàng)建一個不可變數(shù)組對象,然后將friends屬性指向新創(chuàng)建的不可變數(shù)組。

雖然使用這種方式不會引起奔潰,但是在創(chuàng)建Person類時,既然將friends屬性設置為不可變數(shù)組,那么就應該避免再向其添加對象,因為這與最初的邏輯相左。

3、KVC驗證

使用KVC也是有風險的,因為通過字符串去訪問實例變量,雖然KVC提供類復雜的查找邏輯來幫助找到對應的成員變量,但是仍然會發(fā)生找不到的情況。

例如:我們使用-setValue:forKey來對對象進行賦值訪問,當-setValue:forUndefinedKey沒有實現(xiàn),而且如果key不存在,將會導致奔潰。

[person setValue:@"123" forKey:@"key"];

如果想避免奔潰,實現(xiàn)-setValue:forUndefinedKey,打印不存在的key,或者使用try-catch,如下使用try-catch捕獲異常,得到相關信息

    NSDictionary *dic = @{@"name": @"xiao",
                          @"age": @22,
                          @"address": @"China"
                          };
    Person *person = [[Person alloc]init];
    [person setValuesForKeysWithDictionary:dic];

    @try {
        [person setValue:@"123" forKey:@"thought"];
    } @catch (NSException *exception) {
        NSLog(@" %@", exception.userInfo);
    } @finally {
        
    }

打印如下:

{
    NSTargetObjectUserInfoKey = "<Person: 0x600000ed3840>";
    NSUnknownUserInfoKey = thought;
}

NSUnknownUserInfoKey所對應的值就是不存在的key

除了上面的方法,KVC還提供了一種值驗證的方法

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue
               forKey:(NSString *)inKey
                error:(out NSError * _Nullable __autoreleasing *)outError

該方法是驗證值是否符合key所對應的類型,或者說值類型是否正確。方法中的ioValue參數(shù)是要賦值二級指針類型。如果不是我們想要的值,則可以直接改變ioValue的指向,也就是重新指向一個正確的值。

例如:我們現(xiàn)在驗證Person實例的字符串屬性name,因此調用驗證方法來判斷

 UIColor *color = [UIColor yellowColor];
 NSError *error = nil;
 BOOL isOK = [person validateValue:&color forKeyPath:@"name" error:&error];

name屬性需要接受字符串類型的值,很顯然我們傳一個color是錯誤的,然而通過運行發(fā)現(xiàn)驗證的返回值是YES,表示驗證通過,這明顯不合理。

根據(jù)官方文檔描述,對于屬性的驗證分為是否必需,默認為不必需,如果是不必需,則會直接返回YES,不會對其進行驗證,而如果需要驗證,則需要在Person.m中實現(xiàn)如下方法

-(BOOL)validateName:(id *)ioValue error:(NSError **)error {
    if ([*ioValue isKindOfClass:NSString.class]) {
        return YES;
    }
    return NO;
}

現(xiàn)在再去驗證,會發(fā)現(xiàn)返回NO,符合我們的預期。

4、函數(shù)操作

同樣還是對于一些集合類型的數(shù)據(jù),我們希望可以利用共同性去做一些快捷的操作,例如求平均值和求和等,不需要再去for循環(huán)或者枚舉。

例如:有一個Person類型的數(shù)組,想求所有person的age之和

NSDictionary *dic1 = @{@"name": @"1",
                       @"age": @22
                      };
NSDictionary *dic2 = @{@"name": @"2",
                       @"age": @21
                       };
NSDictionary *dic3 = @{@"name": @"3",
                       @"age": @23
                      };
Person *person1 = [[Person alloc]init];
Person *person2 = [[Person alloc]init];
Person *person3 = [[Person alloc]init];

[person1 setValuesForKeysWithDictionary:dic1];
[person2 setValuesForKeysWithDictionary:dic2];
[person3 setValuesForKeysWithDictionary:dic3];

NSArray *persons = @[person1, person2, person3];
NSNumber *sumAge = [persons valueForKeyPath:@"@sum.age"];
NSLog(@"sumAge: %@", sumAge); // 66

上面獲取的就是所有人的age之和,不用for循環(huán),直接使用KVC實現(xiàn)。

注意:使用的是KeyPath,并且sum前面加一個@表示是數(shù)組特有的鍵.

除此之外,還可以求出數(shù)組的平均值、最大值、最小值

 NSNumber *count = [persons valueForKeyPath:@"@count"];
 NSLog(@"count: %@", count); // 3
 NSNumber *ageAve = [persons valueForKeyPath:@"@avg.age"];
 NSLog(@"avg: %@", ageAve); // 22
 NSNumber *maxAge = [persons valueForKeyPath:@"@max.age"];
 NSLog(@"max: %@", maxAge); // 23
 NSNumber *minAge = [persons valueForKeyPath:@"@min.age"];
 NSLog(@"min: %@", minAge); // 21

在NSKeyValueCoding.h文件中,定義了一系列的NSKeyValueOperator,而這些Operator都是為數(shù)組類型準備的。

更多詳細內容可以參考這里

參考

Key-Value Coding Programming Guide

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,629評論 1 32
  • 本文參考: KVC官方文檔 KVC原理剖析 iOS KVC詳解 KVC 簡介 KVC全稱是Key Value Co...
    擰發(fā)條鳥xds閱讀 5,436評論 6 23
  • 原文:iOS 關于KVC的一些總結 本文參考: KVC官方文檔 KVC原理剖析 iOS KVC詳解 KVC 簡介 ...
    liyoucheng2014閱讀 1,002評論 0 3
  • 關于鍵值編碼 鍵值編碼(KVC)是一種由NSKeyValueCoding非正式協(xié)議提供的機制,對象采用該機制來提供...
    漸z閱讀 1,170評論 0 0
  • 大一課堂表現(xiàn)不咋樣…… 大二被主課老師拉去辦公室談話,老師說了很久,老師說我為啥沒有反應…… 沉默半天我突然想起 ...
    安娜菇涼愛吃甜點閱讀 474評論 0 0

友情鏈接更多精彩內容