概述
KVC:鍵值編碼昭伸,使用字符串的方式管理對象的成員、屬性
KVO:鍵值監(jiān)聽,一種觀察者模式顶籽,監(jiān)聽屬性的改變抑胎,可實現(xiàn)UI和數(shù)據(jù)模型的分離,基于KVC像捶。
鍵值編碼KVC(NSKeyValueCoding)
作用:動態(tài)管理對象的成員變量讀寫操作。
KVC的操作方法有是由 NSKeyValueCoding
協(xié)議提供,NSObject 實現(xiàn)了這個協(xié)議沉眶,這意味著OC中幾乎所有的對象都支持KVC操作。
使用方式:
- 簡單路徑
設(shè)值:[obj setValue:值 forKey:變量名]
取值:[obj valueForKey:變量名]
- 復(fù)合路徑
設(shè)值:[obj setValue:值 forKeyPath:變量路徑]
取值:[obj valueForKeyPath:變量路徑]
示例:
我們定義一個Person類杉适,并聲明一個name的屬性
Person.h文件
@interface Person : NSObject
@property (copy ,nonatomic) NSString *name;
@end
我們在主程序中使用KVC來控制name的取谎倔、設(shè)值操作
Person *person = [Person new];
[person setValue:@"LOLITA" forKey:@"name"];
NSLog(@"-->%@",[person valueForKey:@"name"]);
KVC不僅可以設(shè)置person的屬性,person的成員變量也可以操作猿推,不管是公有還是私有片习。
我們給Person類新增成員變量
Person.h
@interface Person : NSObject
{
@private
NSString *_sex;
@public
CGFloat _height;
}
@property (copy ,nonatomic) NSString *name;
@end
主程序中
Person *person = [Person new];
[person setValue:@"fale" forKey:@"sex"];
NSLog(@"-->%@",[person valueForKey:@"sex"]);
[person setValue:@"170.0" forKey:@"_height"];
NSLog(@"-->%@",[person valueForKey:@"height"]);
我們可以看到,Person的成員變量是 _sex
和 _height
,設(shè)值和取值的時候是否帶"_"效果都是一樣的藕咏,這跟KVC設(shè)置的機制有關(guān)状知。
- 設(shè)值:優(yōu)先尋找
setter
方法,如果沒有該方法則尋找成員變量_a
侈离,如果仍然不存在试幽,則尋找成員變量a
,如果還是沒找到則會調(diào)用這個類的setValue:forUndefinedKey:
方法卦碾,并且不管這些方法铺坞、成員變量是私有還是公有的甚至是只讀的都可以正確設(shè)置
優(yōu)先級為:setter方法--> _a --> a --> setValue:forUndefinedKey: 方法
- 取值:優(yōu)先尋找
getter
方法,如果沒有找到該方法則尋找成員變量_a
洲胖,如果仍然不存在济榨,則尋找成員變量a
,如果還是沒有找到則會待用這個類valueforUndefinedKey:
方法
優(yōu)先級為:getter方法--> _a --> a --> valueforUndefinedKey: 方法
復(fù)合路徑
如果Person中有一個Accont類绿映,表示賬戶余額擒滑,要怎么使用KVC呢?
@interface Account : NSObject
{
float _balance; // 賬戶余額
}
@end
@interface Person : NSObject
@property (strong ,nonatomic) Account *account; // 賬戶余額
@end
主程序中
Person *person = [Person new];
[person setValue:@"1234.6" forKeyPath:@"account.balance"];
NSLog(@"-->%@",[person valueForKeyPath:@"account.balance"]);
鍵值監(jiān)聽KVO(NSKeyValueObserving)
作用:實現(xiàn)UI和數(shù)據(jù)模型的分離
KVO是一種觀察者模式叉弦,可以監(jiān)聽某對象的屬性值的變化丐一,當該屬性值發(fā)生變化時,作為監(jiān)聽者就可以做出相應(yīng)的響應(yīng)動作淹冰,利用這一模式库车,我們可以在MVC模式下實現(xiàn)Model和View的之間的通信,即當Model發(fā)生變化時樱拴,UI作為觀察者就可以發(fā)生相應(yīng)變化柠衍。
使用步驟:
- 注冊成觀察者
- 重寫監(jiān)聽回調(diào)方法
- 注銷觀察者
示例:
這里使用控制器作為觀察者,觀察某個模型的屬性來演示KVO的使用晶乔。
首先我們創(chuàng)建一個項目珍坊,并新建一個數(shù)據(jù)模型
@interface DataModel : NSObject
{
NSString *_title;
}
@end
步驟一:將控制器注冊為該模型的觀察者
self.model = [DataModel new];
[self.model addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil]; // 注冊為觀察者
步驟二:重寫KVO的監(jiān)聽回調(diào)
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"title"]&&object==self.model) {
self.label.text = [change objectForKey:@"new"];
}
else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]
}
}
步驟三:手動注銷觀察者
-(void)dealloc{
[self.model removeObserver:self forKeyPath:@"title"];
}
這樣,當數(shù)據(jù)模型發(fā)生變化時正罢,我們就可以監(jiān)聽到阵漏,并作UI上的改變了
// 改變數(shù)據(jù)模型title的值
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.module setValue:@"a new value" forKey:@"title"];
});
運行結(jié)果
??注:不是所有的賦值方式都支持 KVO 監(jiān)聽。如果你直接通過成員變量進行賦值操作是無法監(jiān)聽到的翻具。
// 通過變量直接賦值 (不可以)
_title = @"hahahaha";
// 通過調(diào)用 setter 方法賦值(可以)
self.title = @"aaaa";
// 通過 KVC 賦值 (可以)
[self setValue:@"xxxxx" forKey:@"title"];
上述三種賦值方式袱饭,只有后兩種才會被監(jiān)聽到,這和 KVO 實現(xiàn)的底層原理有關(guān)呛占。
KVO 的優(yōu)化
系統(tǒng)提供的 KVO 使用方式有一些不便之處。一個是所有的變化回調(diào)都會經(jīng)過方法 -observeValueForKeyPath:ofObject:change:change context:
懦趋,這也就意味著需要集中處理所有的屬性監(jiān)聽變化晾虑,我們需要通過 keyPath
、object
甚至是 context
來落地何種處理方式,另一就是需要手動移除帜篇,這個對新手不是非常的友好糙捺,因為如果處理不當,經(jīng)常會發(fā)生意想不到的問題笙隙。
那么針對上述兩個不便之處進行一定的優(yōu)化洪灯。我的思路是如下。
- 轉(zhuǎn)移監(jiān)聽者
將監(jiān)聽權(quán)利移交給其他對象竟痰,包括移除的部分签钩。
- 事件回調(diào)
監(jiān)聽對象不應(yīng)處理具體業(yè)務(wù),通過回調(diào)將事件交給外部處理坏快。使用回調(diào)的另一個好處就是形成了一對一的情況铅檩,即:一個監(jiān)聽對象一個事件回調(diào),這樣避免了集中處理的窘境莽鸿。
根據(jù)上述思路昧旨,我的優(yōu)化代碼如下。
@class LLKVOHandle;
@interface LLKVO : NSObject
/// 返回值值需要被持有
+(LLKVOHandle*)addObserverTo:(NSObject*)obj forKey:(NSString*)keyPath block:(void(^)(NSDictionary*changes))block;
@end
/// KVO 的幾個必要數(shù)據(jù)
@interface LLKVOHandle : NSObject
/// 被觀察者
@property (nonatomic, strong) NSObject* target;
/// 觀察屬性
@property (nonatomic, copy) NSString* keyPath;
/// 屬性變化的回調(diào)
@property (nonatomic, copy) void (^block)(NSDictionary*);
/// 構(gòu)造器
-(instancetype)initWithTarget:(NSObject*)target keyPath:(NSString*)keyPath;
@end
@implementation LLKVO
+(LLKVOHandle*)addObserverTo:(NSObject*)obj forKey:(NSString*)keyPath block:(void (^)(NSDictionary *))block{
LLKVOHandle* handle = [[LLKVOHandle alloc] initWithTarget:obj keyPath:keyPath];
handle.block = block;
return handle;
}
@end
@implementation LLKVOHandle
/// 構(gòu)造器
-(instancetype)initWithTarget:(NSObject*)target keyPath:(NSString*)keyPath{
if (self = [super init]) {
self.target = target;
self.keyPath = keyPath;
// 在當前類中進行監(jiān)聽
[target addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
}
return self;
}
// 處理 KVO 的事件
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (self.block) {
self.block(change); // 將事件拋出
}
}
/// 管理觀察者和被觀察者祥得,這里進行了移除清理操作
-(void)removeObserver{
[self.target removeObserver:self forKeyPath:self.keyPath context:nil];
self.target = nil;
}
/// 釋放之后自動移除觀察者
-(void)dealloc{
[self removeObserver];
}
@end
其中兔沃,LLKVOHandle
就是新的監(jiān)聽對象,它需要做的就是添加監(jiān)聽事件级及,完成事件回調(diào)乒疏,移除清理。LLKVO
則是便捷入口创千。
那么缰雇,優(yōu)化過后的代碼,在使用時就會向下面這樣追驴。
self.kvoHandle = [LLKVO addObserverTo:self.model forKey:@"title" block:^(NSDictionary *changes) {
// 處理屬性變化
NSLog(@"%@", changes);
}];
需要注意一點的就是 block 的內(nèi)存泄漏問題械哟,需要弱引用。
KVO 的實現(xiàn)原理
KVO 的實現(xiàn)依賴于OC強大的運行時 Runtime
殿雪。
原理:
當觀察某對象時暇咆,KVO動態(tài)創(chuàng)建該對象的子類,將原始類和子類的屬性設(shè)置setter
方法進行交互丙曙,并重寫子類被觀察屬性 setter
方法爸业,隨后通知觀察者該屬性的變化狀況。
因此運行時主要做了三件事亏镰。
- 創(chuàng)建子類
- 方法交換
- 通知觀察者
運行時在 KVO 機制中扮演了黑客的身份扯旷,截獲了用戶的信息,稍加改造之后繼續(xù)發(fā)出去索抓,但同時將用戶的信息解析之后販賣給了別人(監(jiān)聽者)钧忽。
實現(xiàn)過程:
Apple使用方法交換(isa-swizzling)來實現(xiàn)KVO毯炮。
當觀察對象A時,KVO動態(tài)創(chuàng)建了新的名為 NSKVONotifying_A
的新類耸黑,該類是對象A的子類桃煎,并且使用 swizzling
交換了所觀察的屬性的 setter
方法,KVO重寫了新類的觀察屬性的 setter
方法大刊,在調(diào)用原類中的 setter
方法后为迈,通知所有觀察者該屬性的變化情況。
- NSKVONotifying_A
每個對象內(nèi)部都有 isa
指針缺菌,這個指針指向該對象的類葫辐,在KVO機制中,該isa
指針被修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_A
男翰,那么當被觀察者修改被檢測的屬性的值時候另患,就會調(diào)用KVO重寫的setter
方法,從而激活鍵值通知機制蛾绎,實現(xiàn)當前類屬性改變的監(jiān)聽昆箕。
所以當我們從應(yīng)用層面上來看,并沒有意識到有新類的出現(xiàn)租冠,這是apple隱瞞類對KVO的底層實現(xiàn)過程鹏倘,而我們還以為是原來的類,但是此時如果我們創(chuàng)建一個新的名為 NSKVONotifying_A
的類時顽爹,就會發(fā)現(xiàn)系統(tǒng)運行到注冊KVO的那段代碼時纤泵,程序發(fā)生崩潰,因為系統(tǒng)在注冊監(jiān)聽的時候動態(tài)創(chuàng)建了名為NSKVONotifying_A
的中間類镜粤,并指向這個中間類了捏题。
- 子類重寫
setter
方法
KVO的鍵值觀察通知依賴于NSObject的兩個方法:-willChangeValueForKey:
和 didChangeValueForKey:
,在存取數(shù)值的前后分別調(diào)用2個方法肉渴;
被觀察屬性發(fā)生改變之前公荧,-willChangeValueForKey:
被調(diào)用,通知系統(tǒng)該keyPath的屬性值即將變更同规;當改變發(fā)生后循狰,-didChangeValueForKey:
被調(diào)用,通知系統(tǒng)keyPath的屬性值已經(jīng)發(fā)生改變券勺,之后绪钥,observeValueForKey:ofObject:change:context:
也會被調(diào)用。
注意:重寫觀察屬性的setter方法這種繼承方式的注入是在運行時而不是編譯時實現(xiàn)的
KVO 機制中需要借助重寫 setter
方法关炼,因此這也解釋了為什么直接使用變量進行賦值無法觸發(fā) KVO 通知程腹。