[iOS] KVC Briefing

1. Basic methods

KVC(Key-value coding)鍵值編碼,就是指iOS的開(kāi)發(fā)中,可以允許開(kāi)發(fā)者通過(guò)Key名直接訪問(wèn)對(duì)象的屬性,或者給對(duì)象的屬性賦值。而不需要調(diào)用明確的存取方法。這樣就可以在運(yùn)行時(shí)動(dòng)態(tài)地訪問(wèn)和修改對(duì)象的屬性。而不是在編譯時(shí)確定,這也是iOS開(kāi)發(fā)中的黑魔法之一。很多高級(jí)的iOS開(kāi)發(fā)技巧都是基于KVC實(shí)現(xiàn)的。

KVC的定義都是對(duì)NSObject的擴(kuò)展來(lái)實(shí)現(xiàn)的,對(duì)于所有繼承了NSObject的類(lèi)型,都能使用KVC(一些純Swift類(lèi)和結(jié)構(gòu)體是不支持KVC的,因?yàn)闆](méi)有繼承NSObject),下面是valueForKey的definition文件:

@interface NSObject(NSKeyValueCoding)

- (nullable id)valueForKey:(NSString *)key;

下面是KVC最為重要的四個(gè)方法:

- (nullable id)valueForKey:(NSString *)key;                          //直接通過(guò)Key來(lái)取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通過(guò)Key來(lái)設(shè)值

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過(guò)KeyPath來(lái)取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過(guò)KeyPath來(lái)設(shè)值

NSKeyValueCoding類(lèi)別中其他的一些方法:

+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES,表示如果沒(méi)有找到Set<Key>方法的話(huà),會(huì)按照_key,_iskey,key,iskey的順序搜索成員,設(shè)置成NO就不這樣搜索

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性?驗(yàn)證的API,它可以用來(lái)檢查set的值是否正確、為不正確的值做一個(gè)替換值或者拒絕設(shè)置新值并返回錯(cuò)誤原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個(gè)NSMutableArray,那么可以用這個(gè)方法來(lái)返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且沒(méi)有KVC無(wú)法搜索到任何和Key有關(guān)的字段或者屬性,則會(huì)調(diào)用這個(gè)方法,默認(rèn)是拋出異常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個(gè)方法一樣,但這個(gè)方法是設(shè)值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時(shí)面給Value傳nil,則會(huì)調(diào)用這個(gè)方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對(duì)應(yīng)的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。

偷偷表示其實(shí)dictionaryWithValuesForKeys感覺(jué)還挺好的,可以把程序里面的model類(lèi)直接轉(zhuǎn)成dict,比如上傳給服務(wù)器的時(shí)候比較方便啊~


2. setValueForKey過(guò)程
具體過(guò)程點(diǎn)進(jìn)definition就能看.png

KVC要設(shè)值,那么就要對(duì)象中對(duì)應(yīng)的key,KVC在內(nèi)部是按什么樣的順序來(lái)尋找key的。當(dāng)調(diào)用[test1 setValue:@"ying" forKey:@"name"];的代碼時(shí),底層的執(zhí)行機(jī)制如下:

  • (1) 程序優(yōu)先調(diào)用set<Key>:屬性值方法,代碼通過(guò)setter方法完成設(shè)置。

但是蘋(píng)果默認(rèn)的setter是會(huì)大寫(xiě)變量的首字母的,類(lèi)似于如果有個(gè)變量是name,那么setter的方法名是setName。

故而在KVC中key的大小寫(xiě)不同可能會(huì)造成不一樣的結(jié)果哦。

如果你只有一個(gè)setter是setname(沒(méi)有name實(shí)例變量只定義了set方法),那么當(dāng)你通過(guò)KVC給key為name設(shè)value的時(shí)候會(huì)crash的,當(dāng)然如果你給Name設(shè)值也會(huì)crash;如果你的setter是setName,那么無(wú)論你設(shè)值的key是name還是Name都不會(huì)crash的~

舉個(gè)例子~

