編輯:關於Android編程
KVO是一套當目標對象的屬性值改變時觀察者對象能夠接受到通知的機制。必須先理解KVC才能更好的理解KVO,前者是後者的實現基礎。這樣的通信機制在MVC設計模式很是常見
實現過程簡單來說分為3步:
1、添加觀察這和監測對象
2、監測對象改變
3、收到值改變通知,處理後續邏輯
舉個生活中的例子就是給銀行卡開通短信通知的業務,總體也是分3步“
1、去銀行辦理短信業務
2、賬號財產變動
3、收到短信通知
KVO是框架級別的服務,無需自己發送通知,使用方便,基本不需要添加額外代碼即可使用。
為了使用KVO,必須滿足以下3步
1、目標對象的屬性,必須支持KVO
2、注冊觀察者與被觀察者addObserver:forKeyPath:options:context:
3、觀察者必須實現
observeValueForKeyPath:ofObject:change:context:方法
第一步、確保目標支持KVO
被監測的目標對象的屬性支持KVO必須滿足以下條件:
1、目標對象的屬性必須支持KVC,對於1對1屬性簡單來說就是實現set和get方法。詳情和1對多請閱讀官方說明。系統已有類及子類自動支持,放心使用。
2、自動和手動屬性通知
目標對象必須能發出屬性變化通知。系統默認支持,也可自定義。
系統默認支持,且支持的很好,一般無需自定義。
//如果需要自定義,需要重新此方法,默認返回YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
//在set方法中手動調用,變化類型只是針對NSKeyValueChangeSetting
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
例如
//假設有屬性
@property (nonatomic,copy)NSString *name;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
/* 只自定義指定屬性,其它仍然自動發送通知 */
if ([theKey isEqualToString:@"name"])
{
//在set方法中手動調用相關方法
automatic = NO;
}
else
{
//此方法默認返回YES
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
- (void)setName:(NSString *)name
{
//即將變化
[self willChangeValueForKey:@"name"];
_name = name;
//已經變化
[self didChangeValueForKey:@"name"];
}
//如果說只有值不相等時才發送通知,提升性能
- (void)setName:(NSString *)name
{
if (![name isEqualToString:_name])
{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
如果涉及1對多的容器類,需要自己實現 NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement三種操作對應的方法,例如
//Keys為屬性名稱
- (void)removeKeysAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"keys"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"keys"];
}
3、屬性依賴
如果目標對象屬性存在依賴關系,注冊合適的依賴Keys。核心方法為
第一種、
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key NS_AVAILABLE(10_5, 2_0);
說明:
1、返回目標屬性依賴屬性的KeyPath的Set。當對象注冊後,KVO自動監測該對象所有的KeyPaths。
2、其默認實現從對象所屬類的方法列表中匹配方法:+keyPathsForValuesAffecting(為屬性名,比如Name),如果存在執行並返回結果;如果不存在向底層尋找已經廢棄的方法+setKeys:triggerChangeNotificationsForDependentKey:
3、可以用來替換手動調用-willChangeValueForKey:/-didChangeValueForKey:來實現屬性依賴的解決方案
4、不能在已有類的Category中使用,在Category禁止重寫此方法,可以使用+keyPathsForValuesAffecting來實現。
第二種、
或者重寫此格式+keyPathsForValuesAffecting(為屬性名,比如Name)的方法名
比如說,姓名=姓+名;當二者任一變動時更新姓名
@property (nonatomic,copy)NSString *name;//姓名
@property (nonatomic,copy)NSString *firstName;//姓
@property (nonatomic,copy)NSString *lastName;//名
//重新get方法,表明字段組成
-(NSString *)name
{
return [NSString stringWithFormat:@"%@%@",_firstName,_lastName];
}
//通過此方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"name"])
{
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
或者
+ (NSSet *)keyPathsForValuesAffectingName
{
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
//改變值
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:@"張" forKey:@"firstName"];
[self setValue:@"三" forKey:@"lastName"];
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//名稱改變,刷新姓名
[self setValue:@"龍" forKey:@"lastName"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
}
//輸出結果
2016-09-06 17:05:01.904 KVC[4192:307124] NSKeyValueChangeOldKey:張三
2016-09-06 17:05:01.904 KVC[4192:307124] NSKeyValueChangeNewKey:張龍
注意:以上關於屬性依賴的處理方法不支持一對多的關系。比如說ViewController對象有一個totalNumber表示總數和屬性datas數組,數組中Data的對象,對象含有number屬性。
/* Data對象 */
@interface Data : NSObject
@property (nonatomic,assign)NSInteger number;
@end
/* ViewController對象 */
@interface ViewController ()
@property (nonatomic,assign)NSInteger totalNumber;
@property (nonatomic,strong)NSArray *datas;
@end
可以采用以下方法解決
1、注冊每個Data對象的年齡屬性為監測屬性,ViewController對象為觀察者,data.number變化時,使用集合運算符求和,然後設置ViewController的totalNumber屬性值
- (void)viewDidLoad {
[super viewDidLoad];
/* 創建Data對象 */
Data * data1 = [[Data alloc] init];
data1.number = 0;
Data *data2 = [[Data alloc] init];
data2.number = 0;
Data *data3 = [[Data alloc] init];
data3.number = 0;
/* self.datas屬性賦值 */
[self setValue:@[data1,data2,data3] forKey:@"datas"];
/* 監測self.datas中每個data對象的number屬性 */
//(0, 3) 中0表示index從0開始,0表示長度3,也就是index(0、1、2);但是不能使得self.datas數組越界
NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 3)];
[self.datas addObserver:self toObjectsAtIndexes:set forKeyPath:@"number" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
/* 監測totalNumber屬性 */
[self addObserver:self forKeyPath:@"totalNumber" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
/* 改變值,查看輸出結果 */
[data1 setValue:@"1" forKey:@"number"];
[data2 setValue:@"1" forKey:@"number"];
[data3 setValue:@"1" forKey:@"number"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"number"])
{
[self updateTotalNumber];
}
else if ([keyPath isEqualToString:@"totalNumber"])
{
NSLog(@"%@--%@",keyPath,change);
}
}
//更新總數
- (void)updateTotalNumber
{
NSNumber *total = [self valueForKeyPath:@"[email protected]"];
[self setValue:total forKey:@"totalNumber"];
}
//輸出結果
2016-09-07 14:10:10.694 KVC[3034:165515] totalNumber--{
kind = 1;
new = 1;
old = 0;
}
2016-09-07 14:10:10.695 KVC[3034:165515] totalNumber--{
kind = 1;
new = 2;
old = 1;
}
2016-09-07 14:10:10.695 KVC[3034:165515] totalNumber--{
kind = 1;
new = 3;
old = 2;
}
以上代碼中不涉及移除,根據需要添加代碼,對象delloc之前,必須移除。
2、Core Data,自動實現類似的功能。
第二步、注冊
1、注冊通知
為了能夠獲取目標屬性值改變的通知,需要注冊觀察者和觀察對象屬性
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
參數說明:
observer:觀察者對象,就是想收到變動通知的對象
keyPath:監測的目標屬性的路徑
options:決定了通知中內容和發送時間
context:C指針或者對象,傳遞參數,一般不用傳NULL
例如在新建的工程的ViewController中
@property (nonatomic,copy)NSString *name;
- (void)registerAsObserver
{
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
注意:此方法不持有觀察者對象、被觀察對象、context,管理好其生命周期。
2、接受通知
當監測的目標對象的屬性變化時,觀察者將調用observeValueForKeyPath:ofObject:change:context: message,所有的觀察者都必須實現此方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;
keyPath:監測的目標屬性的路徑
object:觀察者對象
change:變化內容
context:C指針或者對象,傳遞參數,一般不用傳NULL
3、移除通知
當不再使用時,需要通過以下方法移除通知。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
keyPath:監測的目標屬性的路徑
observer:觀察者對象
context:C指針或者對象,傳遞參數,一般不用傳NULL
以上兩個方法,根據需要選擇使用。
特別注意:NSArray、NSOrderedSet、NSSet不支持以上三個方法,調用會拋出異常。
第三步、屬性變化
使用KVC方法,或者能夠觸發KVC方法使得監測的目標對象屬性變化。
第四步、接收變化
當監測的目標對象的屬性變化時,觀察者將調用
observeValueForKeyPath:ofObject:change:context: message,所有的觀察者都必須實現此方法。在此方法中處理變化
以上第二、三、四步組成一次完整的KVO使用過程,下邊關於一些參數的用法說明
參數說明
關於NSKeyValueObservingOptions
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
options:決定了通知中內容和發送時間
NSKeyValueObservingOptions是一個枚舉類型
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
/*通知dic中是否包含新值*/
NSKeyValueObservingOptionNew = 0x01,
/*通知dic中是否包含新值*/
NSKeyValueObservingOptionOld = 0x02,
/*添加此操作,通知dic中是否包含注冊通知前的初始值;如果目標屬性是容器類,每個元素都會觸發通知發送*/
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
/*添加此操作,每次值變化,將觸發兩次:1、變化前(dic中包含NSKeyValueChangeNotificationIsPriorKey,值為1,NSNumber類型)
2、變化後
*/
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
};
關於Change Dictionary
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;
change:變化內容
其包含以下幾種內容,可以使用以下字段取值
//值變化類型
FOUNDATION_EXPORT NSString *const NSKeyValueChangeKindKey;
//新值
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNewKey;
//舊值
FOUNDATION_EXPORT NSString *const NSKeyValueChangeOldKey;
//容器類中,變化值所在位置,NSIndexSet類型
FOUNDATION_EXPORT NSString *const NSKeyValueChangeIndexesKey;
//是否值變化前,NSNumber類型
FOUNDATION_EXPORT NSString *const NSKeyValueChangeNotificationIsPriorKey NS_AVAILABLE(10_5, 2_0);
其中NSKeyValueChangeKindKey有以下幾種類型
typedef NS_ENUM(NSUInteger, NSKeyValueChange)
{
NSKeyValueChangeSetting = 1,//值變化
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替換
};
上邊的NSKeyValueChangeKindKey2、3、4分別對應著有序集合比如NSArray中增、刪、改操作。
參數說明-代碼示例
1、NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
@property (nonatomic,copy)NSString *name;
//注冊
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:@"zwq" forKey:@"name"];
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[self setValue:@"zwq2" forKey:@"name"];
}
//接收
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"change dic:%@",change);
}
//輸出結果
2016-09-06 14:58:55.349 KVC[3614:231751] NSKeyValueChangeKindKey:1
2016-09-06 14:58:55.351 KVC[3614:231751] NSKeyValueChangeOldKey:zwq
2016-09-06 14:58:55.351 KVC[3614:231751] NSKeyValueChangeNewKey:zwq2
2016-09-06 14:58:55.352 KVC[3614:231751] change dic:{
kind = 1;
new = zwq2;
old = zwq;
}
從以上代碼可以看出change中的key如何取值,以及其中內容。
2、NSKeyValueObservingOptionInitial
@property (nonatomic,copy)NSString *name;
//注冊
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:@"zwq" forKey:@"name"];
//為了明顯觀察值變化,多添加兩個key
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
[self setValue:@"zwq2" forKey:@"name"];
}
//接收
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"change dic:%@",change);
}
//輸出結果
2016-09-06 15:06:49.963 KVC[3654:237675] NSKeyValueChangeKindKey:1
2016-09-06 15:06:49.964 KVC[3654:237675] NSKeyValueChangeOldKey:(null)
2016-09-06 15:06:49.964 KVC[3654:237675] NSKeyValueChangeNewKey:zwq
2016-09-06 15:06:49.964 KVC[3654:237675] change dic:{
kind = 1;
new = zwq;
}
2016-09-06 15:06:49.964 KVC[3654:237675] NSKeyValueChangeKindKey:1
2016-09-06 15:06:49.965 KVC[3654:237675] NSKeyValueChangeOldKey:zwq
2016-09-06 15:06:49.965 KVC[3654:237675] NSKeyValueChangeNewKey:zwq2
2016-09-06 15:06:49.965 KVC[3654:237675] change dic:{
kind = 1;
new = zwq2;
old = zwq;
}
對比1、2的輸出結果,可以看出2中可以獲得初始值,後續值變化接收到的通知dic內容同1中一樣(可以多改變幾次賦值,觀察結果)
3、NSKeyValueObservingOptionPrior
@property (nonatomic,copy)NSString *name;
//注冊
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:@"zwq" forKey:@"name"];
//為了明顯觀察值變化,多添加兩個key
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionPrior|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
[self setValue:@"zwq2" forKey:@"name"];
[self setValue:@"zwq3" forKey:@"name"];
}
//接收
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"NSKeyValueChangeKindKey:%@",change[NSKeyValueChangeKindKey]);
NSLog(@"NSKeyValueChangeOldKey:%@",change[NSKeyValueChangeOldKey]);
NSLog(@"NSKeyValueChangeNewKey:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"NSKeyValueChangeNotificationIsPriorKey:%@",change[NSKeyValueChangeNotificationIsPriorKey]);
NSLog(@"change dic:%@",change);
}
//輸出結果
2016-09-06 15:17:16.325 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.326 KVC[3730:246941] NSKeyValueChangeOldKey:zwq
2016-09-06 15:17:16.326 KVC[3730:246941] NSKeyValueChangeNewKey:(null)
2016-09-06 15:17:16.326 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:1
2016-09-06 15:17:16.327 KVC[3730:246941] change dic:{
kind = 1;
notificationIsPrior = 1;
old = zwq;
}
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeOldKey:zwq
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeNewKey:zwq2
2016-09-06 15:17:16.327 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:(null)
2016-09-06 15:17:16.328 KVC[3730:246941] change dic:{
kind = 1;
new = zwq2;
old = zwq;
}
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeOldKey:zwq2
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeNewKey:(null)
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:1
2016-09-06 15:17:16.328 KVC[3730:246941] change dic:{
kind = 1;
notificationIsPrior = 1;
old = zwq2;
}
2016-09-06 15:17:16.328 KVC[3730:246941] NSKeyValueChangeKindKey:1
2016-09-06 15:17:16.329 KVC[3730:246941] NSKeyValueChangeOldKey:zwq2
2016-09-06 15:17:16.329 KVC[3730:246941] NSKeyValueChangeNewKey:zwq3
2016-09-06 15:17:16.329 KVC[3730:246941] NSKeyValueChangeNotificationIsPriorKey:(null)
2016-09-06 15:17:16.329 KVC[3730:246941] change dic:{
kind = 1;
new = zwq3;
old = zwq2;
}
從以上代碼輸出結果不難看出:a、通知分開發送:變化前和變化後 b、變化前NSKeyValueChangeNewKey為空,但是NSKeyValueChangeNotificationIsPriorKey值為1;變化後反之。
結合以上3段代碼結論:4種各有所用,可以單獨使用,也可以組合使用,根據需要選擇合適。作用簡單概括:1.新、舊值 2.初始值 3.值變化前後。
4、NSKeyValueChangeKindKey
@interface ViewController ()
@property (nonatomic,strong)NSArray *datas;
@end
- (void)viewDidLoad {
[super viewDidLoad];
/* 創建Data對象 */
Data * data1 = [[Data alloc] init];
Data *data2 = [[Data alloc] init];
Data *data3 = [[Data alloc] init];
Data *data4 = [[Data alloc] init];
/* self.datas屬性賦值 */
[self setValue:@[data1,data2,data3] forKey:@"datas"];
/* 監測self.datas屬性 */
[self addObserver:self forKeyPath:@"datas" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//修改datas數組:增刪改
NSMutableArray *mutable_subdatas = [self mutableArrayValueForKeyPath:@"datas"];
[mutable_subdatas addObject:data4];
[mutable_subdatas removeObject:data4];
[mutable_subdatas replaceObjectAtIndex:2 withObject:data4];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@--%@",keyPath,change);
}
//輸出結果
2016-09-07 14:46:50.366 KVC[3231:186568] datas--{
indexes = "<_NSCachedIndexSet: 0x7f8591428c60>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
kind = 2;
new = (
""
);
}
2016-09-07 14:46:50.367 KVC[3231:186568] datas--{
indexes = "<_NSCachedIndexSet: 0x7f8591428c60>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
kind = 3;
old = (
""
);
}
2016-09-07 14:46:50.367 KVC[3231:186568] datas--{
indexes = "<_NSCachedIndexSet: 0x7f8591428c40>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
kind = 4;
new = (
""
);
old = (
""
);
}
如果我們在每個xml文件中都把相同的布局都重寫一遍,一個是代碼冗余,可讀性很差;另一個是修改起來比較麻煩,對後期的修改和維護非常不利
※效果 ※使用方法 package com.fancyy.calendarweight; import java.util.ArrayList; import j
Java文件大小轉換工具類 (B,KB,MB,GB,TB,PB之間的大小轉換) 有時候要做出如下所示的展示文件大小的效果時候,需要對文件大小進行轉換,然後再進行相關的代碼
最近做項目,碰到如下的需求:ViewPager分頁,如果是6頁(包括6頁)就用圓點,如果是6頁以上就用進度條來切換。前面一種交互方法最常見,用小圓點來表示當前選中的頁面,