iOS KVC/KVO小結(jié)


本文對 KVC、KVO 相關(guān)知識進(jìn)行全面的整理總結(jié),介紹了相關(guān)的基本概念、使用方法、注意事項、實現(xiàn)原理等。后續(xù)如有更深的理解會繼續(xù)整理總結(jié)。

簡介

KVC ( Key-value coding 鍵值編碼 ) 是一種由 NSKeyValueCoding 非正式協(xié)議啟用的機(jī)制,對象采用該機(jī)制提供對其屬性的間接訪問。當(dāng)對象符合鍵值編碼時,通過字符串名稱訪問對象屬性。
鍵值編碼的機(jī)制也是其他 Cocoa 框架的基礎(chǔ),例如 KVO。

KVO ( Key-value observing 鍵值觀察 ) 這一機(jī)制基于 NSKeyValueObserving 非正式協(xié)議,Cocoa 通過這個協(xié)議為所有遵守協(xié)議的對象提供了一種自動化的屬性觀察能力。對目標(biāo)對象的某屬性添加觀察,當(dāng)該屬性發(fā)生變化時,通過觸發(fā)觀察者對象實現(xiàn)的 KVO 接口方法,來通知觀察者。KVO 是 Cocoa 框架使用觀察者模式的一種途徑。

KVC

基本使用方法

KVC 提供了簡潔的方法,來訪問對象屬性。

- (nullable id)valueForKey:(NSString *)key;  
- (void)setValue:(nullable id)value forKey:(NSString *)key;

上面兩個方法,分別是對應(yīng)于 getter 訪問器的 valueForKey: 和對應(yīng)于 setter 訪問器的 setValue:forKey: 。

  • valueForKey: 首先查找以鍵 -getKey、 -key 或 -isKey 命名的 getter 方法。如果不存在 getter 方法(假設(shè)沒有通過@synthesize提供存取方法),它將在對象內(nèi)部查找名為 _key 或 key 的實例變量。如果最后沒找調(diào)用 valueForUndefinedKey: 方法。
  • setValue:forKey: 首先查找以鍵 -setKey、 -_setKey 命名的 setter 方法,如果不存在 setter 方法,它將在類中查找名為 _key 或 key 的實例變量。如果最后沒找到則調(diào)用 setValue:forUndefinedKey: 方法。

例如某對象有屬性 name、age,我們就可使用上面兩個方法進(jìn)行訪問、設(shè)置屬性值。

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

[obj setValue:@"jone" forKey:@"name"];
[obj setValue:@(10) forKey:@"age"];
id stAge = [obj valueForKey:@"age"];

對于屬性是基本的數(shù)據(jù)類型時 (int, CGFloat) 是放入 NSNumber 或 NSValue 中來設(shè)置的。
相比直接訪問,KVC的效率會稍低一點,所以只有當(dāng)你非常需要它提供的可擴(kuò)展性時才使用它。

其他使用方法

1、屬性的屬性的訪問
KVC 還提供了訪問屬性的屬性的操作方法:

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

例如下面兩個類:

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end

@interface Teacher : NSObject
@property (nonatomic, strong) Student *student;
@end

//路徑訪問
[teacher setValue:@"haha" forKeyPath:@"student.name"];
id name = [teacher valueForKeyPath:@"student.name"];

2、多值訪問
同時訪問多個屬性的方法:

[obj setValuesForKeysWithDictionary:@{@"name":@"Tom", @"age":@(2)}];
NSDictionary *values = [obj dictionaryWithValuesForKeys:@[@"name",@"age"]];

3、集合屬性
KVC 同樣適用于集合對象,可以通過 valueForKey: 和 setValue:forKey:(或它們的鍵路徑方式)獲取或設(shè)置集合對象。

//@property (nonatomic, copy) NSArray<Student *> *students;
id array = [teacher valueForKeyPath:@"students.name"];
//返回數(shù)組,包含屬性 student.name

KVC 還提供了接口 mutableArrayValueForKey:、 mutableSetValueForKey: 來操作集合類型的屬性。

//@property (nonatomic, copy) NSArray *items;

obj.items = @[@"a", @"b", @"c"];
NSMutableArray *items = [obj mutableArrayValueForKey:@"items"];
[items addObject:@"d"];
//添加后,同時也改變了 obj.items