// .h文件
@interface KVOTest : NSObject

@end

// .m文件
#import "KVOTest.h"

@interface KVOTest() {
    NSString *name;
}

@end

@implementation KVOTest

- (instancetype)init {
    if (self = [super init]) {
        
    }
    return self;
}

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

@end

======
KVOTest *test1 = [[KVOTest alloc] init];

不crash:
[test1 setValue:@"ying" forKey:@"LastName"];
[test1 setValue:@"ying" forKey:@"lastName"];

如果將setLastName改成setlastName,那么無(wú)論是設(shè)值LastName或者lastName都會(huì)crash……

也就是說(shuō),請(qǐng)大寫(xiě)你的setter方法setXXX首字母(例如setName)~

  • (2) 如果沒(méi)有找到setName:方法,KVC機(jī)制會(huì)檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒(méi)有返回YES,默認(rèn)該方法會(huì)返回YES,如果你重寫(xiě)了該方法讓其返回NO的話(huà),那么在這一步KVC會(huì)執(zhí)行setValue:forUndefinedKey:方法。

只要accessInstanceVariablesDirectly不是NO,KVC機(jī)制會(huì)搜索該類(lèi)里面有沒(méi)有名為<key>的成員變量,無(wú)論該變量是在類(lèi)接口處定義,還是在類(lèi)實(shí)現(xiàn)處定義,也無(wú)論用了什么樣的訪問(wèn)修飾符,只在存在以<key>命名的變量,KVC都可以對(duì)該成員變量賦值。

再來(lái)舉個(gè)例子~
如果KVOTest有個(gè)property是name,還有個(gè)實(shí)例變量是name,那么其實(shí)他有兩個(gè)實(shí)例變量分別是name和_name,那么當(dāng)我們?cè)O(shè)值key為name的時(shí)候,究竟設(shè)值的是哪個(gè)呢?

// .h文件
@interface KVOTest : NSObject
@property (nonatomic, copy) NSString * name;
@end

// .m
@interface KVOTest() {
    NSString *name;
}
@end

// 設(shè)值
[test1 setValue:@"ying" forKey:@"_name"];
[test1 setValue:@"ying" forKey:@"name"];

以上兩種設(shè)值的方法最后看test1都是_name被設(shè)為了ying,但是name都是nil。

如果該類(lèi)即沒(méi)有set<key>:方法,也沒(méi)有_<key>成員變量,KVC機(jī)制會(huì)搜索_is<Key>的成員變量。

// .h文件
@interface KVOTest : NSObject
@property (nonatomic, copy) NSString * isName;
@end

// .m文件
@interface KVOTest() {
    NSString *isName;
}
@end

// 使用
[test1 setValue:@"ying" forKey:@"name"];
[test1 setValue:@"ying" forKey:@"Name"];

同理,上面兩種最后都是_isName __NSCFConstantString * @"ying" 0x00000001038ddc08,當(dāng)然如果你注釋掉.h里面的property,就會(huì)發(fā)現(xiàn)isName被成功賦值了~

但是哦,和setter有同樣的一個(gè)問(wèn)題,就是isXXX也是蘋(píng)果默認(rèn)會(huì)首字母大寫(xiě),于是當(dāng)你找key name的時(shí)候,它會(huì)找isName,如果你只有一個(gè)isname變量,那么你是無(wú)法set值給key是name或者Name的。

所以屬性isXXX也要首字母大寫(xiě)啊~如isName

但是很神奇的是,如果變量是name或者Name,那么你set key的時(shí)候無(wú)論是name還是Name都是可以正常運(yùn)行不會(huì)crash的。但是如果你給key nAme設(shè)值還是會(huì)crash哦。

也就是說(shuō),對(duì)于正常字母一致的變量,不會(huì)檢查首字符大小寫(xiě)的一致性,例如變量是name但是可以給key Name設(shè)值。但是對(duì)于isXXX的變量以及setter方法,由于蘋(píng)果默認(rèn)是首字母大寫(xiě)的格式生成的,也就是例如key是name,setter就應(yīng)該是setName,故而會(huì)檢查大小寫(xiě),例如如果給只有isname變量的對(duì)象設(shè)值key為Name的value就會(huì)crash啦。

