KVO+FBKVOController使用与源码解析

测试Github地址

简介

Key-value observing is a mechanism that allows objects to 
be notified of changes to specified properties of other 
objects.

简单来说就是可以通过KVO监听对象属性的变化。

使用

我们简单的写一个model类:Person如下:

#import 

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *location;

@end

#import "Person.h"

@implementation Person

- (void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

/**
 是否自动控制监听属性的变化
 
 @param key 键值
 @return YES/NO
 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if([key isEqualToString:@"name"]){
        return NO;
    }
    return YES;
}

@end

写一个简单的测试例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self setupViews];
    [self setupObservers];
}

- (void)setupViews{
    UIButton *changeNameButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
    [changeNameButton setTitle:@"change name" forState:UIControlStateNormal];
    changeNameButton.backgroundColor = [UIColor redColor];
    changeNameButton.center = CGPointMake(self.view.center.x, 100);
    [changeNameButton addTarget:self action:@selector(changeName:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:changeNameButton];
    
    UIButton *changeAgeButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
    [changeAgeButton setTitle:@"change age" forState:UIControlStateNormal];
    changeAgeButton.backgroundColor = [UIColor redColor];
    changeAgeButton.center = CGPointMake(self.view.center.x, 200);;
    [changeAgeButton addTarget:self action:@selector(changeAge:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:changeAgeButton];
}

- (void)setupObservers{
    person = [Person new];
    person.name = @"xz";
    person.age = 20;
    person.location = @"深圳";
    NSLog(@"before %s",object_getClassName(person));
    [person addObserver:self
             forKeyPath:@"name"
                options:NSKeyValueObservingOptionNew
                context:nil];
    
    [person addObserver:self
             forKeyPath:@"age"
                options:NSKeyValueObservingOptionNew
                context:nil];
}

- (void)changeName:(id)sender{
    person.name = @"xsc";
    NSLog(@"after %s",object_getClassName(person));
}

- (void)changeAge:(id)sender{
    person.age = 22;
    NSLog(@"after %s",object_getClassName(person));
}

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

分别点击change namechange age输出的日志如下:

2017-10-12 16:22:19.606 KVOExample[27496:963593] before Person
2017-10-12 16:22:21.900 KVOExample[27496:963593] {
    kind = 1;
    new = xsc;
}
2017-10-12 16:22:21.901 KVOExample[27496:963593] after NSKVONotifying_Person
2017-10-12 16:22:23.147 KVOExample[27496:963593] {
    kind = 1;
    new = 22;
}
2017-10-12 16:22:23.148 KVOExample[27496:963593] after NSKVONotifying_Person

原理分析

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.

You should never rely on the isa pointer to determine 
class membership. Instead, you should use the class method 
to determine the class of an object instance.
  • 1.isa-swizzling的实际上是就是对象isa指针的替换技术。
  • 2.结合使用中的例子输出的日志after NSKVONotifying_Person与上述的说明我们不难分析出,当给被观察的Person类实例添加观察者时,默认会触发生成NSKVONotifying_Person的子类,子类中重写了监听的属性的set方法。
To implement manual observer notification, you invoke 
willChangeValueForKey: before changing the value, and 
didChangeValueForKey: after changing the value. The 
example in Listing 3 implements manual notifications for 
the balance property
  • 1.上述描述了如果需要实现手动的观察者的通知,需要在改变对应的属性的值前后分别调用willChangeValueForKey:,didChangeValueForKey:方法。结合使用中的例子,我们也得出相应的结论:NSKVONotifying_Person的子类中重写了Person属性的set方法,方法中分别调用了willChangeValueForKey:,didChangeValueForKey:以达到通知观察者的目的。

存在问题与解决

通过使用的例子不难分析出KVO存在如下几个问题:

  • 1.添加观察者与属性变化回调的代码逻辑是分开的。
  • 2.移除观察者的操作必须存在,不然会导致内存泄漏或Crash。
  • 3.属性变化监听的回调只能根据keyPath区分写不同的处理逻辑,代码耦合。

因此我们考虑二次封装KVO去解决这些问题。我们查看主流的关于这一块的封装facebook封装的KVOController其实是一个不错的选择。下面我们展开分析。

FBKVOController

FBKVOController的使用

#import "ViewController.h"
#import "Person.h"
#import "FBKVOController.h"

@interface ViewController (){
    Person *person;
    FBKVOController *KVOController;
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setupViews];
    
    [self setupPerson];
       
    // 2.FB对KVO的封装
    [self setupFBKVO];
}

- (void)setupViews{
    UIButton *changeNameButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
    [changeNameButton setTitle:@"change name" forState:UIControlStateNormal];
    changeNameButton.backgroundColor = [UIColor redColor];
    changeNameButton.center = CGPointMake(self.view.center.x, 100);
    [changeNameButton addTarget:self action:@selector(changeName:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:changeNameButton];
    
    UIButton *changeAgeButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 150, 50)];
    [changeAgeButton setTitle:@"change age" forState:UIControlStateNormal];
    changeAgeButton.backgroundColor = [UIColor redColor];
    changeAgeButton.center = CGPointMake(self.view.center.x, 200);;
    [changeAgeButton addTarget:self action:@selector(changeAge:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:changeAgeButton];
}

- (void)setupPerson{
    person = [Person new];
    person.name = @"xz";
    person.age = 20;
    person.location = @"深圳";
    NSLog(@"before %s",object_getClassName(person));
}

- (void)setupFBKVO {
    KVOController = [FBKVOController controllerWithObserver:self];
    [KVOController observe:person
                   keyPath:@"name"
                   options:NSKeyValueObservingOptionNew
                     block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary * _Nonnull change) {
                         NSLog(@"%@",change);
                     }];
    
    [KVOController observe:person
                   keyPath:@"age"
                   options:NSKeyValueObservingOptionNew
                     block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary * _Nonnull change) {
                         NSLog(@"%@",change);
                     }];
}

- (void)changeName:(id)sender{
    person.name = @"xsc";
    NSLog(@"after %s",object_getClassName(person));
}

- (void)changeAge:(id)sender{
    person.age = 22;
    NSLog(@"after %s",object_getClassName(person));
}

测试结果:

2017-10-12 19:04:32.343 KVOExample[37615:1335210] before Person
2017-10-12 19:04:33.491 KVOExample[37615:1335210] {
    FBKVONotificationKeyPathKey = name;
    kind = 1;
    new = xsc;
}
2017-10-12 19:04:33.492 KVOExample[37615:1335210] after NSKVONotifying_Person
2017-10-12 19:04:35.053 KVOExample[37615:1335210] {
    FBKVONotificationKeyPathKey = age;
    kind = 1;
    new = 22;
}
2017-10-12 19:04:35.054 KVOExample[37615:1335210] after NSKVONotifying_Person

FBKVOController 实现分析

FBKVOController 添加观察者

+ (instancetype)controllerWithObserver:(nullable id)observer
    - (instancetype)initWithObserver:(nullable id)observer
        - (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

关于NSMapTable可以查看NSHash​Table & NSMap​Table。上述代码完成如下工作:

  • 1.初始化了一个全局字典,配置相应的比较策略,用于存储后续的KVO的实例。
  • 2.初始化一个全局的锁,避免多线程操作导致数据异常。

FBKVOController 设置观察的属性

- (void)observe:(nullable id)object
        keyPath:(NSString *)keyPath
        options:(NSKeyValueObservingOptions)options
          block:(FBKVONotificationBlock)block
// create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  
// observe object with info
[self _observe:object info:info];
  • 1.利用传入的keyPath,options初始化一个_FBKVOInfo实例,_FBKVOInfo是一个model类用来存在KVO过程中的全部信息。
  • 2.触发真正的添加观察属性的操作。

我们深入分析步骤2中的代码:

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] observe:object info:info];
}
  • 1.每次对全局KVO信息字典表的操作都需要先执行锁操作,保证安全性。
  • 2.以观察的实例作为键值,获取的集合就是观察的所有该实例的属性初始化的_FBKVOInfo类的集合。
  • 3.操作该集合添加新的_FBKVOInfo类。

_FBKVOSharedController 真正KVO的触发实例

添加观察者
- (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];
  }
}
  • 观察的实例将[_FBKVOSharedController sharedController]实例添加到观察者中,全局的上下文传入初始化好的KVO的全局信息info,这样在触发回调时可以区分处理。
处理KVO回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary *)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 *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary *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];
        }
      }
    }
  }
}
  • 1.根据回到的context获取KVO的全部信息,然后选择block,'action',原生处理三种不同的方式分发处理。
移除观察者

回到FBKVOController类,聚焦到dealloc函数中,该函数是在对象被释放时触发。

- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

查看调用栈信息最终的触发函数如下:

- (void)_unobserveAll
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMapTable *objectInfoMaps = [_objectInfosMap copy];

  // clear table and map
  [_objectInfosMap removeAllObjects];

  // unlock
  pthread_mutex_unlock(&_lock);

  _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];

  for (id object in objectInfoMaps) {
    // unobserve each registered object and infos
    NSSet *infos = [objectInfoMaps objectForKey:object];
    [shareController unobserve:object infos:infos];
  }
}
  • 1.清理掉全局存储的KVO的信息集合。
  • 2.shareController中也需要清理存储的KVO的信息,同时移除观察者。参考如下代码段:
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
  if (0 == infos.count) {
    return;
  }

  // unregister info
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

  // remove observer
  for (_FBKVOInfo *info in infos) {
    if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;
  }
}

参考文章:

如何优雅地使用 KVO

你可能感兴趣的