KVO原理探究

2019-05-08  本文已影响0人  barry

KVO(Key-value observing)提供一种在其它对象的属性更改时通知观察它的对象的一种机制。当然它和通知都是观察者模式的实现,只是侧重点不同而已。KVO在模型和控制器之前的交互起着非常重要的作用。在OSX平台中,控制器层的绑定技术很依赖KVO。可以利用KVO观察简单属性,一对一关系的属性和一对多关系的属性。下面会一一展示三种情况

Demo

一、基本用法

场景:Person代表一个人,Account代表这个人在银行的账户。当Account中对应属性发生改变的时候会通知Person

@implementation Person
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}
@end

@interface Account : NSObject

@property (nonatomic, assign) double balance; //余额
@property (nonatomic, assign) double interestRate; //利率

@end

添加观察者

- (void)basicUse {
    self.person = [[Person alloc] init];
    self.account = [[Account alloc] init];
    self.account.balance = 0.0;
    self.account.interestRate = 2.01;
    [self.account addObserver:self.person forKeyPath:@"balance" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
//最后移除观察者
- (void)dealloc {
    [self.account removeObserver:self.person forKeyPath:"balance" context:nil];
}

1.1、注册成为观察者

被注册的对象发消息addObserver:forKeyPath:options:context:
其中
options
(指定选项按位或操作)会影响通知中提供的更改字典的内容以及生成通知的方式。options的配置选项:
NSKeyValueObservingOptionOld表示获取旧值,
NSKeyValueObservingOptionNew表示获取新值,
NSKeyValueObservingOptionInitial表示在添加观察的时候就立马响应一个回调,
NSKeyValueObservingOptionPrior表示在被观察属性变化前后都回调一次

Context
正常情况下可以指定为nil,可以通过observeValueForKeyPath:ofObject:change:context:中的key path来判断监听的哪个属性发生的改变,但是有父类和子类都监听同一属性的时候会出现问题,利用key path是无法区分的。所以一种更安全,更可扩展的方法是使用context来确保您收到的通知来自您的观察者而不是父类。

1.2、接收通知

通知的接收主要是observeValueForKeyPath:ofObject:change:context:这个方法。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonAccountBalanceContext) {
        NSLog(@"PersonAccountBalanceContext");
    } else if (context == PersonAccountInterestRateContext) {
        NSLog(@"PersonAccountInterestRateContext");
    } else {
        //因为没有对象处理这个消息会抛出一个NSInternalInconsistencyException异常
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

1.3、移除观察者

在观察者不需要监听属性变化的时候要确保观察者一定被移除,否则会造成crash
移除观察者要记住以下三点:

1.4、观察List(ordered, unOrdered)

核心示例如下:


- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
    // 无法监听array的属性
    //    [self.array addObserver:self forKeyPath:@"count" options:(NSKeyValueObservingOptionNew) context:nil];

    // 设置了NSKeyValueObservingOptionInitial 之后就会立即触发了一个NSKeyValueChangeSetting类型的通知
    [self addObserver:self forKeyPath:@"array" options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial context:nil];
}

//typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
//    NSKeyValueChangeSetting = 1,
//    NSKeyValueChangeInsertion = 2,
//    NSKeyValueChangeRemoval = 3,
//    NSKeyValueChangeReplacement = 4,
//};

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
    NSInteger kind = [change[@"kind"] integerValue];
    switch (kind) {
        case NSKeyValueChangeSetting:
            NSLog(@"NSKeyValueChangeSetting");
            break;
        case NSKeyValueChangeInsertion:
            NSLog(@"NSKeyValueChangeInsertion");
            break;
        case NSKeyValueChangeRemoval:
            NSLog(@"NSKeyValueChangeRemoval");
            break;
        case NSKeyValueChangeReplacement:
            NSLog(@"NSKeyValueChangeReplacement");
            break;
    }
    NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static NSInteger i = 0;
    //##### 注: 数组一定要通过这种方法取出,否则不会触发通知
    NSMutableArray *tempArray = [self mutableArrayValueForKey:@"array"];
    switch (i % 4) { // add
        case 0:
            [tempArray addObject:@"1"];
            break;
        case 1:  // replace
            [tempArray replaceObjectAtIndex:0 withObject:@"2"];
            break;
        case 2: // remove
            [tempArray removeObjectAtIndex:0];
            break;
        case 3:
            [tempArray removeAllObjects]; // 不会触发通知
            break;
        default:
            break;
    }
    i ++;
}