我們?cè)僭囈幌?_isKey和key的成員變量哪個(gè)優(yōu)先級(jí)更高~

// .h文件
@interface KVOTest : NSObject
@property (nonatomic, copy) NSString * isName;
@end

// .m文件
@interface KVOTest() {
    NSString *name;
}
@end

// 使用
[test1 setValue:@"ying" forKey:@"name"];

結(jié)果是_isName __NSCFConstantString * @"ying" 0x0000000101413c08

故而,總體查找變量的順序是 _key > _isKey > key > isKey

  • (3) 如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會(huì)執(zhí)行該對(duì)象的setValue:forUndefinedKey:方法,默認(rèn)是拋出異常。

如果開(kāi)發(fā)者想讓這個(gè)類(lèi)禁用KVC里,那么重寫(xiě)+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話(huà)如果KVC沒(méi)有找到set<Key>:屬性名時(shí),會(huì)直接用setValue:forUndefinedKey:方法。

如果你重寫(xiě)了setValue:forUndefinedKey:就可以不拋出異常啦,只是不建議這么做。

@implementation KVOTest

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

@end

使用:
[test1 setValue:@"ying" forKey:@"nname"];

輸出:
2019-12-08 19:55:58.123500+0800 Example1[2597:78836] forUndefinedKey: nname

3. valueForKey過(guò)程

當(dāng)調(diào)用valueForKey:@"name"的代碼時(shí),KVC對(duì)key的搜索方式不同于setValueforKey,其搜索方式如下:

  • (1) 首先按get<Key>,<key>,is<Key>的順序方法查找getter方法,找到的話(huà)會(huì)直接調(diào)用。如果是BOOL或者Int等值類(lèi)型, 會(huì)將其包裝成一個(gè)NSNumber對(duì)象。

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

舉個(gè)例子~

#import "KVOTest.h"

@interface KVOTest() 

@end

@implementation KVOTest

- (NSUInteger)countOfNames {
    return 1;
}

-(id)objectInNamesAtIndex:(NSUInteger)index {
    return @"yy";
}

@end

=====

使用:
KVOTest *test1 = [[KVOTest alloc] init];
id name = [test1 valueForKey:@"names"];

打斷點(diǎn)看下name就是醬紫的:

name    NSKeyValueArray *   0x6000039a5f20  0x00006000039a5f20

(lldb) po name
<NSKeyValueArray 0x6000039a5f20>(
yy
)
  • (3) 如果上面的方法沒(méi)有找到,那么會(huì)同時(shí)查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個(gè)方法都找到,那么就返回一個(gè)可以響應(yīng)NSSet所的方法的代理集合,和上面一樣,給這個(gè)代理集合發(fā)NSSet的消息,就會(huì)以countOf<Key>,enumeratorOf<Key>,memberOf<Key>組合的形式調(diào)用。

  • (4) 如果還沒(méi)有找到,再檢查類(lèi)方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為),那么和先前的設(shè)值一樣,會(huì)按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量名,這里不推薦這么做,因?yàn)檫@樣直接訪問(wèn)實(shí)例變量破壞了封裝性,使代碼更脆弱。如果重寫(xiě)了類(lèi)方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話(huà),那么會(huì)直接調(diào)用valueForUndefinedKey:方法,默認(rèn)是拋出異常。


4. 基本類(lèi)型的讀取和設(shè)置

不是每一個(gè)方法都返回對(duì)象,但是valueForKey:總是返回一個(gè)id對(duì)象,如果原本的變量類(lèi)型是值類(lèi)型或者結(jié)構(gòu)體,返回值會(huì)封裝成NSNumber或者NSValue對(duì)象。