4、運(yùn)算符
運(yùn)算符是一個特殊的 Key Path,可以作為參數(shù)傳遞給 valueForKeyPath:方法,注意只能是這個方法,如果傳給了valueForKey:方法會崩潰。
運(yùn)算符是一個以@開頭的特殊字符串:

  • 簡單集合運(yùn)算符有 @avg,@count,@max,@min,@sum
  • 對象運(yùn)算符,比集合運(yùn)算符稍微復(fù)雜,能以數(shù)組的方式返回指定的內(nèi)容,有兩種 @distinctUnionOfObjects、@unionOfObjects ,前者會去除重復(fù)的以后返回,后者直接返回。
  • Array和Set操作符,這種情況更復(fù)雜了,說的是集合中包含集合的情況,有三種 @distinctUnionOfArrays、@unionOfArrays、@distinctUnionOfSets,前兩個針對的集合是Arrays,后一個針對的集合是Sets。因為Sets中的元素本身就是唯一的,所以沒有對應(yīng)的 @unionOfSets 運(yùn)算符。
NSNumber *value = [teacher valueForKeyPath:@"students.@max.age"];
NSNumber *count = [teacher valueForKeyPath:@"students.@count"];

 NSArray * array = [teacher valueForKeyPath:@"students.@distinctUnionOfObjects.age"];
 
 NSMutableArray *someStudents = [NSMutableArray array];
[someStudents addObject:@[st0, st1, st2]];
[someStudents addObject:@[st3, st4]];
id value = [someStudents valueForKeyPath:@"@distinctUnionOfArrays.age"];

異常情況

1、找不到對應(yīng)的 key
當(dāng)調(diào)用 setValue:forKey: 或者 valueForKey: 找不到對應(yīng) key 命名的屬性時,就會 NSUnknownKeyException 異常崩潰,可以在對象里重寫下面兩個方法,防止崩潰。

- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
}

2、將不是對象類型的屬性設(shè)置為 nil
將對象賦值為 nil 這是可以的相當(dāng)于把對象置空。但是當(dāng)使用 setValue:forKey: 將非對象類型的屬性值( int、CGFloat、結(jié)構(gòu)體等),設(shè)置為 nil 時會 NSInvalidArgumentException 異常崩潰。我們可以重寫方法 setNilValueForKey: 處理設(shè)置為 nil 的情況:

//@property (nonatomic, assign) int age;
//[obj setValue:nil forKey:@"age"];

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@"age"];
    } else {
        [super setNilValueForKey:key];
    }
}


KVO

KVO 是 Cocoa 框架使用觀察者模式的一種途徑。 KVC 是 KVO 技術(shù)實現(xiàn)的基礎(chǔ) ,參與 KVO 的對象需要符合 KVC 的要求和存取方法,也可以手動實現(xiàn)觀察者通知。

使用方法

1、添加觀察:

[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)self];
  • options 回調(diào)選項,NSKeyValueObservingOptionOld 表示獲取舊值,NSKeyValueObservingOptionNew 表示獲取新值,NSKeyValueObservingOptionInitial 表示在添加觀察的時候就立馬響應(yīng)一個回調(diào),NSKeyValueObservingOptionPrior 表示在被觀察屬性變化前后都回調(diào)一次。

  • context 可以是 C 指針或者一個對象引用,既可以當(dāng)作一個唯一的標(biāo)識來分辨被觀察的變更,也可以向觀察者提供數(shù)據(jù)。

2、觀察回調(diào):

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"observe  key = %@, obj = %@, change = %@", keyPath, object, change);
}

/*
observe  key = name, obj = <Student: 0x6000016c6e80>, change = {
    kind = 1;
    new = Tony;
    old = Tom;
}
*/

change 是一個字典,對應(yīng)的鍵有:NSKeyValueChangeKindKey、NSKeyValueChangeNewKey、NSKeyValueChangeOldKey、NSKeyValueChangeIndexesKey、NSKeyValueChangeNotificationIsPriorKey。

NSKeyValueChangeKindKey 指明了變更類型,設(shè)置、插入、移除、替換:

enum {
   NSKeyValueChangeSetting = 1,
   NSKeyValueChangeInsertion = 2,
   NSKeyValueChangeRemoval = 3,
   NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

NSKeyValueChangeNotificationIsPriorKey 指明是變更前或變更后,觸發(fā)的回調(diào)。

3、移除觀察:

[obj removeObserver:self forKeyPath:@"name"];

移除觀察,和移除通知比較類似,需要在不用繼續(xù)觀察的時候移除它,比如在控制器的 dealloc 方法里面釋放,注意重復(fù)移除會 crash。

4、調(diào)試 KVO
可以打斷點,在 lldb 中查看被觀察對象的所有觀察信息。

lldb po [obj observationInfo]

這會打印出有關(guān)誰觀察誰之類的很多信息。

KVO 兼容

有兩種方法可以保證變更通知被發(fā)出。自動發(fā)送通知是 NSObject 提供的,并且一個類中的所有屬性都默認(rèn)支持,只要是符合 KVC 的。
手動變更通知需要些額外的代碼,但也對通知發(fā)送提供了額外的控制??梢酝ㄟ^重寫子類 automaticallyNotifiesObserversForKey: 方法的方式控制子類一些屬性的自動通知。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

- (void)setName:(NSString *)name {
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = [name copy];
        [self didChangeValueForKey:@"name"];
    }
}

