KVO是IOS中一種強(qiáng)大且有效的機(jī)制定罢,當(dāng)一個(gè)對象的屬性發(fā)生變化時(shí)吼鱼,注冊成為這個(gè)對象的觀察者的其他對象可以收到通知韩脑。我們可以使用KVO來觀察對象屬性的變化劫谅。比如见坑,想實(shí)現(xiàn)下拉刷新效果時(shí),可以使用KVO觀察UITableView的contenOffset屬性的變化來實(shí)現(xiàn)的捏检。
In order to be considered KVO-compliant for a specific property, a class must ensure the following:
- The class must be key-value coding compliant for the property, as specified in Ensuring KVC Compliance.KVO supports the same data types as KVC, including Objective-C objects and the scalars and structures listed in Scalar and Structure Support
- The class emits KVO change notifications for the property.
- Dependent keys are registered appropriately (see Registering Dependent Keys).
文檔里提到了鳄梅,要使一個(gè)類的屬性支持KVO,這個(gè)類對于屬性是滿足KVC的未檩,并且這個(gè)類會發(fā)送KVO的通知。
有兩種技術(shù)來確保發(fā)送KVO通知:
Automatic support :自動支持是NSObject提供的粟焊,對于支持KVC的屬性默認(rèn)都是可用的冤狡。不需要寫額外的代碼來發(fā)送屬性改變的通知。
Manual change notification:手動發(fā)送通知需要額外的代碼项棠。
Automatic Change Notification
NSObject provides a basic implementation of automatic key-value change notification. Automatic key-value change notification informs observers of changes made using key-value compliant accessors, as well as the key-value coding methods.
NSObject實(shí)現(xiàn)了自動改變的通知悲雳。自動通知有兩種,一種是使用屬性的setter方法香追,一種是使用KVC合瓢。
KVO的原理
KVO的實(shí)現(xiàn)依賴于runtime,Apple文檔里提到過KVO的實(shí)現(xiàn)
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
蘋果使用了 isa-swizzling來實(shí)現(xiàn)KVO透典。當(dāng)給一個(gè)被觀察者的屬性添加觀察者后晴楔,被觀察者的isa指針會被改變,指向一個(gè)中間的class峭咒,而不是原來真正的class税弃。具體來說,會創(chuàng)建一個(gè)新的類凑队,這個(gè)類繼承自被觀察者则果,并且重寫了被觀察的屬性的setter方法,重寫的setter方法會在負(fù)責(zé)調(diào)用原setter方法的前后漩氨,通知所有觀察者值得變化(使用willChangeValueForKey
和didChangeValueForKey
來通知)西壮,并且把isa的指針指向這個(gè)新創(chuàng)建的子類。
-(void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"]; //KVO在調(diào)用存取方法之前總調(diào)用
[super setValue:newName forKey:@"name"]; //調(diào)用父類的存取方法
[self didChangeValueForKey:@"name"]; //KVO在調(diào)用存取方法之后總調(diào)用}
看一下下面的測試代碼:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
#import <objc/runtime.h>
#import "Person.h"
int main(int argc, char * argv[]) {
Person *p = [[Person alloc] init];
PrintDescriptionid(p);
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
PrintDescriptionid(p);
return 0;
}
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 < methodCount; i++)
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
free(methodList);
return array;
}
static void PrintDescriptionid( id obj)
{
NSString *str = [NSString stringWithFormat:
@"NSObject class %s\nLibobjc class %s\nSuper Class %s\nimplements methods <%@>",
class_getName([obj class]),
class_getName(object_getClass(obj)),
class_getName(class_getSuperclass(object_getClass(obj))),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
log:
//添加觀察者之前
NSObject class Person
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
//添加觀察者之后
NSObject class Person
Libobjc class NSKVONotifying_Person
Super Class Person
implements methods <setName:, class, dealloc, _isKVOA>
object_getClass(obj)會獲取obj對象isa指向的類叫惊。從log可以看出款青,添加觀察者后,obj對象isa指針指向NSKVONotifying_Person
這個(gè)類赋访,它的父類是Person
,并且NSKVONotifying_Person
里實(shí)現(xiàn)了<setName:, class, dealloc, _isKVOA>
這個(gè)幾個(gè)方法可都。
Manual Change Notification
如果你想完全控制一個(gè)屬性的通知缓待,需要重寫automaticallyNotifiesObserversForKey:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
在運(yùn)行一下之前的代碼log:
//添加KVO之前
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
//添加KVO之后
NSObject class Person
Libobjc class Person
Super Class NSObject
implements methods <.cxx_destruct, name, setName:>
此時(shí)也不會創(chuàng)建NSKVONotifying_Person
這個(gè)類了。為了實(shí)現(xiàn)手動發(fā)送通知渠牲,這是在改變值之前要調(diào)用 willChangeValueForKey:旋炒,改變值之后調(diào)用didChangeValueForKey:
KVO的使用:
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
這個(gè)方法是給tableView添加了一個(gè)觀察者來監(jiān)測tableView的contentOffset屬性的變化。這個(gè)方法不會增加方法的調(diào)用者(self.tableView)和觀察者(self)的引用計(jì)數(shù)签杈。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@", change);
}
在觀察者里實(shí)現(xiàn)這個(gè)方法瘫镇,當(dāng)被觀察者的被觀察的屬性發(fā)生變化時(shí),會調(diào)用這個(gè)方法答姥。
最后铣除,不要忘記移除觀察者:
- (void)dealloc
{
[self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}
FBKVOController
facebook開源的FBKVOController框架可以很方便地使用KVO。
使用FBKVOController鹦付,上面的代碼可以改寫成:
[self.KVOController observe:self.tableView keyPath:@"contentOffset" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"%@", change);
}];
在FBKVOController尚粘,有一個(gè)NSObject的category,里面給NSObject添加了兩個(gè)屬性
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
使用KVOController時(shí)會對被觀察的對象強(qiáng)引用敲长,使用KVOControllerNonRetaining對被觀察的對象是弱引用郎嫁。
FBKVOController類里有一個(gè)實(shí)例變量:
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
NSMapTable 的key存儲的是被觀察的對象,在初始化方法里可以設(shè)置成強(qiáng)引用或者弱引用的祈噪。它的value存放的是_FBKVOInfo對象泽铛,主要是關(guān)于被觀察者的keyPath等信息。
FBKVOController使用-observer:keyPath:options:block:
觀察對象屬性變化時(shí)辑鲤,用到了_FBKVOSharedController這個(gè)類盔腔,這個(gè)類是一個(gè)單例,它的實(shí)例方法添加了觀察者:
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
// add observer
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
它也實(shí)現(xiàn)了觀察變化的方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// lookup context in registered infos, taking out a strong reference only if it exists
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
id observer = controller.observer;
if (nil != observer) {
// dispatch custom block or action, fall back to default action
if (info->_block) {
NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);
} else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
當(dāng)KVOController調(diào)用dealloc時(shí)月褥,會移除觀察者弛随。