這兩個(gè)類(lèi)會(huì)處理從數(shù)字,布爾值到指針和結(jié)構(gòu)體任何類(lèi)型。然后需要我們手動(dòng)轉(zhuǎn)換成原來(lái)的類(lèi)型。

盡管valueForKey:會(huì)自動(dòng)將值類(lèi)型封裝成對(duì)象,但是setValueforKey:卻不行。你必須手動(dòng)將值類(lèi)型轉(zhuǎn)換成NSNumber或者NSValue類(lèi)型,才能傳遞過(guò)去。

例如對(duì):

@interface KVOTest() {
    int isName;
}

使用:
KVOTest *test1 = [[KVOTest alloc] init];
[test1 setValue:@10 forKey:@"name"];

結(jié)果:
isName  int 10

也就是說(shuō)雖然傳入的value是NSNumber,但是賦值給int屬性的還是int哦~ 但valueForKey拿到的就是id(NSNumber)啦~

數(shù)值類(lèi)基本類(lèi)型我們都知道會(huì)用NSNumber封裝,那么CGPoint這種struct呢?答案是NSValue

可以使用NSValue的數(shù)據(jù)類(lèi)型有:

+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);

NSValue主要用于處理結(jié)構(gòu)體型的數(shù)據(jù),它本身提供了如上集中結(jié)構(gòu)的支持。任何結(jié)構(gòu)體都是可以轉(zhuǎn)化成NSValue對(duì)象的,包括其它自定義的結(jié)構(gòu)體。

struct aStruct
{
    int a;
    int b;
};
typedef struct aStruct aStruct;

aStruct struct; 
struct.a = 0; 
struct.b = 0;

NSValue *anObj = [NSValue value:&struct withObjCType:@encode(aStruct)];

5. keyPath

在開(kāi)發(fā)過(guò)程中,一個(gè)類(lèi)的成員變量有可能是自定義類(lèi)或其他的復(fù)雜數(shù)據(jù)類(lèi)型,你可以先用KVC獲取該屬性,然后再次用KVC來(lái)獲取這個(gè)自定義類(lèi)的屬性,

但這樣是比較繁瑣的,對(duì)此,KVC提供了一個(gè)解決方案,那就是鍵路徑keyPath。顧名思義,就是按照路徑尋找key。

其實(shí)keyPath就是按照順序一層一層的用key value取值/賦值

犯懶偷來(lái)一個(gè)例子~

#import <Foundation/Foundation.h>

@interface Test1: NSObject {
    NSString *_name;
}
@end

@implementation Test1
@end

@interface Test: NSObject {
    Test1 *_test1;
}

@end

@implementation Test
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對(duì)象
        Test *test = [[Test alloc] init];
        //Test1生成對(duì)象
        Test1 *test1 = [[Test1 alloc] init];
        //通過(guò)KVC設(shè)值test的"test1"
        [test setValue:test1 forKey:@"test1"];
        //通過(guò)KVC設(shè)值test的"test1的name"
        [test setValue:@"xiaoming" forKeyPath:@"test1.name"];
        //通過(guò)KVC取值age打印
        NSLog(@"test的\"test1的name\"是%@", [test valueForKeyPath:@"test1.name"]);
        
    }
    return 0;
}

[test setValue:@"xiaoming" forKeyPath:@"test1.name"];相當(dāng)于先取test1的value,然后再設(shè)置拿到的value的name。


6. 周邊方法

有幾個(gè)相關(guān)的方法其實(shí)名字就可以看出是干啥的~ 例如validateValuevalueForUndefinedKeysetValueforUndefinedKey:setNilValueForKey

valueForUndefinedKeysetValueforUndefinedKey:一個(gè)是get的時(shí)候沒(méi)有拿到,一個(gè)是set的時(shí)候找不到key,默認(rèn)都是拋出異常的,如果你覆寫(xiě)可以不拋出,但一般不建議覆寫(xiě)。

setNilValueForKey是當(dāng)你給一個(gè)基本數(shù)據(jù)類(lèi)型賦值nil的時(shí)候會(huì)觸發(fā),如果變量是NSString這種對(duì)象類(lèi)型是不會(huì)的哦。默認(rèn)的setNilValueForKey是會(huì)拋出異常的,一般會(huì)覆寫(xiě)讓他不拋出。

