Runtime-KVO

KVO

​ KVO(键值观察),是iOS实现的无侵入式的观察者模式,该功能是基于runtime和KVC来完成的。

功能

自动监听

实现自动监听

1、在观察者类中实现

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context

2、使用下面三个方法,添加和删除观察者

// 添加观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

// 删除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context

// 删除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
监听方法中参数说明

keyPath:监听的属性名称,可以是属性的属性。

object:被观察的对象

change:根据注册时设置的枚举,监听时设置不同值,常用情况是记录该属性变化前后的值。

context:上下文环境,用来区分当前观察者,因为KVO允许多对多监听。

options: 说明监听类型,监听更新后的值,监听更新之前的值,监听动作等。

NSKeyValueObservingOptionNew:监听更新后的值,NSKeyValueChangeNewKey读取

NSKeyValueObservingOptionOld:监听更新前的值,NSKeyValueChangeOldKey读取。

NSKeyValueObservingOptionInitial,注册KVO时,触发监听调用,修改值时也会触发监听。

NSKeyValueObservingOptionPrior:每次修改前触发一次监听,因此与其他枚举搭配会触发两次监听,

KVO可以监听的数据类型

> 1、类中定义的属性。
>
> 2、类中定义的私有成员变量,无getter和setter方法。
>
> 3、类中只有一个符合OC要求的setter 方法,无相应属性和成员变量。
>
> 4、类中属性的属性,其实是会监听keypath上的每个节点。(在原理章节再说明))
>
> 5、类中集合属性,给其赋值可以触发KVO,但是如果增删改集合的元素不会触发。
>
> 
>
> 总结:由上面可以看出KVO是基于KVC的(在原理章节再说明),因此想要实现实现KVO监听,必须要至少具备以下条件的一种:
>
> 1、类中有对应的成员变量。
>
> 2、类中有相应的setter方法,例如对分类定义的属性的监听。
对于集合的KVO监听

​ 通常KVO只能监听变量本身内容的改变,对于像集合变量的增删改的变化,KVO无法监听。因此可以借助KVC的帮助进行监听。

// forKey
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

// forKeypath
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

实例,使用KVC获取集合就可以触发KVO监听了。

@interface ZPPerson7 : NSObject
@property (nonatomic, copy) NSString *nameAutoKVO;
@property (nonatomic, strong)NSMutableArray *arrM;
@end

// main   
// 监听集合
ZPPerson7 *p8 = [ZPPerson7 new];
p8.arrM = [NSMutableArray arrayWithCapacity:3];
ZPObserverOfPerson6 *obs5 = [ZPObserverOfPerson6 new];
[p8 addObserver:obs5 forKeyPath:@"arrM" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
// 使用KVC获取集合属性
NSMutableArray *arr = [p8 mutableArrayValueForKey:@"arrM"];
[arr addObject:@"1111"];

// 直接这样增删改数组元素不会触发KVO监听
[p8.arrM addObject:@"2222"];
[p8 removeObserver:obs5 forKeyPath:@"arrM"];

KVO键值观察依赖

​ 类中属性有时是依赖其他属性的,例如姓名全称与属性姓和名是存在依赖关系的,当姓或名改变时,姓名全称也需要改变。

实现观察依赖的步骤:

1、在建立依赖的类中实现+keyPathsForValuesAffectingValueForKey:方法,如果是属性的话,则实现+keyPathsForValuesAffecting方法,用来返回一个依赖关联集合。

2、实现自动监听步骤。

观察依赖支持的数据类型

只要支持KVC的读写就行,即可以是无setter、getter方法的私有成员变量,可以是只有getter和setter方法没有私有成员变量。

@interface ZPPerson6 : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) ZPPerson6 *father;
@property (nonatomic, strong) ZPPerson6 *mother;
@property (nonatomic, assign) NSUInteger totalAge1; // 家庭年龄总和
@end

@interface ZPPerson6 ()
{
    NSUInteger _totalAge2; // 家庭年龄总和
}
@end

@implementation ZPPerson6
  // 家庭成员每修改一次年龄,都需要修改_totalAge2。由于没有getter方法,需要自己统计。
