目錄
一砾淌、什么是KVO
KVO
是基于KVC
的啦撮,全稱是Key-Value-Observer
鍵值觀察者。KVO
提供一種機(jī)制汪厨,指定一個(gè)被觀察的對(duì)象(A類)赃春,當(dāng)對(duì)象某個(gè)屬性(A中的屬性name
)發(fā)生更改時(shí),對(duì)象會(huì)獲得通知劫乱,并作出相應(yīng)處理织中;【且不需要給被觀察的對(duì)象添加任何額外代碼,就能使用KVO
機(jī)制】衷戈。KVO
是Objective-C
對(duì)觀察者設(shè)計(jì)模式的一種實(shí)現(xiàn)狭吼。
KVO
在MVC
設(shè)計(jì)架構(gòu)下的項(xiàng)目很適合實(shí)現(xiàn)Model
模型和View
視圖之間的通訊。
例如:代碼中殖妇,在模型類M創(chuàng)建屬性數(shù)據(jù)刁笙,在控制器中創(chuàng)建觀察者,一旦屬性數(shù)據(jù)發(fā)生改變觀察者就會(huì)收到通知,然后刷新相應(yīng)的視圖疲吸。
官方文檔:Key-Value Observing Programming Guide
二座每、KVO基本使用
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.person = [NAPerson new];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"differ";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
CJLog(@"%@",change[NSKeyValueChangeNewKey]);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
}
1、context的作用
當(dāng)前觀察者觀察了多個(gè)對(duì)象摘悴,當(dāng)這些對(duì)象中有同名的KeyPath
時(shí)可以用來(lái)區(qū)分是哪個(gè)context
下的KeyPath
尺栖。eg:
static void *PersonNameContext = &PersonNameContext;
static void *AnimalNameContext = &AnimalNameContext;
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
2、是否有必要移除觀察者
有必要烦租。官方文檔提示如果沒(méi)移除觀察者在某些情況下會(huì)出現(xiàn)NSRangeException
異常崩潰。
3除盏、手動(dòng)開(kāi)啟KVO
默認(rèn)情況下KVO
都是自動(dòng)開(kāi)啟的叉橱,但是我們可實(shí)現(xiàn)手動(dòng)開(kāi)關(guān)KVO
@implementation NAPerson
// 自動(dòng)開(kāi)關(guān)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
@end
4、集合類型的觀察
[self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:NULL];
//[self.person.dataArray addObject:@"1"];不起作用
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
三者蠕、KVO實(shí)現(xiàn)原理
@interface NAPerson : NSObject {
@public
NSString *_nickName;
}
@end
// ViewController
[self.person addObserver:self forKeyPath:@"_nickName" options:NSKeyValueObservingOptionNew context:NULL];
self.person->_nickName = @"differ";
[self.person removeObserver:self forKeyPath:@"_nickName" context:NULL];
通過(guò)對(duì)成員變量_nickName
添加KVO
觀察后可以發(fā)現(xiàn):KVO
不會(huì)對(duì)成員變量進(jìn)行觀察窃祝,只對(duì)屬性進(jìn)行觀察。由此可猜想KVO
和屬性的setter
進(jìn)行了關(guān)聯(lián)踱侣。
通過(guò)官方文檔可知粪小,KVO
會(huì)產(chǎn)生一個(gè)中間類,將觀察對(duì)象的isa
指向了這個(gè)中間類抡句。斷點(diǎn)調(diào)試如下:
KVO產(chǎn)生的中間類是分類還是派生子類探膊?
修改代碼遍歷KVO
觀察對(duì)象的類以及子類
- (void)viewDidLoad {
[super viewDidLoad];
[self printClasses:[NAPerson class]];
self.person = [NAPerson new];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self printClasses:[NAPerson class]];
}
#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls {
// 注冊(cè)類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個(gè)數(shù)組, 其中包含給定對(duì)象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 獲取所有已注冊(cè)的類
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
從控制臺(tái)輸出的情況可以看出KVO
生成的中間類是別被觀察對(duì)象類的派生子類待榔。
KVO產(chǎn)生的中間類是繼承還是重寫(xiě)屬性的setter方法
打印出NSKVONotifying_NAPerson
類中的方法
[self printClassAllMethod:objc_getClass("NSKVONotifying_NAPerson")];
#pragma mark - 遍歷當(dāng)前類的方法
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
輸出:
setName:-0x1807930a4
class-0x180791b78
dealloc-0x1807918fc
_isKVOA-0x1807918f4
由于NSKVONotifying_NAPerson
類中存在setName
方法逞壁,因此KVO子類
重寫(xiě)了觀察屬性的setter
方法。
對(duì)象的isa什么時(shí)候指回NAPerson
在移除觀察者前后斷點(diǎn)打印類名可知移除觀察者后被觀察對(duì)象的isa
指回了原類
盡管移除了觀察者锐锣,但是KVO
產(chǎn)生的派生子類并不會(huì)移除腌闯,依然存在于內(nèi)存中。因?yàn)楫?dāng)前界面銷毀后通過(guò)上一個(gè)界面打印NAPerson
類以及子類(printClasses:
)依然會(huì)出現(xiàn)NSKVONotifying_NAPerson
雕憔。
KVO產(chǎn)生的派生子類重寫(xiě)的setter做了什么操作
添加斷點(diǎn):
修改name進(jìn)入斷點(diǎn):
進(jìn)入?yún)R編中可以看到調(diào)用父類setName:
方法前后調(diào)用了NSKeyValueWillChange姿骏、NSKeyValueDidChange
四、總結(jié)
基本原理:
-
KVO
是基于Runtime
機(jī)制實(shí)現(xiàn)的 - 當(dāng)某個(gè)類的對(duì)象屬性第一次被觀察時(shí)斤彼,系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類分瘦,在這個(gè)派生類中重寫(xiě)基類中任何被觀察屬性的
setter
方法。派生類在被重寫(xiě)的setter
方法內(nèi)實(shí)現(xiàn)真正的通知機(jī)制 - 如果原類為
Person
畅卓,那么生成的派生類名為NSKVONotifying_Person
- 每個(gè)類對(duì)象中都有一個(gè)
isa
指針指向當(dāng)前類擅腰,當(dāng)一個(gè)類對(duì)象的第一次被觀察,那么系統(tǒng)就會(huì)偷偷將isa
指針指向動(dòng)態(tài)生成的派生類翁潘,從而在給被監(jiān)控屬性賦值是執(zhí)行的是派生類的setter
方法 - 鍵值觀察通知依賴于
NSObject
的兩個(gè)方法:willChangeValueForKey:
和didChangeValueForKey:
,在一個(gè)被觀察屬性發(fā)生改變之前趁冈,willChangeValueForKey:
一定會(huì)被調(diào)用,這就會(huì)記錄舊的值。而當(dāng)改變發(fā)生后渗勘,didChangeValueForKey:
會(huì)被調(diào)用沐绒,繼而observeValueForKey:ofObject:change:context:
也會(huì)被調(diào)用
關(guān)于KVO中間類,有如下說(shuō)明:
- 實(shí)例對(duì)象
isa
的指向在注冊(cè)KVO
觀察者之后旺坠,由原有類更改為指向中間類 - 中間類重寫(xiě)了觀察屬性的
setter
方法乔遮、class、dealloc取刃、_isKVOA
方法 -
dealloc
方法中蹋肮,移除KVO
觀察者之后,實(shí)例對(duì)象isa
指向由中間類更改為原類 - 中間類從創(chuàng)建后璧疗,就一直存在內(nèi)存中坯辩,不會(huì)被銷毀
KVO為子類的觀察者屬性重寫(xiě)的setter方法的工作原理相當(dāng)于:
- ( void)setName:( NSString *)name {
[self willChangeValueForKey: @"name"];
[super setValue:name forKey: @"name"]; //調(diào)用父類的存取方法
[self didChangeValueForKey: @"name"];
}
特點(diǎn):
觀察者觀察的是屬性,只有遵循
KVO
變更屬性值的方式才會(huì)執(zhí)行KVO
的回調(diào)方法崩侠,例如是否執(zhí)行了setter
方法漆魔、或者是否使用了KVC
賦值。如果賦值沒(méi)有通過(guò)
setter
方法或者KVC
却音,而是直接修改屬性對(duì)應(yīng)的成員變量改抡,例如:僅調(diào)用_name = @"newName"
,這時(shí)是不會(huì)觸發(fā)KVO
機(jī)制系瓢,更加不會(huì)調(diào)用回調(diào)方法的阿纤。所以使用
KVO
機(jī)制的前提是遵循KVO
的屬性賦值方式來(lái)變更屬性值。