例如:

@interface KVOTest() {
    int isName;
}

@end

@implementation KVOTest

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"setNilValueForKey:%@", key);
}

@end

// 使用
KVOTest *test1 = [[KVOTest alloc] init];
[test1 setValue:nil forKey:@"name"];

// 輸出:
2019-12-08 21:00:55.785039+0800 Example1[3330:120630] setNilValueForKey:name

如果你把isName的類(lèi)型改成NSString或者NSNumber就都不會(huì)有上面的輸出了哦~

我在嘗試這個(gè)的時(shí)候還發(fā)現(xiàn),其實(shí)即使你的isName類(lèi)型定義的是NSString,還是KVC給它設(shè)置成@24這種number,感覺(jué)KVC真的很黑魔法了但非常的危險(xiǎn)啊類(lèi)型檢查都木有。

validateValue聽(tīng)起來(lái)就知道是用于看value是不是可以賦值給key的一個(gè)驗(yàn)證,例如給之前的類(lèi)增加這個(gè)方法:

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError {
    NSNumber *num = *ioValue;
    if (num.intValue == 10) {
        NSLog(@"validateValue no");
        return NO;
    }
    
    NSLog(@"validateValue yes");
    return YES;
}

注意這里面(inout id _Nullable __autoreleasing *)ioValue是指向value的指針哦

調(diào)用的時(shí)候醬紫:

KVOTest *test1 = [[KVOTest alloc] init];
[test1 validateValue:&number forKey:@"name" error:NULL];
[test1 setValue:@10 forKey:@"name"];

輸出:
2019-12-08 21:16:00.689897+0800 Example1[3496:130130] validateValue yes

也就是其實(shí)[test1 setValue:@10 forKey:@"name"];不會(huì)自動(dòng)觸發(fā)valid,無(wú)論是不是valid都會(huì)賦值給key,其實(shí)這個(gè)方法是讓我們自己賦值之前手動(dòng)調(diào)用的……


7. 容器集合類(lèi)

同時(shí)蘋(píng)果對(duì)一些容器類(lèi)比如NSArray或者NSSet等,KVC有著特殊的實(shí)現(xiàn)。

這一段其實(shí)我們?cè)趃et值得時(shí)候也看到過(guò)~

有序集合Array對(duì)應(yīng)方法如下:

-countOf<Key>//必須實(shí)現(xiàn),對(duì)應(yīng)于NSArray的基本方法count:2  -objectIn<Key>AtIndex:

-<key>AtIndexes://這兩個(gè)必須實(shí)現(xiàn)一個(gè),對(duì)應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實(shí)現(xiàn)的,但實(shí)現(xiàn)后可以提高性能,其對(duì)應(yīng)于 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個(gè)必須實(shí)現(xiàn)一個(gè),類(lèi)似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個(gè)必須實(shí)現(xiàn)一個(gè),類(lèi)似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://可選的,如果在此類(lèi)操作上有性能問(wèn)題,就需要考慮實(shí)現(xiàn)之

無(wú)序集合Set對(duì)應(yīng)方法如下:

-countOf<Key>//必須實(shí)現(xiàn),對(duì)應(yīng)于NSArray的基本方法count:

-objectIn<Key>AtIndex:

-<key>AtIndexes://這兩個(gè)必須實(shí)現(xiàn)一個(gè),對(duì)應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實(shí)現(xiàn)的,但實(shí)現(xiàn)后可以提高性能,其對(duì)應(yīng)于 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個(gè)必須實(shí)現(xiàn)一個(gè),類(lèi)似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個(gè)必須實(shí)現(xiàn)一個(gè),類(lèi)似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://這兩個(gè)都是可選的,如果在此類(lèi)操作上有性能問(wèn)題,就需要考慮實(shí)現(xiàn)之

