KVO
文章目录
- KVO
- KVO概念
- KVO使用步骤
- 注册KVO监听
- KVO监听实现
- 移除KVO监听
- KVO基本用法
- KVO传值
- 禁止KVO的方法
- 使用注意事项
- KVO原理
- GSKVOInfo
- GSKVOPathInfo
- GSKVOObservation
- 为什么要重写class方法呢?
- GSKVOReplacement
- GSKVOBase
- GSKVOBase
- 小结
- 源码实现
- 移除观察者
- 总结
KVO概念
KVO是一种开发模式,它的全称是Key-Value Observing (观察者模式) 是苹果Fundation框架下提供的一种开发机制,使用KVO,可以方便地对指定对象的某个属性进行观察,当属性发生变化时,进行通知,告诉开发者属性旧值和新值对应的内容
KVO使用步骤
注册KVO监听
通过[addObserver:forKeyPath:options:context:]
方法注册KVO,这样可以接收到keyPath属性的变化事件;
observer
:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context:
方法。keyPath
:要观察的属性名称。要和属性声明的名称一致。options
:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容context
:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。
KVO监听实现
通过方法[observeValueForKeyPath:ofObject:change:context:]
实现KVO的监听;
keyPath
:被观察对象的属性object
:被观察的对象change
:字典,存放相关的值,根据options传入的枚举来返回新值旧值context
:注册观察者的时候,context传递过来的值
移除KVO监听
在不需要监听的时候,通过方法[removeObserver:forKeyPath:]
,移除监听;
KVO基本用法
我们对这个Button的背景色做监听,点击button改变按钮的背景色,当背景色改变的时候,打印改变前和改变后的背景色。
//KVO最基本的使用
self.view.backgroundColor = [UIColor whiteColor];
self.kvoButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.kvoButton.frame = CGRectMake(100, 100, 100, 100);
self.kvoButton.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.kvoButton];
[self.kvoButton addTarget:self action:@selector(press) forControlEvents:UIControlEventTouchUpInside];
//给所要监听的对象注册监听
[self.kvoButton addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- (void)press {
//改变被监听对象的值
[self.kvoButton setValue:[UIColor colorWithRed:arc4random() % 255 / 255.0 green:arc4random() % 255 / 255.0 blue:arc4random() % 250 / 250.0 alpha:1] forKey:@"backgroundColor"];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//打印监听结果
if ([keyPath isEqual:@"backgroundColor"]) {
NSLog(@"old value is: %@", [change objectForKey:@"old"]);
NSLog(@"new value is: %@", [change objectForKey:@"new"]);
}
}
我们点击一次button:
KVO传值
KVO传值也很简单,可以理解为我们对第二个viewController的某一个属性做一个监听,当我们跳转到第一个viewController的时候就可以监听到值的改变。
//FirstViewController
- (void)pressChuanZhi {
SecondViewController *secondViewController = [[SecondViewController alloc] init];
secondViewController.modalPresentationStyle = UIModalPresentationFullScreen;
//为试图中的属性注册一个监听事件
[secondViewController addObserver:self forKeyPath:@"content" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self presentViewController:secondViewController animated:YES completion:nil];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqual:@"content"]) {
id value = [change objectForKey:@"new"];
self.chuanzhiLabel.text = value;
}
}
//SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
self.backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.backButton.frame = CGRectMake(100, 100, 100, 100);
self.backButton.backgroundColor = [UIColor blueColor];
[self.backButton addTarget:self action:@selector(pressBack) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.backButton];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 250, 200, 50)];
self.textField.keyboardType = UIKeyboardTypeDefault;
self.textField.borderStyle = UITextBorderStyleRoundedRect;
[self.view addSubview:self.textField];
}
- (void)pressBack {
self.content = self.textField.text;
[self dismissViewControllerAnimated:YES completion:nil];
}
禁止KVO的方法
//返回NO禁止KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"content"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
使用注意事项
- 调用
[removeObserver:forKeyPath:]
需要在观察者消失之前,否则会导致Crash
。 - 在调用
addObserver
方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
。 - 观察者需要实现
observeValueForKeyPath:ofObject:change:context:
方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash
。 - KVO的
addObserver
和removeObserver
需要是成对的,如果重复remove
则会导致NSRangeException
类型的Crash
,如果忘记remove
则会在观察者释放后再次接收到KVO回调时Crash
。 - 在调用KVO时需要传入一个
keyPath
,由于keyPath
是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash
。我们可以利用系统的反射机制将keyPath
反射出来,这样编译器可以在@selector()
中进行合法性检查。
KVO原理
在分析KVO的内部实现之前,先来分析一下KVO的存储结构,主要用到了以下几个类:
GSKVOInfo
GSKVOPathInfo
GSKVOObservation
GSKVOInfo
KVO是基于NSObject类别实现的非正式协议,所以所有继承于NSObject的类都可以使用KVO,其中可以通过- (void*) observationInfo方法获取对象相关联的所有KVO信息,而返回值就是GSKVOInfo,源码如下
@interface GSKVOInfo : NSObject
{
NSObject *instance; // Not retained.
GSLazyRecursiveLock *iLock;
NSMapTable *paths;
}
- 它保存了一个对象的实例,重点
Not retained
由于没有持有,也不是weak
,所以当释放之后,在调用会崩溃,需要在对象销毁前,移除所有观察者 paths
用于保存keyPath
到GSKVOPathInfo
的映射:
GSKVOPathInfo
@interface GSKVOPathInfo : NSObject
{
@public
unsigned recursion;
unsigned allOptions;
NSMutableArray *observations;
NSMutableDictionary *change;
}
- 它保存了一个
keypath
对应的所有观察者 observations
保存了所有的观察者(GSKVOObservation
类型)allOptions
保存了观察者的options集合change
保存了KVO触发要传递的内容
GSKVOObservation
@interface GSKVOObservation : NSObject
{
@public
NSObject *observer; // Not retained (zeroing weak pointer)
void *context;
int options;
}
@end
它保存了单个观察的所有信息
observer
保存观察者 注意这里也是Not retained
context options
都是添加观察者时传入的参数
KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
为什么要重写class方法呢?
如果没有重写class
方法,当该对象调用class
方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class
方法是NSObject
中的方法,如果不重写最终可能会返回NSKVONotifying_Apple
,就会将该类暴露出来。
要实现isa-swizzling技术,主要通过以下几个类实现:
GSKVOReplacement
GSKVOBase
GSKVOSetter
GSKVOReplacement
@interface GSKVOReplacement : NSObject
{
Class original; /* The original class */
Class replacement; /* The replacement class */
NSMutableSet *keys; /* The observed setter keys */
}
- (id) initWithClass: (Class)aClass;
- (void) overrideSetterFor: (NSString*)aKey;
- (Class) replacement;
@end
// 创建
- (id) initWithClass: (Class)aClass
{
NSValue *template;
NSString *superName;
NSString *name;
original = aClass;
/*
* Create subclass of the original, and override some methods
* with implementations from our abstract base class.
*/
superName = NSStringFromClass(original); // original == Temp
name = [@"GSKVO" stringByAppendingString: superName]; // name = GSKVOTemp
template = GSObjCMakeClass(name, superName, nil); // template = GSKVOTemp
GSObjCAddClasses([NSArray arrayWithObject: template]);
replacement = NSClassFromString(name);
GSObjCAddClassBehavior(replacement, baseClass);
/* Create the set of setter methods overridden.
*/
keys = [NSMutableSet new];
return self;
}
- 这个类保存了被观察对象原始类信息
original
- 创建一个原始类的子类 命名为
GSKVO<原类名>
,iOS系统使用命名规则为NSKVONotifying_
原类名 - 拷贝
GSKVOBase
类中的方法到新类中 - 后续会通过
object_setClass
, 将被观察对象的isa
指向这个新类,也就是isa-swizzling
技术,而isa保存的是类的信息,也就是说被观察者对象就变成了新类的实例,这个新类用于实现KVO通知机制
GSKVOBase
这个类默认提供了几个方法,都是对NSObject方法的重写,而从上面得知,这些方法都要拷贝到新创建的替换类中。也就是被观察者会拥有这几个方法的实现
- (void) dealloc
{
// Turn off KVO for self ... then call the real dealloc implementation.
[self setObservationInfo: nil];
object_setClass(self, [self class]);
[self dealloc];
GSNOSUPERDEALLOC;
}
对象释放后,移除KVO数据,将对象重新指向原始类
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}
此方法用来隐藏替换类信息,应用层获取类的信息,仍然是原始类的信息. 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
- (Class) superclass
{
return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}
此方法和class方法原理相同
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);
imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}
这个方法是属于KVC中的,重写这个方法,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey]
和[self didChangeValueForKey: aKey]
,而这两个方法是触发KVO通知的关键。
所以说KVO是基于KVC的,而KVC正是KVO触发的入口。
GSKVOBase
@interface GSKVOSetter : NSObject
- (void) setter: (void*)val;
- (void) setterChar: (unsigned char)val;
- (void) setterDouble: (double)val;
- (void) setterFloat: (float)val;
- (void) setterInt: (unsigned int)val;
- (void) setterLong: (unsigned long)val;
#ifdef _C_LNG_LNG
- (void) setterLongLong: (unsigned long long)val;
#endif
- (void) setterShort: (unsigned short)val;
- (void) setterRange: (NSRange)val;
- (void) setterPoint: (NSPoint)val;
- (void) setterSize: (NSSize)val;
- (void) setterRect: (NSRect)rect;
@end
这个类和上面重写KVC方法原理相同,将来会替换被观察者keypath
的setter
方法实现。会在原始setter
方法前后添加[self willChangeValueForKey: aKey]
和[self didChangeValueForKey: aKey]
小结
那么到这里,就对KVO的实现有个大致的了解,通过isa-swizzling
技术, 替换被观察的类信息,并且hook被观察keyPath setter
方法,在原始方法调用前后添加[self willChangeValueForKey: aKey]
和[self didChangeValueForKey: aKey]
,从而达到监听属性变化的功能
源码实现
接下来从源码中查看KVO的所有流程
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOInfo *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;
setup();
[kvoLock lock];
// Use the original class
r = replacementForClass([self class]);
/*
* Get the existing observation information, creating it (and changing
* the receiver to start key-value-observing by switching its class)
* if necessary.
*/
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}
/*
* Now add the observer.
*/
dot = [aPath rangeOfString:@"."];
if (dot.location != NSNotFound)
{
forwarder = [[NSKeyValueObservationForwarder alloc]
initWithKeyPath: aPath
ofObject: self
withTarget: anObserver
context: aContext];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: forwarder];
}
else
{
[r overrideSetterFor: aPath];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: aContext];
}
[kvoLock unlock];
}
KVO的入口,添加观察者方法,主要做了以下几件事:
- 1.
replacementForClass
,创建替换类,并且加入到全局classTable
中,方便以后使用 - 2.获取对象的监听者数据
GSKVOInfo
,如果没有就创建新的 - 3.接下来分为两种情况
-
- 如果利用了点语法(
self.keyPath.keyPath
), 会利用NSKeyValueObservationForwarder
递归创建子对象监听,子对象在将监听到的变化转发到上层,以后再具体分析
- 如果利用了点语法(
-
- 默认情况(keyPath)直接监听对象的某个属性,则会调用
overrideSetterFor
方法,hook属性的setter方法,将setter方法的实现替换为相应参数类型的GSKVOSetter
中的方法实现
- 默认情况(keyPath)直接监听对象的某个属性,则会调用
- 4.然后调用
[info addObserver: anObserver forKeyPath: aPath options: options context: aContext]
;方法,将新的监听保存起来。
GSKVOInfo 中的添加方法
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOPathInfo *pathInfo;
GSKVOObservation *observation;
unsigned count;
if ([anObserver respondsToSelector:
@selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
{
return;
}
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo == nil)
{
pathInfo = [GSKVOPathInfo new];
// use immutable object for map key
aPath = [aPath copy];
NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
[pathInfo release];
[aPath release];
}
observation = nil;
pathInfo->allOptions = 0;
count = [pathInfo->observations count];
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver)
{
o->context = aContext;
o->options = options;
observation = o;
}
pathInfo->allOptions |= o->options;
}
if (observation == nil)
{
observation = [GSKVOObservation new];
GSAssignZeroingWeakPointer((void**)&observation->observer,
(void*)anObserver);
observation->context = aContext;
observation->options = options;
[pathInfo->observations addObject: observation];
[observation release];
pathInfo->allOptions |= options;
}
if (options & NSKeyValueObservingOptionInitial)
{
/* If the NSKeyValueObservingOptionInitial option is set,
* we must send an immediate notification containing the
* existing value in the NSKeyValueChangeNewKey
*/
[pathInfo->change setObject: [NSNumber numberWithInt: 1]
forKey: NSKeyValueChangeKindKey];
if (options & NSKeyValueObservingOptionNew)
{
id value;
value = [instance valueForKeyPath: aPath];
if (value == nil)
{
value = null;
}
[pathInfo->change setObject: value
forKey: NSKeyValueChangeNewKey];
}
[anObserver observeValueForKeyPath: aPath
ofObject: instance
change: pathInfo->change
context: aContext];
}
[iLock unlock];
}
此方法主要是保存观察者信息
1.查询相应的GSKVOPathInfo
–GSKVOObservation
如果有就更新,如果没有就创建新的并保存
2.如果options
中包含NSKeyValueObservingOptionInitial
,则立马调用[anObserver observeValueForKeyPath: aPath ofObject: instance change: pathInfo->change context: aContext]
;发送消息给观察者
2.其中获取当前值通过KVC中的[instance valueForKeyPath: aPath]
;获取
willChangeValueForKey
didChangeValueForKey
这两个方法分别添加在setter
和KVC
赋值前后,用来保存属性值的变化,以及发送消息给观察者
willChangeValueForKey :
主要记录属性值的oldValue
保存到pathInfo->change
中,如果options
包含NSKeyValueObservingOptionPrior
,则会遍历所有观察者,立马发送消息给观察者
NSKeyValueObservingOptionPrior
表示属性值修改前后都会收到通知didChangeValueForKey
根据options保存属性的新旧值,遍历所有的观察者,发送消息
移除观察者
/*
* removes the observer
*/
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
GSKVOPathInfo *pathInfo;
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo != nil)
{
unsigned count = [pathInfo->observations count];
pathInfo->allOptions = 0;
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver || o->observer == nil)
{
[pathInfo->observations removeObjectAtIndex: count];
if ([pathInfo->observations count] == 0)
{
NSMapRemove(paths, (void*)aPath);
}
}
else
{
pathInfo->allOptions |= o->options;
}
}
}
[iLock unlock];
}
此方法主要用来移除相应keyPath的观察者,方法实现很简单,根据参数传入的anObserver
和aPath
在前面介绍的数据结构中查询并移除
总结
- 主要用了
isa-swizzling
,修改了观察者的类信息,并且hooksetter
方法,当setter
方法调用时发送消息给所有观察者 - 由上面源码可以看出对观察者、被观察者的引用都是Not Retain, 所以对象释放前一定要移除观察者。
- 消息的发送主要由
[self willChangeValueForKey: key]
,[self didChangeValueForKey: key]
触发,并且必须成对出现,automaticallyNotifiesObserversForKey
方法用来控制,是否要主要添加上述的两个方法,默认返回值为YES,如果返回NO则不会自动添加,也就是说setter的调用以及KVC修改都不会触发通知 + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
此方法用来设置依赖关系,有时候需要某属性值随着同一对象的其他属性的改变而改变。可以通过事先将这样的依赖关系在类中注册,那么即便属性值间接地发生了改变,也会发送通知消息,被观察者类重写返回和key依赖的所有key集合
内部实现也比较简单,将所有依赖关系存储在全局的dependentKeyTable
中,然后hook了所有依赖的key
的setter
方法,当[self willChangeValueForKey: key]
,[self didChangeValueForKey: key]
调用时会查找所有的依赖关系,然后发送消息- KVO内部多次用到了KVC
-
- 重写
setValue:forKey
- 重写
-
- 使用
valueForKey --- valueForKeyPath
获取属性的值,尤其是在使用点语法的时候,只有valueForKeyPath
可以获得深层次的属性值。
所以KVO是基于KVC而实现的。
- 使用