@end

其它的序列如NSMutableSet, NSMutableOrderedSet 类似,只不过取值方式一一样

二、手动干预观察流程

2.1、使某一属性只有在新值和旧值不相同时发通知

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
- (void)setBalance:(double)balance {
    if (_balance != balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = balance;
        [self didChangeValueForKey:@"balance"];
    }
}

2.2、更改次数的统计

//统计更改的次数,只有balance改变才触发itemChanged
- (void)setBalance:(double)balance {
    [self willChangeValueForKey:@"itemChanged"];
    _balance = balance;
    _itemChanged ++;
    [self didChangeValueForKey:@"itemChanged"];
}

//禁用itemChanged的通知但是可以手动触发
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"itemChanged"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

2.3、对于一对多属性的更改

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {

[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];

// Remove the transaction objects at the specified indexes.
[self.transactions removeObjectsAtIndexes:indexes];

[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];

}

除了删除操作,还有其它的一些操作

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

三、键依赖

在许多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。 如果一个属性的值发生更改,则还应通知依赖这个属性的值的属性进行更改。

3.1、To-one Relationships 的属性依赖

下面的例子中监听firstName, lastName和fullName,当firstName, lastName中的任一一个值更改时都会触发fullName更改的通知


// 只要 firstName 和 lastName 有一个改变就会触发fullName的通知
// 方式 1
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}

或者可以利用简便的方法


- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}

// 只要 firstName 和 lastName 有一个改变就会触发fullName的通知
// 方式 2
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

3.2、To-many Relationships 的属性依赖

如果某个属性的值依赖一个数组中的每个元素的话,可以进行下面的操作。总共的薪水依赖每个的雇用者的薪资的总和

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
        // deal with other observations and/or invoke super...
    }
}
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}

- (void)setTotalSalary:(NSNumber *)newTotalSalary {
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}

- (NSNumber *)totalSalary {
    return _totalSalary;
}

四、KVO原理

KVO是通过isa-swizzling技术实现的(官方文档就是一句话概括的)。具体来说就是在运行时动态创建一个中间类对象,这个中间类对象是原类对象的子类(即superClass指针指向原来的类对象),并动态修改当前实例对象的isa指向中间类对象。并且将class方法重写,返回原类对象的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取实例对象的类型。
测试代码

.h文件

@interface DeepSearch : NSObject
    @property int x;
    @property int y;
    @property int z;

    + (NSArray *)ClassMethodNames:(Class) c;
    + (void)PrintDescription:(NSString *)name obj:(id) obj;
@end

.m文件

#import "DeepSearch.h"
#import <objc/runtime.h>

struct temp_objc_class {
    Class _Nonnull isa;
    Class superclass;
};

@implementation DeepSearch

//获取当前类所有的实例方法
+ (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 < methodCount; i++)
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    free(methodList);

    return array;
}


+ (void)PrintDescription:(NSString *)name obj:(id) obj {

    struct temp_objc_class *c = (__bridge struct temp_objc_class *)(obj);

    NSString *str = [NSString stringWithFormat:
    @"%@: \n\t当前对象 --- %@\n\tNSObject class --- %s\n\tlibobjc class --- %s\n\timplements methods --- <%@>\t\n%@",
    name,
    obj,
    class_getName([obj class]),
    class_getName(c->isa),
    [[self ClassMethodNames:c->isa] componentsJoinedByString:@", "],
    [[self ClassMethodNames:c->superclass] componentsJoinedByString:@", "]];
    printf("%s\n", [str UTF8String]);
}

