KVO+FBKVOController使用與原始碼解析

weixin_34320159發表於2017-10-14

測試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

相關文章