iOS 深入浅出 KVO

KVO全称Key-Value Observing,是苹果提供的一套事件通知机制。 允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
简单来说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。

KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。 区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

KVO 使用

使用KVO分为三个步骤:

  • 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
  • 在观察者中实现 observeValueForKeyPath:ofObject:change:context: 方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  • 当观察者不需要监听时,可以调用 removeObserver:forKeyPath: 方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。

注册

如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察对象)上调用以下两个方法来注册和解除注册

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
observer: 被观察者
keyPath:键路径参数,描述将要观察的属性,相对于被观察者
options:标识KVO希望变化如何传递给观察者,可以使用|进行多选(有四个选项)
context:上下文内存区,通常为nil
NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)

在注册观察者时,可以传入options参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。

还可以通过方法context传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。

在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。

监听方法

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

观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。change字典中存放KVO属性相关的值,根据options时传入的枚举来返回。枚举会对应相应key来从字典中取出值,例如有NSKeyValueChangeOldKey字段,存储改变之前的旧值。

change中还有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平级的关系,来提供本次更改的信息,对应NSKeyValueChange枚举类型的value。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting。

如果被观察对象是集合对象,在NSKeyValueChangeKindKey字段中会包含NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacement的信息,表示集合对象的操作方式。

兼容的调用方式

调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,KVO兼容很多种调用方式。

// 直接调用set方法,或者通过属性的点语法间接调用
[account setName:@"Savings"];
 
// 使用KVC的setValue:forKey:方法
[account setValue:@"Savings" forKey:@"name"];
 
// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"Savings" forKeyPath:@"account.name"];

// 通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

实际应用

KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的iOS教程中有一个很经典的案例,通过KVO在Model和Controller之间进行通信。

在这里插入图片描述

注意

KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。

苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

手动调用KVO

KVO中,当被观察的属性改变时,KVO被触发。举例如下:
KVO监测Person类实例person的name属性。当name值改变时,方法- setName:被调用。此时下面两个方法会在运行- setName:之前之后被调用。

// 运行- setName:之前被调用
- (void)willChangeValueForKey:(NSString *)key
// 运行- setName:之后被调用
- (void)didChangeValueForKey:(NSString *)key

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

上述代码的automaticallyNotifiesObserversForName方法是选择是否自动通知,我们只需返回NO,系统就不会自动通知,然后再在setName方法中我们手动调用通知的两个方法,甚至可以加上加上自己想要的判断条件。
针对非自动通知的属性,可以分别在变化之前和之后手动调用如下方法(will在前,did在后)来手动通知观察者:

(will/did)ChangeValueForKey:
(will/did)ChangeValueForKey:withSetMutation:usingObjects:
(will/did)Change:valuesAtIndexes:forKey:

实现原理

KVO是通过isa-swizzling 技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

isa-swizzling 类指针交换

就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。

KVO的实现

当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。 派生类在被重写的setter方法内实现真正的通知机制 如果原类为,那么生成的派生类名为NSKVONotifying_xxx 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法

键值依赖(注册从属key)

有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object 引入了依赖键。要为一对一关系自动触发通知,应该重写keyPathsForValuesAffectingValueForKey或实现一个合适的方法,该方法遵循它为注册依赖键定义的模式。
例如,fullName取决于firstName和lastName。返回fullName的方法可以写成如下:

- (NSString *)fullName {
    return [NSString stringWithFormat:@“%@@”,firstName,lastName];
}

当firstName或lastName发生改变时,必须通知观察fullName属性的程序,因为它们影响这个属性的值。一个解决方案是重写keyPathsForValuesAffectingValueForKey来指定fullName属性依赖于lastName和firstName。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

通过重写keyPathsForValuesAffecting也可以达到相同的效果。

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

KVO和线程

一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange… 会触发 KVO 通知。
所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。
KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:
首先,如果我们调用一个支持 KVO 的 setter 方法,如下所示:

self.exchangeRate = 2.345;

KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Flutter和Objective-C(简称OC)都是开发移动应用程序的技术,但它们在很多方面有着不同的特点。 首先,Flutter是一种跨平台的移动应用开发框架,由谷歌开发。它使用Dart语言编写,具有热重载、响应式UI框架和丰富的UI组件等特点。Flutter的一大优势是可以同时在iOS和Android平台上开发应用程序,并且拥有高性能和良好的用户体验。Flutter也支持使用原生代码进行集成,因此可以很好地与Objective-C进行交互。 Objective-C是一种面向对象的编程语言,主要用于iOS和macOS平台的应用程序开发。Objective-C采用了一种称为KVO(Key-Value Observing)的机制,允许对象对属性和值的变化进行观察和响应。通过注册观察者,当被观察对象的属性发生变化时,观察者可以接收到通知并执行相应的操作。KVO是一种非常强大的工具,可以用于实现对象之间的数据绑定和通信。 在使用Flutter开发应用时,可以与Objective-C进行集成,并利用Objective-C提供的KVO机制来实现对Flutter应用内部变量的监视和响应。这可以通过在Flutter与Objective-C之间建立桥接来实现,从而达到在Flutter应用中使用KVO的目的。 总的来说,Flutter和Objective-C KVO是两种不同的技术,Flutter是一个跨平台的移动应用开发框架,而Objective-C KVO是一种可以用于观察和响应对象属性变化的机制。在合适的场景下,可以通过Flutter与Objective-C进行集成,从而利用KVO机制来实现对Flutter应用内部变量的监视和响应。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值