比較神奇的是keyPath對(duì)集合有特殊操作哦!(不是valueForKey,必須是valueForKeyPath哦)

NSArray *numA = @[@20, @30, @40];
NSNumber *sum = [numA valueForKeyPath:@"@sum.intValue"];

例如上面醬紫的sum就是@90~

簡(jiǎn)單集合運(yùn)算符共有@avg, @count , @max , @min ,@sum5種,就是對(duì)集合內(nèi)的元素們的某個(gè)屬性做平均、求和等操作。

還有兩個(gè)神奇的運(yùn)算符,分別是@distinctUnionOfObjects和@unionOfObjects~

@distinctUnionOfObjects會(huì)返回去重后的集合,@unionOfObjects會(huì)返回全集

例如:

NSArray *numA = @[@20, @30, @40, @30];
NSArray *distinctA = [numA valueForKeyPath:@"@distinctUnionOfObjects.intValue"];
NSArray *unionA = [numA valueForKeyPath:@"@unionOfObjects.intValue"];

輸出:
Printing description of distinctA:
<__NSArrayI 0x6000028b52c0>(
20,
30,
40
)

Printing description of unionA:
<__NSArrayM 0x6000028b4450>(
20,
30,
40,
30
)

※ Dictionary的KVC and 通過(guò)字典批量操作

這段我也是懶了后面的就直接借別人的了,對(duì)dict的話(huà)其實(shí)用kvc和dict的key value取值是一樣的。。

當(dāng)對(duì)NSDictionary對(duì)象使用KVC時(shí),valueForKey:的表現(xiàn)行為和objectForKey:一樣。所以使用valueForKeyPath:用來(lái)訪問(wèn)多層嵌套的字典是比較方便的。

KVC里面還有兩個(gè)關(guān)于NSDictionary的方法:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

dictionaryWithValuesForKeys:是指輸入一組key,返回這組key對(duì)應(yīng)的屬性,再組成一個(gè)字典。

setValuesForKeysWithDictionary是用來(lái)修改Model中對(duì)應(yīng)key的屬性。

這倆方法說(shuō)白了就是對(duì)任何對(duì)象批量進(jìn)行key value操作。

下面直接用代碼會(huì)更直觀一點(diǎn):

#import <Foundation/Foundation.h>

@interface Address : NSObject

@end

@interface Address()

@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;

@end

@implementation Address

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //模型轉(zhuǎn)字典
        Address* add = [Address new];
        add.country = @"China";
        add.province = @"Guang Dong";
        add.city = @"Shen Zhen";
        add.district = @"Nan Shan";
        NSArray* arr = @[@"country",@"province",@"city",@"district"];
        NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對(duì)應(yīng)key所有的屬性全部取出來(lái)
        NSLog(@"%@",dict);
        
        //字典轉(zhuǎn)模型
        NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict];            //用key Value來(lái)修改Model的屬性
        NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);
    }
    return 0;
}

打印結(jié)果:

