KVO总结和FBKVOController

KVO是IOS中一种强大且有效的机制,当一个对象的属性发生变化时,注册成为这个对象的观察者的其他对象可以收到通知。我们可以使用KVO来观察对象属性的变化。比如,想实现下拉刷新效果时,可以使用KVO观察UITableView的contenOffset属性的变化来实现的。

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).

文档里提到了,要使一个类的属性支持KVO,这个类对于属性是满足KVC的,并且这个类会发送KVO的通知。

有两种技术来确保发送KVO通知:

  • Automatic support :自动支持是NSObject提供的,对于支持KVC的属性默认都是可用的。不需要写额外的代码来发送属性改变的通知。

  • Manual change notification:手动发送通知需要额外的代码。

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实现了自动改变的通知。自动通知有两种,一种是使用属性的setter方法,一种是使用KVC。

KVO的原理

KVO的实现依赖于runtime,Apple文档里提到过KVO的实现

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来实现KVO。当给一个被观察者的属性添加观察者后,被观察者的isa指针会被改变,指向一个中间的class,而不是原来真正的class。具体来说,会创建一个新的类,这个类继承自被观察者,并且重写了被观察的属性的setter方法,重写的setter方法会在负责调用原setter方法的前后,通知所有观察者值得变化(使用willChangeValueForKeydidChangeValueForKey来通知),并且把isa的指针指向这个新创建的子类。

-(void)setName:(NSString *)newName{ 
[self willChangeValueForKey:@"name"];    //KVO在调用存取方法之前总调用 
[super setValue:newName forKey:@"name"]; //调用父类的存取方法 
[self didChangeValueForKey:@"name"];     //KVO在调用存取方法之后总调用}

看一下下面的测试代码:

@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这个类,它的父类是Person,并且NSKVONotifying_Person里实现了<setName:, class, dealloc, _isKVOA>这个几个方法。

Manual Change Notification

如果你想完全控制一个属性的通知,需要重写automaticallyNotifiesObserversForKey:

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

在运行一下之前的代码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:>

此时也不会创建NSKVONotifying_Person这个类了。为了实现手动发送通知,这是在改变值之前要调用 willChangeValueForKey:,改变值之后调用didChangeValueForKey:

KVO的使用:

[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];

这个方法是给tableView添加了一个观察者来监测tableView的contentOffset属性的变化。这个方法不会增加方法的调用者(self.tableView)和观察者(self)的引用计数。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"%@", change);
}

在观察者里实现这个方法,当被观察者的被观察的属性发生变化时,会调用这个方法。

最后,不要忘记移除观察者:

- (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,有一个NSObject的category,里面给NSObject添加了两个属性

@property (nonatomic, strong) FBKVOController *KVOController;

@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

使用KVOController时会对被观察的对象强引用,使用KVOControllerNonRetaining对被观察的对象是弱引用。

FBKVOController类里有一个实例变量:

  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;

NSMapTable 的key存储的是被观察的对象,在初始化方法里可以设置成强引用或者弱引用的。它的value存放的是_FBKVOInfo对象,主要是关于被观察者的keyPath等信息。
FBKVOController使用-observer:keyPath:options:block:观察对象属性变化时,用到了_FBKVOSharedController这个类,这个类是一个单例,它的实例方法添加了观察者:

- (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];
  }
}

它也实现了观察变化的方法:

- (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];
        }
      }
    }
  }
}

当KVOController调用dealloc时,会移除观察者。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容