Key-Value Observing (键值监视)

kvo原理

Apple 使用了isa-swizzling来实现 KVO。

当观察对象A时,系统就会在运行期动态地创建该类的一个子类NSKVONotifying_A,在子类中重写任何被观察属性的setter方法,同时还重写了class方法以“欺骗”外部调用者它就是起初的那个类,并且改变了该对象的isa指针的指向(指向了新建的子类),当属性的值发生改变了,会调用子类的set方法,然后发出通知。

当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

注意:

  • 重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的。
  • KVO监听的是指针变化
  • 由于重写了class方法,无法获了真正的类名,可以使用object_getClass获取。

kvo相关接口说明

// 注册:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
// 解除注册:
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
// 处理变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

这两个方法的定义在 Foundation/NSKeyValueObserving.h 中,NSObjectNSArrayNSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。在该头文件中,我们还可以看到NSObject还实现了NSKeyValueObserverNotification的 category 方法,这两个方法在手动实现键值观察时会用到:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的setter方法,或通过key-path来设置:

target.age = 30;
[target setAge:30];
[target setValue:@30 forKey:@"age"];

关于容器类(如NSMutableArray)的观察, 当通过addObject:向数组中添加对象, 不会触发KVO, 因为并没有触发set方法。解决方法:通过KVC方法- mutableArrayValueForKey:来修改。

kvo的基本使用

// Person
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

static void *xContext = &xContext;

// 添加观察者
- (void)addObserver {
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial;
    [_person addObserver:self forKeyPath:@"name" options:options context:xContext];
    [_person addObserver:self forKeyPath:@"age" options:options context:xContext];
}

// 移除观察者
- (void)removeObserver {
    [_person removeObserver:self forKeyPath:@"name" context:xContext];
    [_person removeObserver:self forKeyPath:@"age" context:xContext];
}

// 观察的变量发生变化时,该方法就会被回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == xContext) {
        NSLog(@"%@ - change: %@", keyPath, change);
        if ([keyPath isEqualToString:@"name"]) {
            
        } else if ([keyPath isEqualToString:@"age"]) {
            
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

kvo的手动实现

首先,需要手动实现属性的setter方法,并在设置操作的前后分别调用willChangeValueForKey:didChangeValueForKey:方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

@interface Target : NSObject {
    int age;
}
- (int)age;
- (void)setAge:(int)theAge;

@end

@implementation Target

- (instancetype)init {
    if (self = [super init]) {
        age = 10;
    }
    return self;
}
- (int)age {
    return age;
}
- (void)setAge:(int)theAge{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

@end

键值观察依赖键

有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object 引入了依赖键。

@interface Target : NSObject

@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;

@end

// 观察的是 TargetWrapper 类的 information 属性,该属性是依赖于 Target 类的 age 和 grade 属性
@interface TargetWrapper : NSObject

@property(nonatomic, assign) NSString *information;
@property(nonatomic, strong) Target *target;

@end

@implimentation TargetWrapper

- (NSString *)information {
    return [[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]];
}

- (void)setInformation:(NSString *)theInformation {
    NSArray *array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

+ (NSSet *)keyPathsForValuesAffectingInformation {
    NSSet *keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
    return keyPaths;
}

//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
//    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//    if ([key isEqualToString:@"information"]) {
//        NSArray *moreKeyPaths = @[@"target.age", @"target.grade"];
//        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
//    }
//    
//    return keyPaths;
//}

@end

首先,要手动实现属性informationsetter/getter方法,在其中使用Target的属性来完成其settergetter
其次,要实现keyPathsForValuesAffectingInformationkeyPathsForValuesAffectingValueForKey:方法来告诉系统information属性依赖于哪些其他属性,这两个方法都返回一个 key-path 的集合。

自定义KVO

根据kvo的原理,可以自定义一个kvo,建一个NSObject的分类, 添加方法:

@interface NSObject (XSKVO)

- (void)xs_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

@end

// .m文件
#import "NSObject+XSKVO.h"
#import <objc/runtime.h>

// 通过runtime的方式, 动态创建一个类, 并给该类添加方法
@implementation NSObject (XSKVO)

- (void)xs_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    
    //1.新建一个类
    NSString *className = [@"XSKVO" stringByAppendingString: NSStringFromClass([self class])];
    Class newClass = objc_allocateClassPair([self class], className.UTF8String, 0);
     //注册类
    objc_registerClassPair(newClass);
    //2.修改 调用者类型
    object_setClass(self, newClass);
    
    //3.给子类添加set方法(子类里面没有set方法的)
    //OC方法:方法编号SEL ,方法实现IMP
    class_addMethod(newClass, @selector(setName:), xssetName, "");
}

/*
 隐藏的参数:
 self  方法的调用者
 _cmd  方法的编号
 */
void xssetName(id self, SEL _cmd, NSString *newName){
    NSLog(@"自定义的实现%@",newName);
    //方案一:通过消息机制 发送消息 -observeValueForKeyPath
}

@end

Introduction to Key-Value Observing Programming Guide
Better key-value observing for Cocoa: MAKVONotificationCenter
深入浅出Cocoa之类与对象
深入浅出Cocoa 之动态创建类
iOS--KVO的实现原理与具体应用

猜你喜欢

转载自blog.csdn.net/weixin_33953384/article/details/86892425
今日推荐