2018-05-05 17:08:48.824653+0800 KVCKVO[35807:6368235] {
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
2018-05-05 17:08:48.825075+0800 KVCKVO[35807:6368235] country:USA province:california city:Los angle

打印出來(lái)的結(jié)果完全符合預(yù)期。


8. 應(yīng)用場(chǎng)景
  • 用KVC來(lái)訪問(wèn)和修改私有變量
    對(duì)于類(lèi)里的私有屬性,Objective-C是無(wú)法直接訪問(wèn)的,但是KVC是可以的。

  • Model和字典轉(zhuǎn)換
    這是KVC強(qiáng)大作用的又一次體現(xiàn),KVC和Objc的runtime組合可以很容易的實(shí)現(xiàn)Model和字典的轉(zhuǎn)換。

  • 修改一些控件的內(nèi)部屬性
    很多UI控件都由很多內(nèi)部UI控件組合而成的,但是Apple度沒(méi)有提供這訪問(wèn)這些控件的API,這樣我們就無(wú)法正常地訪問(wèn)和修改這些控件的樣式。
    而KVC在大多數(shù)情況可下可以解決這個(gè)問(wèn)題。最常用的就是個(gè)性化UITextField中的placeHolderText了。

  • 操作集合
    Apple對(duì)KVC的valueForKey:方法作了一些特殊的實(shí)現(xiàn),比如說(shuō)NSArray和NSSet這樣的容器類(lèi)就實(shí)現(xiàn)了這些方法。所以可以用KVC很方便地操作集合。

  • 用KVC實(shí)現(xiàn)高階消息傳遞
    當(dāng)對(duì)容器類(lèi)使用KVC時(shí),valueForKey:將會(huì)被傳遞給容器中的每一個(gè)對(duì)象,而不是容器本身進(jìn)行操作。結(jié)果會(huì)被添加進(jìn)返回的容器中,這樣,開(kāi)發(fā)者可以很方便的操作集合來(lái)返回另一個(gè)集合。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSArray* arrStr = @[@"english",@"franch",@"chinese"];
        NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
        for (NSString* str  in arrCapStr) {
            NSLog(@"%@",str);
        }
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.integerValue);
        }
        
    }
    return 0;
}

打印結(jié)果:
2018-05-05 17:16:21.975983+0800 KVCKVO[35824:6395514] English
2018-05-05 17:16:21.976296+0800 KVCKVO[35824:6395514] Franch
2018-05-05 17:16:21.976312+0800 KVCKVO[35824:6395514] Chinese
2018-05-05 17:16:21.976508+0800 KVCKVO[35824:6395514] 7
2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 6
2018-05-05 17:16:21.976550+0800 KVCKVO[35824:6395514] 7

方法capitalizedString被傳遞到NSArray中的每一項(xiàng),這樣,NSArray的每一員都會(huì)執(zhí)行capitalizedString并返回一個(gè)包含結(jié)果的新的NSArray。

從打印結(jié)果可以看出,所有String都成功以轉(zhuǎn)成了大寫(xiě)。
同樣如果要執(zhí)行多個(gè)方法也可以用valueForKeyPath:方法。它先會(huì)對(duì)每一個(gè)成員調(diào)用 capitalizedString方法,然后再調(diào)用length,因?yàn)閘enth方法返回是一個(gè)數(shù)字,所以返回結(jié)果以NSNumber的形式保存在新數(shù)組里。

所以當(dāng)對(duì)array取key是count的value的時(shí)候,它會(huì)取找每個(gè)元素的key有木有count,木有就會(huì)crash……而不是找array.count。


9. 獲取對(duì)象的屬性列表以及方法列表

KVC如果你想找到可以改啥,至少得先知道有什么ivar以及setter、getter。下面的方式可以讓你獲取一個(gè)現(xiàn)成類(lèi)的方法和實(shí)例變量~

unsigned int count;
Method *methods = class_copyMethodList([UITextView class], &count);
for (int i = 0; i < count; i++) {
    Method method = methods[i];
    SEL selector = method_getName(method);
    NSString *name = NSStringFromSelector(selector);
    NSLog(@"method_getName:%@",name);
}

unsigned int numIvars;
Ivar *vars = class_copyIvarList([UITextView class], &numIvars);
NSString *key=nil;
for(int i = 0; i < numIvars; i++) {
    
    Ivar thisIvar = vars[i];
    key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
    NSLog(@"variable_name :%@", key);
}
free(vars);

輸出了400+行截取一下吧