@end

调用

- (void)deepSearchTest {
    DeepSearch *x = [[DeepSearch alloc] init];
    DeepSearch *y = [[DeepSearch alloc] init];
    DeepSearch *xy = [[DeepSearch alloc] init];
    DeepSearch *control = [[DeepSearch alloc] init];

    [x addObserver:x forKeyPath:@"x" options:0 context:NULL];
    [xy addObserver:xy forKeyPath:@"x" options:0 context:NULL];
    [y addObserver:y forKeyPath:@"y" options:0 context:NULL];
    [xy addObserver:xy forKeyPath:@"y" options:0 context:NULL];

    [DeepSearch PrintDescription:@"control" obj:control];
    [DeepSearch PrintDescription:@"x" obj:x];
    [DeepSearch PrintDescription:@"y" obj:y];
    [DeepSearch PrintDescription:@"xy" obj:xy];

    printf("使用NSObject方法, 正常的 setX 地址: is %p, 重写 setX后的地址: is %p\n",
        [control methodForSelector:@selector(setX:)],
        [x methodForSelector:@selector(setX:)]);
    printf("使用libobjc方法, 正常的 setX 地址: is %p, 重写 setX后的地址: is %p\n",
        method_getImplementation(class_getInstanceMethod(object_getClass(control), @selector(setX:))),
        method_getImplementation(class_getInstanceMethod(object_getClass(x), @selector(setX:))));
}

然后创建了4个DeepSearch实例,每一个都使用了不同的观察方式。x实例有一个观察者x观察key xy实例有一个观察者y观察key y , xy实例有一个观察者观察key xy。为了做比较,key z没有观察者。最后control实例没有任何观察者。
下面打印的结果:

control: 
    当前对象 --- <DeepSearch: 0x6000017263c0>
    class_getName([obj class]) --- DeepSearch
    class_getName(c->isa) --- DeepSearch
    implements methods --- <setZ:, x, setX:, y, setY:, z>
    父类方法 --- _isMKClusterAnnotation, ...中间方法太多省略了..., isFault
    x: 
    当前对象 --- <DeepSearch: 0x600001726420>
    class_getName([obj class]) --- DeepSearch
    class_getName(c->isa) --- NSKVONotifying_DeepSearch
    implements methods --- <setY:, setX:, class, dealloc, _isKVOA>
    父类方法 --- setZ:, x, setX:, y, setY:, z
y: 
    当前对象 --- <DeepSearch: 0x600001726400>
    class_getName([obj class]) --- DeepSearch
    class_getName(c->isa) --- NSKVONotifying_DeepSearch
    implements methods --- <setY:, setX:, class, dealloc, _isKVOA>
    父类方法 --- setZ:, x, setX:, y, setY:, z
xy: 
    当前对象 --- <DeepSearch: 0x6000017263e0>
    class_getName([obj class]) --- DeepSearch
    class_getName(c->isa) --- NSKVONotifying_DeepSearch
    implements methods --- <setY:, setX:, class, dealloc, _isKVOA>
    父类方法 --- setZ:, x, setX:, y, setY:, z
    使用NSObject方法, 正常的 setX 地址: is 0x104e2b850, 重写 setX后的地址: is 0x10518a3d2
    使用libobjc方法, 正常的 setX 地址: is 0x104e2b850, 重写 setX后的地址: is 0x10518a3d2

打印结果分析:

简单总结

简单总结.jpg

demo纯属展示里面的一些细节

手动实现kvoDemo

mikeash大神KVO实现

mikeash大神

五、KVO缺点

可能的面试题:

1、KVO的本质是什么?

利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

2、能不能手动触发KVO?

可以,手动调用willChangeValueForKey:和didChangeValueForKey:

3、直接修改成员变量会触发KVO么?

不会

demo

上一篇 下一篇

猜你喜欢

热点阅读