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 <Foundation/Foundation.h>

@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<NSKeyValueChangeKey,id> *)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<NSKeyValueChangeKey,id> * _Nonnull change) {
                         NSLog(@"%@",change);
                     }];
    
    [KVOController observe:person
                   keyPath:@"age"
                   options:NSKeyValueObservingOptionNew
                     block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _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<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];
        }
      }
    }
  }
}
  • 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

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,577评论 18 139
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,009评论 0 26
  • KVC 什么是 KVC KVC 是 Key-Value-Coding 的简称。 KVC 是一种可以直接通过字符串的...
    LeeJay阅读 2,206评论 6 41
  • 20- 枚举,枚举原始值,枚举相关值,switch提取枚举关联值 Swift枚举: Swift中的枚举比OC中的枚...
    iOS_恒仔阅读 2,223评论 1 6
  • 1.objective-c常见面试题:1、**OC **语言的基本特点OC 语言是 C 语言的一个超集,只是在 C...
    LZM轮回阅读 957评论 0 3