2019-12-08 22:34:28.229672+0800 Example1[3815:169799] method_getName:_cnui_applyContactStyle
2019-12-08 22:34:28.229768+0800 Example1[3815:169799] method_getName:ab_text
2019-12-08 22:34:28.229854+0800 Example1[3815:169799] method_getName:setAb_text:
2019-12-08 22:34:28.229937+0800 Example1[3815:169799] method_getName:ab_textAttributes
2019-12-08 22:34:28.230014+0800 Example1[3815:169799] method_getName:setAb_textAttributes:
2019-12-08 22:34:28.230091+0800 Example1[3815:169799] method_getName:isLayoutSizeDependentOnPerpendicularAxis
2019-12-08 22:34:28.230170+0800 Example1[3815:169799] method_getName:_nui_additionalInsetsForBaselines
2019-12-08 22:34:28.230239+0800 Example1[3815:169799] method_getName:respondsToSelector:
2019-12-08 22:34:28.230309+0800 Example1[3815:169799] method_getName:.cxx_destruct
2019-12-08 22:34:28.230388+0800 Example1[3815:169799] method_getName:dealloc
2019-12-08 22:34:28.230460+0800 Example1[3815:169799] method_getName:encodeWithCoder:
2019-12-08 22:34:28.230534+0800 Example1[3815:169799] method_getName:initWithCoder:

……
2019-12-08 22:34:28.507996+0800 Example1[3815:169799] variable_name :_textStorage
2019-12-08 22:34:28.508084+0800 Example1[3815:169799] variable_name :_textContainer
2019-12-08 22:34:28.508349+0800 Example1[3815:169799] variable_name :_layoutManager
2019-12-08 22:34:28.508607+0800 Example1[3815:169799] variable_name :_containerView
2019-12-08 22:34:28.508853+0800 Example1[3815:169799] variable_name :_layoutView
2019-12-08 22:34:28.509114+0800 Example1[3815:169799] variable_name :_inputDelegate
2019-12-08 22:34:28.509537+0800 Example1[3815:169799] variable_name :_tokenizer
2019-12-08 22:34:28.509824+0800 Example1[3815:169799] variable_name :_inputController
2019-12-08 22:34:28.510100+0800 Example1[3815:169799] variable_name :_interactionAssistant
2019-12-08 22:34:28.510367+0800 Example1[3815:169799] variable_name :_textInputTraits
2019-12-08 22:34:28.510639+0800 Example1[3815:169799] variable_name :_autoscroll
2019-12-08 22:34:28.510907+0800 Example1[3815:169799] variable_name :_tvFlags
2019-12-08 22:34:28.511168+0800 Example1[3815:169799] variable_name :_contentSizeUpdateSeqNo
2019-12-08 22:34:28.511435+0800 Example1[3815:169799] variable_name :_scrollTarget
2019-12-08 22:34:28.511709+0800 Example1[3815:169799] variable_name :_scrollPositionDontRecordCount
2019-12-08 22:34:28.511986+0800 Example1[3815:169799] variable_name :_scrollPosition

我試了一下之前用的KVOTest類(lèi):

2019-12-08 22:37:43.043772+0800 Example1[3830:171932] method_getName:countOfNames
2019-12-08 22:37:43.043867+0800 Example1[3830:171932] method_getName:objectInNamesAtIndex:
2019-12-08 22:37:43.043951+0800 Example1[3830:171932] method_getName:init
2019-12-08 22:37:43.044030+0800 Example1[3830:171932] method_getName:setValue:forUndefinedKey:
2019-12-08 22:37:43.044113+0800 Example1[3830:171932] method_getName:setNilValueForKey:
2019-12-08 22:37:43.044187+0800 Example1[3830:171932] method_getName:setLastName:
2019-12-08 22:37:43.044263+0800 Example1[3830:171932] method_getName:validateValue:forKey:error:
2019-12-08 22:37:43.044343+0800 Example1[3830:171932] variable_name :isName

可以看到真的是只要是里面的方法都輸出了,無(wú)論是不是對(duì)外,屬性也是都有~ 父類(lèi)的應(yīng)該都不會(huì),畢竟NSObject也很多方法,這里也都沒(méi)輸出。


參考:
http://www.lxweimin.com/p/b9f020a8b4c9
http://www.lxweimin.com/p/797a99f30ff9

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評(píng)論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,740評(píng)論 3 420
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,856評(píng)論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,175評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,931評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,321評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,533評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,082評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,891評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,319評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,732評(píng)論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,987評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,794評(píng)論 3 394
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,076評(píng)論 2 375