//如果一個操作導(dǎo)致多個鍵變化,需要嵌套變更通知
- (void)setLastName:(NSString *)lastName {
    [self willChangeValueForKey:@"lastName"];
    [self willChangeValueForKey:@"fullName"];
    _lastName = [lastName copy];
    _fullName = [NSString stringWithFormat:@"Title %@", lastName];
    [self didChangeValueForKey:@"fullName"];
    [self didChangeValueForKey:@"lastName"];
}

當(dāng)觀察某個對象的集合屬性時,當(dāng)直接使用 obj.mutableArray 添加、刪除、替換元素時,不會觸發(fā)觀察回調(diào),需要手動添加代碼 willChange:valuesAtIndexes:forKey: 和 didChange:valuesAtIndexes:forKey: 來通知集合屬性發(fā)生了變化?;蛘呤褂?KVC 來操作集合屬性。如下例子:

//某類集合屬性
//@property (nonatomic, strong) NSMutableArray *myArray;

//添加觀察
[obj addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

//變更集合時,手動通知觀察者
[self.obj willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:0] forKey:@"myArray"];
[self.obj.myArray removeObjectAtIndex:0];
[self.obj didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:0] forKey:@"myArray"];

//或者使用 KVC 操作集合,會自動通知觀察者
NSMutableArray *array = [self.obj mutableArrayValueForKey:@"myArray"];
[array removeObjectAtIndex:0];

注冊從屬鍵

某些情況下,一個屬性的值取決于另一個對象中的一個或多個其他屬性的值。如果一個屬性的值發(fā)生更改,則還應(yīng)標(biāo)記派生屬性的值以進(jìn)行更改。
例如,fullName 取決于 firstName 和 lastName。當(dāng) firstName 或 lastName 發(fā)生改變時,必須通知觀察 fullName 屬性的程序,因為它們影響這個屬性的值。
重寫 keyPathsForValuesAffectingValueForKey 來指定 fullName 屬性依賴于lastName和firstName。

- (void)setLastName:(NSString *)lastName {
    _lastName = [lastName copy];
    _fullName = [NSString stringWithFormat:@"%@ %@", _firstName, lastName];
}

//重寫指定 fullName 屬性依賴于lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

//或者重寫
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

基本原理

KVO 是基于 runtime 運(yùn)行時來實現(xiàn)的,當(dāng)你觀察了某個對象的屬性,內(nèi)部會生成一個該對象所屬類的子類,中間類,然后重寫被觀察屬性的 setter 方法,當(dāng)然在重寫的方法中會調(diào)用父類的 setter 方法從而不會影響框架使用者的邏輯,之后會將該對象的 isa 指針指向新創(chuàng)建的這個類,最后會重寫 -(Class)class; 方法,讓使用者通過 [obj class] 查看當(dāng)前對象所屬類的時候會返回其父類,使其看似沒有改變什么,讓你覺得不需要添加額外的代碼,就能使用 KVO。
Apple 并不希望過多暴露 KVO 的實現(xiàn)細(xì)節(jié)。想要深究實現(xiàn)細(xì)節(jié),可查看文章
下面例子,通過 object_getClass() 方法查看觀察前后的變化。

//object_getClass 獲取 isa 指針指向的對象
//object_setClass 更改對象的 isa 指針指向。將對象設(shè)置為別的類類型,返回原來的Class
NSLog(@"1 --- %p %@ %p", obj, object_getClass(obj), [obj methodForSelector:@selector(setName:)]);

[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:(__bridge void *)self];

NSLog(@"2 --- %p %@ %p", obj, object_getClass(obj), [obj methodForSelector:@selector(setName:)]);


[self.obj removeObserver:self forKeyPath:@"name"];
NSLog(@"3 --- %p %@ %p", self.obj, object_getClass(self.obj), [self.obj methodForSelector:@selector(setName:)]);

//1 --- 0x60000225cea0  AStudent 0x1067f72a0
//2 --- 0x60000225cea0  NSKVONotifying_AStudent 0x7fff258e454b
//3 --- 0x60000225cea0  AStudent 0x1067f72a0

通過上面的打印結(jié)果可知,添加觀察后,原本的類變成了 NSKVONotifying_AStudent,移除觀察后又變回去了, setName: 方法也發(fā)生了變化。


References

KVC/KVO原理詳解及編程指南
Objective-C中的KVC和KVO
透徹理解 KVO 觀察者模式

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

友情鏈接更多精彩內(nèi)容