—— KVO、KVC

时间:2021-7-3 作者:qvyue

1. KVO 的原理

对于下面使用KVO的例子,仅仅调用一行代码,便实现了实现 KVO 机制。

//// 对Person类的调用
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person1.age = 21;
    self.person2.age = 22;
}
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

//// Person类定义
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end

@implementation Person
- (void)setAge:(int)age{
    _age = age;
}
@end

在修改属性值时,只有能够触发到target类setter方法的动作才会有KVO回调,而其他的则检测不到。苹果的 KVO 机制设计非常巧妙, 通过运行时动态创建子类来实现。

—— KVO、KVC
实例addObserver之前,对象与类得关系

当我们对一个对象进行kvo监听的时候,会生成一个NSKVONotifying_前缀的类,然后我们实际的操作是对这个类进行的。某对象在addObserver:之后,这个对象的isa指针已经指向了NSKVONotifying_前缀的类,其父类被设置为Person类。

  • 验证一
    如果在原有工程中,创建NSKVONotifying_Person类,运行代码会报 KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class 错误,因为原有工程中已经存在该类,故无法运行时生成该类。

  • 验证二
    我们可以在addObserver:前后断点打印对象的isa指针,会发现两个实例对应的打印结果不同。

NSLog(@"KVO之前 - %@", object_getClass(self.person));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %@", object_getClass(self.person));

//log
//2021-06-22 20:02:02.101462+0800 TestProj[81369:8704592] KVO之前 - Person
//2021-06-22 20:02:07.003729+0800 TestProj[81369:8704592] KVO之后 - NSKVONotifying_Person
  • 验证三
    在 person 对象调用 addObserver: forKeyPath: options: context: 方法前后添加如下代码,打印结果不同 。
NSLog(@"KVO之前 - %p", [self.person methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %p", [self.person methodForSelector:@selector(setAge:)]);

//log
2021-06-22 20:12:15.245515+0800 TestProj[81577:8714132] KVO之前 - 0x10414be60
2021-06-22 20:12:23.842112+0800 TestProj[81577:8714132] KVO之后 - 0x7fff207bf79f
—— KVO、KVC
实例addObserver之后,对象与类得关系

在addObserver:后调用setage方法,会根据对象的isa找到NSKVONotifying_Person,然后在类的方法列表中找到setage。

通过下面方法,查看NSKVONotifyin_Person 的内部结构:

- (void)printMethodNamesOfClass:(Class)cls{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍历所有的方法
    for (int i = 0; i 

可见Apple是不希望暴露NSKVONotifyin_Person,重写了class方法,大概实现如下:

- (Class) class {
     // 得到类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}

重写setName: 的内部实现,其中调用了”_NSSetObjectValueAndNotify()” :

- (void)setName:(NSString *)name {
    _NSSetObjectValueAndNotify()
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
    [observer observeValueForKeyPath:@"name"];
}

void _NSSetObjectValueAndNotify() {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

2. 手动触发KVO

因为 KVO 的本质是重写了 set 方法, set 方法内部调用了willChangeValueForKey 和 didChangeValueForKey 方法,直接修改成员变量并不会调用 set 方法。由此可知,KVO 的触发条件一般是修改监听对象属性值,但也可在不修改被监听属性值的情况下触发 KVO 监听回调。

[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];

注:直接修改成员变量不会触发 KVO 监听方法,

3. KVC触发KVO

kvc常见的API有:

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

setValue:forKey:的原理,

—— KVO、KVC
setValue:forKey:流程

方法accessInstanceVariablesDirectly:(是否允许访问成员变量),默认返回YES。该方法有个应用场景就是如果你自己写框架,你的一些私有的变量不想被外部通过KVC的方式去修改,就可以重写这个方法,返回 NO 即可!

valueForKey:的原理,

—— KVO、KVC
valueForKey:流程

KVC修改属性值,是会触发KVO的。 前因是系统自动实现了set方法,并且KVC底层都会调用 willChangeValueForKey和 didChangeValueForKey。其实,只要手动调用这两个方法,KVC就会被调用。

注意:
1.当调用setValue:forKey:时,程序会先通过setter方法,对属性值进行设置;
2.如果没找到setKey:方法,KVC机制会检查accessInstanceVariablesDirectly方法,默认返回YES的,如果重写返回了NO,那么这一步会执行setValue:forUndefinedKey:方法抛出异常。若为YES,KVC机制会搜索该类中是否有名为key的成员变量,若存在,KVC都可以对该成员变量赋值。
3.若该类既没有setKey方法,也没有_key成员变量,KVC机制会搜索_isKey的成员变量;
4.若_isKey成员变量也没有,KVC机制再会继续搜索和is的成员变量给它们赋值;
5.若上述方法或者成员变量都不存在,系统会执行该对象的setValue:forUndefinedKey:方法
,并抛出异常。
即,如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。

因此,触发kvc的set方法才可以触发kvo机制。

// 这个可以完美触发kvo
[_person setValue:@"名字" forKey:@"name"];
// 这个直接给name赋值,所以不能触发kvo
[_person setValue:@"名字" forKey:@"_name"];

4. 手动实现KVO

苹果给的KVO接入也有比较明显的局限,比如无法自定义addObserver:方法、无法传入bloc看、需要处理父类同样监听同一个对象的同一个属性的情况等。我们也是可以自己实现一套KVO逻辑的,首先创建 NSObject 的分类NSObject (KVO):

//// .h
#import 

@class PGObservationInfo;
typedef void(^PGObservingBlock)(PGObservationInfo *observerInfo, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block;
- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end


@interface PGObservationInfo : NSObject

@property (nonatomic, copy) NSString *key;
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) PGObservingBlock block;

- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(PGObservingBlock)block;

@end


//// .m
#import "NSObject+KVO.h"
#import 
#import 

NSString *const kPGKVOClassPrefix = @"PGKVOClassPrefix_";
NSString *const kPGKVOAssociatedObservers = @"PGKVOAssociatedObservers";

#pragma mark - Debug Help Methods
static NSArray *ClassMethodNames(Class c) {
    
    NSMutableArray *array = [NSMutableArray array];
    
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i nn",
                     name,
                     obj,
                     class_getName([obj class]),
                     class_getName(object_getClass(obj)),
                     [ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
    printf("%sn", [str UTF8String]);
}


#pragma mark - Helpers
static NSString * getterForSetter(NSString *setter)
{
    if (setter.length 

其中复杂的Observer独立成一个类,自行创建工程,试一试吧!😜

参考文章:
http://www.cocoachina.com/articles/25407
https://tech.glowing.com/cn/implement-kvo/
https://www.jianshu.com/p/79b06fabb459
感谢!

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。