KVO基本使用
KVO全名Key Value Observing,監(jiān)聽(tīng)屬性的改變。
首先來(lái)看一下KVO的基本用法。
定義DPLPerson類,添加age屬性。
// DPLPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface DPLPerson : NSObject
@property (nonatomic, assign) int age;
@end
NS_ASSUME_NONNULL_END
// DPLPerson.m
#import "DPLPerson.h"
@implementation DPLPerson
@end
創(chuàng)建兩個(gè)DPLPerson實(shí)例,為person1的age屬性添加屬性監(jiān)聽(tīng)。
self.person1 = [[DPLPerson alloc] init];
self.person1.age = 1;
self.person2 = [[DPLPerson alloc] init];
self.person2.age = 2;
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
點(diǎn)擊屏幕時(shí),改變兩個(gè)實(shí)例的age屬性值。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 11;
self.person2.age = 22;
}
實(shí)現(xiàn)KVO監(jiān)聽(tīng)方法。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監(jiān)聽(tīng)到%@的%@屬性發(fā)生了改變 --- %@", object, keyPath, change);
}
在對(duì)象釋放時(shí),將監(jiān)聽(tīng)移除
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
當(dāng)我們點(diǎn)擊屏幕時(shí),控制打印。
監(jiān)聽(tīng)到<DPLPerson: 0x6000031f2cd0>的age屬性發(fā)生了改變 --- {
kind = 1;
new = 11;
old = 1;
}
以上就是KVO的基本使用。
KVO底層實(shí)現(xiàn)
KVO時(shí)怎樣監(jiān)聽(tīng)屬性值改變的呢?
我們同時(shí)修改了person1和person2的age屬性,只有person1的age屬性值的改變觸發(fā)了KVO,我們來(lái)看一下添加監(jiān)聽(tīng)前后,兩個(gè)實(shí)例對(duì)象發(fā)生了什么變化。
在添加監(jiān)聽(tīng)前后分別打印一下person1和person2的類對(duì)象。
NSLog(@"添加KVO之前 --- %@ %@", object_getClass(self.person1), object_getClass(self.person2));
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"添加KVO之后 --- %@ %@", object_getClass(self.person1), object_getClass(self.person2));
打印
添加KVO之前的類 --- person1:DPLPerson, person2:DPLPerson
添加KVO之后的類 --- person1:NSKVONotifying_DPLPerson, person2:DPLPerson
可以看到,為person1添加監(jiān)聽(tīng)之后,person1的類對(duì)象變成了NSKVONotifying_DPLPerson,這并不是我們創(chuàng)建的,而是系統(tǒng)在運(yùn)行時(shí)為DPLPerson創(chuàng)建的子類,可以通過(guò)之前文章中提到的方式,來(lái)驗(yàn)證一下NSKVONotifying_DPLPerson和DPLPerson的關(guān)系。
struct dpl_objc_class {
Class isa;
Class superclass;
};
NSLog(@"添加KVO之后的地址 --- person1Class:%p, person1SuperClss:%p, person2Class:%p", object_getClass(self.person1), person1SuperClass->superclass, object_getClass(self.person2));
控制臺(tái)打印如下
添加KVO之后的地址 --- person1Class:0x600000379dd0, person1SuperClss:0x10be3f068, person2Class:0x10be3f068
可以看到person1的類對(duì)象,也就是NSKVONotifying_DPLPerson類對(duì)象的superclass指針,指向的就是DPLPerson類的地址,也就是說(shuō)NSKVONotifying_DPLPerson繼承自DPLPerson類。
我們知道,為屬性賦值實(shí)際上就是調(diào)用-set:方法,在為person1設(shè)置屬性監(jiān)聽(tīng)之后,person1的isa指針發(fā)生了變化,不再指向DPLPerson類,而是指向了NSKVONotifying_DPLPerson類。
也就是說(shuō),如果在NSKVONotifying_DPLPerson類中,重寫(xiě)-setAge:方法,并在-setAge:方法中調(diào)用監(jiān)聽(tīng)方法-observeValueForKeyPath:ofObject:change:context:方法就可以實(shí)現(xiàn)對(duì)屬性的監(jiān)聽(tīng)。
我們可以通過(guò)代碼來(lái)驗(yàn)證一下。使用運(yùn)行時(shí)函數(shù),打印NSKVONotifying_DPLPerson類中的方法。
// 打印類中的方法
- (void)printMethodsWithClass:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"%@", methodName);
}
free(methodList);
}
// 在設(shè)置KVO之后調(diào)用
[self printMethodsWithClass:object_getClass(self.person1)];
打印
setAge:
class
dealloc
_isKVOA
可以看到NSKVONotifying_DPLPerson類確實(shí)重寫(xiě)了-setAge:方法。
我們可以打印出NSKVONotifying_DPLPerson類中-setAge:的方法指針,看一下它里面具體調(diào)用了什么方法。
NSLog(@"添加KVO之后 --- %p", [self.person1 methodForSelector:@selector(setAge:)]);
NSLog(@"斷點(diǎn)需要");
在斷點(diǎn)需要處打個(gè)斷點(diǎn),控制臺(tái)打印方法的IMP(方法)指針。
2019-02-25 18:20:55.092328+0800 kvo01[2456:1339986] 添加KVO之后 --- 0x104ceacf2
(lldb) p (IMP)0x104ceacf2
(IMP) $0 = 0x0000000104ceacf2 (Foundation`_NSSetIntValueAndNotify)
可以看到-setAge方法中調(diào)用了Foundtion框架中的_NSSetIntValueAndNotify方法。
Foundtion框架中_NSSetXXXValueAndNotify系列方法,是實(shí)現(xiàn)KVO對(duì)關(guān)鍵。
其中XXX取決于被監(jiān)聽(tīng)屬性的類型。本例中age為int類型,所以會(huì)調(diào)用_NSSetIntValueAndNotify方法;如果被監(jiān)聽(tīng)屬性為Double類型,那么將會(huì)調(diào)用_NSSetDoubleValueAndNotify方法,以此類推。
NSKVONotifying_DPLPerson內(nèi)部setAge方法實(shí)現(xiàn)
這里我們可以給出NSKVONotifying_DPLPerson內(nèi)部的大概實(shí)現(xiàn)的偽代碼
#import "NSKVONotifying_DPLPerson.h"
@implementation NSKVONotifying_DPLPerson
- (void)setAge:(int)age {
// 調(diào)用Foundtion中_NSSetXXXValueAndNotify系列方法
_NSSetIntValueAndNotify(age);
}
void _NSSetIntValueAndNotify(int age) {
// 先調(diào)用對(duì)象的willChangeValueForKey
[self willChangeValueForKey:@"age"];
// 調(diào)用父類的set方法,修改屬性值
[super setAge:age];
// 修改屬性值之后,調(diào)用對(duì)象的didChangeValueForKey
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 調(diào)用-observeValueForKeyPath:ofObject:change:context:方法,通知屬性改變
[observer observeValueForKeyPath:key ofObject:self change:change context:context];
}
@end
NSKVONotifying_DPLPerson內(nèi)部其他方法實(shí)現(xiàn)
從上面的打印可以看到,NSKVONotifying_DPLPerson內(nèi)部除了重寫(xiě)了父類的set方法,還重寫(xiě)了class方法,并且實(shí)現(xiàn)了dealloc和_isKVOA方法。
在添加監(jiān)聽(tīng)之后,調(diào)用class方法來(lái)看一下person1的類型
NSLog(@"添加KVO之后 --- %@", [self.person1 class]);
打印
添加KVO之后 --- DPLPerson
person1的isa指針明明已經(jīng)指向的是NSKVONotifying_DPLPerson,而class方法卻依然返回DPLPerson,可見(jiàn)其內(nèi)部重寫(xiě)了class方法,并且返回其父類。
這樣做了原因就是為了隱藏NSKVONotifying_DPLPerson類的存在,讓開(kāi)發(fā)者忽略這個(gè)類的存在。
dealloc用于處理運(yùn)行時(shí)創(chuàng)建這個(gè)類的銷毀。
_isKVOA返回YES
手動(dòng)觸發(fā)KVO
有時(shí)候我們不改變屬性值,依然想要觸發(fā)KVO監(jiān)聽(tīng),那么我們?cè)撛趺醋瞿兀?/p>
前面已經(jīng)給出了整個(gè)KVO底層的實(shí)現(xiàn)
- 運(yùn)行時(shí)創(chuàng)建子類
- 重寫(xiě)
set方法,調(diào)用_NSSetXXXValueAndNotify - willChangeValueForKey
- 調(diào)用父類set方法為屬性賦值
- didChangeValueForKey
- 調(diào)用監(jiān)聽(tīng)方法
想要手動(dòng)出發(fā)KVO,只要調(diào)用willChangeValueForKey和didChangeValueForKey這兩個(gè)方法就可以了。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
}
當(dāng)我們點(diǎn)擊屏幕時(shí),即使沒(méi)有修改屬性值,同樣會(huì)觸發(fā)監(jiān)聽(tīng)方法。
監(jiān)聽(tīng)到<DPLPerson: 0x6000028b2f70>的age屬性發(fā)生了改變 --- {
kind = 1;
new = 1;
old = 1;
}
