KVO即key-value-observing,鍵值觀察赦颇,是一種觀察者模式的實現(xiàn)機制(另一種為Notification)芥玉。KVO提供了一種機制锹淌,指定一個被觀察對象(如student對象),當被觀察對象student的某個屬性(如name)發(fā)生改變時觀察者對象(如teacher)會收到通知垄琐,同時作出相應處理(這孩子边酒,怎么能隨便改名呢,把你家長叫來)狸窘。
1.KVO的應用步驟
- 注冊觀察者墩朦,實施監(jiān)聽:
[student addObserver:teacher forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
參數(shù)說明:
1)student:被觀察者對象
2)teacher:觀察者對象,該對象必須實現(xiàn)
observeValueForKeyPath:ofObject:change:context: 方法
3)forKeyPath:被觀察者的屬性名翻擒,必須和被觀察者屬性名相同
4)options:屬性配置氓涣,有四個值:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
//接收方法中傳入屬性變化后的新值,鍵為NSKeyValueChangeNewKey
NSKeyValueObservingOptionNew = 0x01,
//接收方法中傳入屬性變化前的舊值韭寸,鍵為NSKeyValueChangeOldKey
NSKeyValueObservingOptionOld = 0x02,
//注冊之后會立刻調用接收方法春哨,可以在程序第一次運行時做一些初始化操作,
如果配置了NSKeyValueObservingOptionNew恩伺,change參數(shù)內容會包含新值
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
//接收方法會在屬性變化前后分別調用一次赴背,變化前的通知change參數(shù)包含鍵值對:notificationIsPrior = 1。
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
}
5)context:接收一個C指針,可以為kvo的回調方法傳值凰荚。
- 在回調方法處理屬性變化
每當監(jiān)聽的keypath發(fā)生改變就會調用該方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == &PrivateKVOContext) {
if ([keyPath isEqualToString:@"name"]) {
NSString *oldName = change[NSKeyValueChangeOldKey];
NSString *newName = change[NSKeyValueChangeNewKey];
}
}
}
參數(shù)說明:
1)keyPath:被監(jiān)聽的keyPath , 用來區(qū)分不同的KVO監(jiān)聽燃观;
2)object:被觀察對象,可以獲得修改后的屬性值便瑟;
3)change:保存信息改變的字典(可能有舊的值缆毁,新的值等)
默認change參數(shù)會包含一個NSKeyValueChangeKindKey鍵值對,傳遞被監(jiān)聽 屬性的變化類型:
enum {
//屬性被重新設置
NSKeyValueChangeSetting = 1,
//表示更改的是集合屬性到涂,分別代表插入脊框、刪除、替換操作
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
如果NSKeyValueChangeKindKey參數(shù)是針對集合屬性的三個之一践啄,change 參數(shù)還會包含一個NSKeyValueChangeIndexesKey鍵值對浇雹,表示變化的index。
4)context:上下文屿讽,可以用來區(qū)分不同的KVO監(jiān)聽昭灵。
- 移除觀察者
因為添加觀察者并不會retain,所以即使被觀察者被釋放了伐谈,其監(jiān)聽信息仍舊存在烂完,如果不將觀察者移除就會出現(xiàn)崩潰。
[student removeObserver:teacher forKeyPath:@"name" context:&PrivateKVOContext];
2.KVO的簡單應用實例
KVO的常用場景是在MVC中同步model和UI诵棵,實現(xiàn)這樣的需求:點擊view的時候更新model的(person)數(shù)據(jù)并觸發(fā)UI同步抠蚣。可以看到應用KVO輕松的監(jiān)聽到模型數(shù)據(jù)的變化非春,進而在回調中更新UI柱徙。
@interface ViewController ()
- (IBAction)randomAge:(UIButton *)sender;
@property (weak, nonatomic) IBOutlet UILabel *ageLabel;
@property (strong, nonatomic) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc]init];
//創(chuàng)建觀察者
[self.person addObserver:self
forKeyPath:@"myAge"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//UI同步
self.ageLabel.text = [NSString stringWithFormat:@"%@",change[NSKeyValueChangeNewKey]];
}
- (IBAction)randomAge:(UIButton *)sender {
//更新model數(shù)據(jù)
self.person.myAge = arc4random() % 100 ;
}
- (void)dealloc {
//移除觀察者
[self removeObserver:self forKeyPath:@"myAge"];
}
@end
3.手動鍵值觀察
在以上KVO的應用中通過創(chuàng)建觀察者,在屬性變化時就會自動發(fā)出通知奇昙,而有些場景需要人為的控制通知的發(fā)送护侮,這需要重寫被觀察者對象屬性的getter/setter方法。
#import "Person.h"
@implementation Person
{
NSUInteger myAge;
}
- (NSUInteger)myAge {
return myAge;
}
- (void)setMyAge:(NSUInteger)newAge {
//發(fā)送通知:鍵值即將改變
[self willChangeValueForKey:@"myAge"];
myAge = newAge;
//發(fā)送通知:鍵值已經修改
[self didChangeValueForKey:@"myAge"];
}
//當設置鍵值之后储耐,通過此方法羊初,決定是否發(fā)送通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
//當key為myAge時,手動發(fā)送通知
if ([key isEqualToString:@"myAge"]) {
return NO;
}
//當為其他key時什湘,自動發(fā)送通知
return [super automaticallyNotifiesObserversForKey:key];
}
@end
4.設置屬性之間的依賴
假設我要監(jiān)聽一個color屬性的變化长赞,而一個color又與三原色相關,每種原色又由不同的組分構成闽撤,如此我們就需要監(jiān)聽N個原色屬性的變化得哆,每個原色屬性變化就去設置color的值,我的天哟旗,太麻煩了贩据,事情總是有解決的辦法:KVO給我們提供了這種鍵之間的依賴方法
+ (NSSet *)keyPathsForValuesAffecting<Key>;
//設置屬性依賴栋操,屬性greenComponent,依賴于屬性lComponent和屬性aComponent
+ (NSSet *)keyPathsForValuesAffectingGreenComponent {
return [NSSet setWithObjects:@"lComponent", @"aComponent",nil];
}
+ (NSSet *)keyPathsForValuesAffectingRedComponent {
return [NSSet setWithObject:@"lComponent"];
}
+ (NSSet *)keyPathsForValuesAffectingBlueComponent {
return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}
+ (NSSet *)keyPathsForValuesAffectingColor {
return [NSSet setWithObjects:@"redComponent", @"greenComponent",@"blueComponent", nil];
}
通過color的屬性依賴設置饱亮,在原色組分lComponent矾芙、aComponent、bComponent發(fā)生變化時觀察者仍能收到color變化的通知近上。
5.KVO機制
前面已經了解到KVO的基本用法剔宪,那么KVO底層是如何實現(xiàn)的呢?Let me think壹无,既然KVO能夠監(jiān)聽屬性的變化葱绒,那么在被觀察者屬性的set方法中判斷如果屬性值發(fā)生了變化就向觀察者發(fā)送通知就可以實現(xiàn),似乎很簡單格遭,但事實是KVO并沒有對被觀察者進行顯示地重寫set方法哈街,那應該在哪重寫set方法呢留瞳?
蘋果通過isa混寫(isa-swizzling)來實現(xiàn)KVO拒迅。當創(chuàng)建觀察者觀察一個對象person時,KVO機制會動態(tài)的創(chuàng)建一個名為NSKVONotifying_Person的新類(繼承自person的本類Person)并將對象person的isa指針從Person類指向NSKVONotifying_Person(在這里只需要知道對象isa指針指向哪個類她倘,對象就會到哪個類去尋找對應方法)璧微。到這里我們應該找到了重寫set方法的地方,只要在NSKVONotifying_Person中重寫了Person的對應觀察屬性name的set方法硬梁,在被觀察對象person的屬性name被修改的時候會調用NSKVONotifying_Person中該屬性的set方法前硫,在set方法中完成相應的通知工作從而實現(xiàn)了屬性變化的監(jiān)聽。由此可以知道只有當屬性值是通過set方法修改的時KVO才有效荧止,例如在Person中有這樣一個方法:
//不是通過set方法修改的屹电,不會觸發(fā)KVO
- (void)changeName {
_name = @"newName";
}
KVO測試:
//輔助代碼
static NSArray *ClassMethodNames(Class c) {
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for (i = 0; i < methodCount; i++) {
[array addObject:NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
static void PrintDescription(NSString *name, id obj) {
struct objc_object *objcet = (__bridge struct objc_object *)obj;
Class cls = objcet->isa;
NSString *str = [NSString stringWithFormat:@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s : super class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(cls),
class_getName(class_getSuperclass(cls)),
[ClassMethodNames(cls) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
// 測試代碼
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
[person2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
PrintDescription(@"person1", person1);
PrintDescription(@"person2", person2);
//輸出
person1: <Person: 0x60800002fec0>
NSObject class Person
libobjc class Person : super class NSObject
implements methods <.cxx_destruct, name, setName:>
person2: <Person: 0x60800002fee0>
NSObject class Person
libobjc class NSKVONotifying_Person : super class Person
implements methods <setName:, class, dealloc, _isKVOA>
可以看到被觀察者對像person2的類已經被改為NSKVONotifying_Person了,并且重寫了setName方法(在set方法里實現(xiàn)通知)跃巡。此外當我們試著打印person2對象的類([person2 class])時危号,仍然是Person。所以在NSKVONotifying_Person中除了重寫相應的setter素邪,還重寫了class方法外莲,讓它返回原先的類。
PS: I am xinghun who is on the road.