- (void)setAge:(NSInteger)age{
    NSInteger totalAge = [[self.son valueForKey:@"totalAge2"] integerValue];
    [self.son setTotalAge4Value:(totalAge - _age + age)];
    _age = age;
}

// 重写totalAge1的getter方法,观察者的observeValueForKeyPath...方法是通过KVC来获取观察对象的值。
- (NSUInteger)totalAge1{
    return self.father.age + self.mother.age + self.age;
}

// 建立依赖方式1,totalAge1依赖于父母和子女的年龄。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingTotalAge1
{
    return [NSSet setWithObjects:@"father.age", @"mother.age", @"age", nil];
}

// 建立依赖方式2,totalAge2依赖于父母和子女的年龄。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSArray *moreKeyPaths = nil;
    if ([key isEqualToString:@"totalAge2"]) {
        moreKeyPaths = [NSArray arrayWithObjects:@"father.age", @"mother.age", @"age", nil];
    }

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if (moreKeyPaths) {
        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
    }

    return keyPaths;
}

-(void)setTotalAge2Value:(NSInteger)value{
    _totalAge1 = value;
}

@end

​ 上面说明了两种建立依赖的方式,方式1适合属性(使用@property定义的成员变量),方式2适合非属性(私有成员变量,或者没有成员变量只有setter和getter方法。)

​ 方式2写法,调用父类方法获取该key的依赖集合,将建立的新依赖保存到这个依赖集合并返回。

手动监听

​ 手动监听步骤:

1、在需要监听的类中实现+automaticallyNotifiesObserversForKey:方法,并关闭该属性的自动KVO监听,返回NO就是告诉系统,建立该key的KVO时,不要建立自动KVO。

2、重写该key对应的setter方法,并在方法中调用willChangeValueForKey:和didChangeValueForKey方法。

这样在添加该属性监听时,就是手动监听。

​ +automaticallyNotifiesObserversForKey:

该方法用来控制该类中哪些key需要手动监听,当返回NO时,则需要手动建立监听;当返回YES时,由系统建立监听。

​ willChangeValueForKey:和didChangeValueForKey:

> 这两个方法用来通知观察者,观察对象发生改变,一般用在观察key的相应setter方法中,在赋值语句前后添加这两句代码。

KVO异常

1、添加和删除KVO时,keyPath设置为nil,会导致程序崩溃。

2、添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。

3、添加和删除KVO语句不匹配,导致崩溃。

​ 1). 删除未注册的KVO,会导致程序崩溃。

​ 2). 添加KVO,但是当被观察者被释放但未删除KVO,导致崩溃,iOS10之后不会崩溃。

​ 3). 添加KVO,但是观察者被释放但未删除KVO,修改观察属性时,导致野指针。

注意:避免使用KVO导致的系统崩溃问题,添加次数和删除次数一定要匹配,即使对监听者对同一个观察者的key添加多次监听,只要最后调用相同次数的移除操作,就不会有问题,也就是上面说的添加和删除语句要匹配。

KVO原理

​ KVO是利用了对象的isa指针,实现了对监听对象无代码侵入式的监听。

​ 给监听对象添加KVO时,系统所做的步骤:

>1、创建一个该监听对象的类的派生类。
>
>2、重写相应key的setter方法,class方法、dealloc方法,并添加isKVO方法。
>
>3、修改对象的isa指针,指向派生类。

​ 删除监听对象的KVO时,系统所做的步骤:

>在dealloc中修改监听对象的isa指针,指向原来的类,销毁生成的派生类。
ZPPerson *p = [ZPPerson new];
ZPObserverOfPerson6 *obs1 = [ZPObserverOfPerson6 new];
    [p1 addObserver:obs1 forKeyPath:@"nameAutoKVO" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

// 系统生成ZPPerson类的派生类NSKVONotifying_ZPPerson,重写setter方法
-(void)setNameAutoKVO(NSString *)nameAutoKVO{
  [self willChangeValueForKey:@"nameAutoKVO"];
  [self setValue:nameAUtoKVO forKeyPath:@"nameAutoKVO"];
  [self didChangeValueForKey:@"nameAutoKVO"];
}

KVO 触发条件

​ 在注册KVO后的三种触发KVO监听的条件

1、显示调用willChangeValueForKey:和didChangeValueForKey:方法。

2、使用KVC设置对应key的值。

3、使用点语法,设置对应key的值。

上面三种方法原理都是一样的,都是通过willChangeValueForKey:和didChangeValueForKey:方法触发KVO,后两者是通过调用派生类重写的setter方法,直接访问对象成员变量是无法触发KVO监听的。

属性的属性的KVO

​ 给属性的属性添加KVO监听时,Runtime会给属性所在的每个类生成一个派生类,并修改isa指针,因此像下面ZPPerson6和ZPPerson7的类都会生成对应的派生类,并修改他们的isa指针指向这些派生类。因此修改keyPath上任意节点的值,都会触发KVO监听,像下面代码修改“son.nameAutoKVO”的son值也会触发KVO监听。

@interface ZPPerson6 : NSObject
@property (nonatomic, copy) NSString *nameAutoKVO;
@end

@interface ZPPerson7 : NSObject

@end

@interface ZPPerson7 ()
{
    ZPPerson6 *_son;
}
@end
@implementation ZPPerson7

@end

// main
void main(){
   ZPObserverOfPerson6 *obs4 = [ZPObserverOfPerson6 new];
    ZPPerson7 *p7 = [ZPPerson7 new];
	  ZPPerson6 *p6 = [ZPPerson6 new];
    [p7 setValue:p6 forKey:@"son"];
  
    [p7 addObserver:obs4 forKeyPath:@"son.nameAutoKVO" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
  	// 修改son对象的nameAutoKVO,触发KVO。
    ZPPerson6 *tempSon = [p7 valueForKey:@"son"];
    tempSon.nameAutoKVO = @"json";
  	//修改p7的son成员变量,也会触发KVO
    [p7 setValue:p1 forKey:@"son"];
}

KVO生成派生类的伪代码

// NSKVONotifying_ZPPerson7
@interface NSKVONotifying_ZPPerson7 : NSObject

@end

@interface NSKVONotifying_ZPPerson7 ()
{
    ZPPerson6 *_son;
}
@end
@implementation NSKVONotifying_ZPPerson7

- (void)setSon:(ZPPerson6 *)son{
  [self willChangeValueForKey:@"son.nameAutoKVO"];
  [super setValue:son forKey:@"son"];
  [self didChangeValueForKey:@"son.nameAutoKVO"];
}

-(ZPPerson6 *)son{
  return [super valueForKey:@"son"];
}
@end

// NSKVONotifying_ZPPerson6
@interface NSKVONotifying_ZPPerson6 : NSObject
@property (nonatomic, copy) NSString *nameAutoKVO;
@end
  
@implementation NSKVONotifying_ZPPerson6

- (void)setNameAutoKVO:(NSString *)nameAutoKVO{
  [self willChangeValueForKey:@"son.nameAutoKVO"];
  [super setValue:son forKey:@"nameAutoKVO"];
  [self didChangeValueForKey:@"son.nameAutoKVO"];
}

-(NSString *)nameAutoKVO{
  return [super valueForKey:@"nameAutoKVO"];
}
@end

无论修改son,还是son.nameAutoKVO都会触发KVO监听。

KVO流程

​ 我们拿上面例子,给"son.nameAutoKVO"赋值的步骤:

>  1、调用NSKVONotifying_ZPPerson7类的son成员的getter方法,并在方法中使用KVC获取原类中的son值。
>
>  2、调用son对象指向的派生类NSKVONotifying_ZPPerson6的setNameAutoKVO方法。
>
>  3、在该setter方法中通知观察者,并使用KVC给原类的nameAutoKVO成员变量赋值。

从这里也可以看出触发KVO的流程,进入派生类的setter方法,调用willChangeValueForKey:和didChangeValueForKey:,并且使用KVC设置父类(原类)的属性

KVO与线程

​ KVO并不是线程安全的,具有同步特性(线性执行),不建议在多线程中使用KVO,除非你能保证KVO在多线程中的线程安全问题。

原文地址:https://www.cnblogs.com/Zp3sss/p/13296635.html