转载

iOS如何一行代码搞定KVO?

前言

发现好久没有研究、学习iOS优秀开源代码,现在大部分时间都在写业务代码, 学习其他语言及一些杂七杂八的事情。所以现在就从简短的开源代码开始学习。

这一篇就写FaceBook, 这个极度热爱开源的公司, 它的一套关于KVO的开源代码。

iOS如何一行代码搞定KVO?

iOS如何一行代码搞定KVO?

GitHub代码演示代码地址

正文

FBKVOController介绍

简单来说,Facebook 开源的这套代码,主要是对我们经常使用的 KVO 机制进行了额外的一层封装。其中最亮眼的特色是提供了一个 block 回调让我们进行处理,避免 KVO 的相关代码四处散落,不再需要使用下面这个方法:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;

相对于原生API优势

1、可以以数组形式,同时对 model 的多个 不同成员变量进行 KVO。

2、利用提供的 block,将 KVO 相关代码集中在一块,而不是四处散落。比较清晰,一目了然。

3、不需要在 dealloc 方法里取消对 object 的观察,当 FBKVOController 对象 dealloc,会自动取消观察。

FBKVOController使用

方式一

使用 CocoaPods,添加下列代码到项目 Podfile 文件:

pod 'KVOController'

方式二

直接把FBKVOController文件夹拖入到你的项目中引用

' #import "FBKVOController.h"

FBKVOController框架主要分两部分:一部分是FBKVOController,主要实现了键值观测,另一部分是NSObject+FBKVOController,主要是实现了初始化的方法。

iOS如何一行代码搞定KVO?

使用一行代码实现KVO, 在self.KVOController直接懒加载创建FBKVOController的对象, 为了清晰展示演示代码没有做懒加载这步大家可以尝试。

监听单个Key改变

[self.KVOController observe:_person keyPath:@"name" options: NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary * _Nonnull change) {
     self.title = change[NSKeyValueChangeNewKey];
}];

监听多个key改变

    [self.KVOController observe:_person keyPaths:@[@"name",@"age"] options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary* change) {

        NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];

        if ([changedKeyPath isEqualToString:@"name"]) {
            NSLog(@"修改了名字");
        }else if ([changedKeyPath isEqualToString:@"age"]) {
            NSLog(@"修改了年龄");
        }

        NSLog(@"旧值是:%@",change[NSKeyValueChangeOldKey]);
        NSLog(@"新值是:%@",change[NSKeyValueChangeNewKey]);

        if ([changedKeyPath isEqualToString:@"name"]) {
            self.title = change[NSKeyValueChangeNewKey];
        }

    }];

监听改变调用方法

 [self.KVOController observe:self keyPath:@"kimiNum" options:0 action:@selector(changeColor)];

源码解析

这套源代码主要包括了FBKVOController.h、FBKVOController.m、NSObject+FBKVOController.h、NSObject+FBKVOController.m四个文件。

其中,NSObject+FBKVOController 这个分类比较简单。它主要干的事是通过 objc_setAssociatedObject (关联对象),以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。

接下来,我会着重介绍一下今天的主角 FBKVOController类。其文件中还包含另外两个类,_FBKVOInfo、_FBKVOSharedController 。下面都会介绍到。

先来看看 FBKVOController 指定初始化函数:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    //一般情况下 observer 会持有 FBKVOController 为了避免循环引用,此处的_observer 的内存管理语义是弱引用
    _observer = observer;
    //定义 NSMapTable key的内存管理策略,在默认情况,传入的参数 retainObserved = YES
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    //创建 NSMapTable  key 为 id 类型,value 为 NSMutableSet 类型
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    //初始化互斥锁,避免多线程间的数据竞争
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

以上初始化代码中,注释都写得比较清楚了。唯一比较陌生的是 NSMapTable 。简单来说,它与 NSDictionary 类似。不同之处是 NSMapTable 可以自主控制 key / value 的内存管理策略。而 NSDictionary 的内存策略是固定为 copy。当 key 为 object 时, copy的开销可能比较大!因此,在这里只能使用相对比较灵活的 NSMapTable。

执行KVO的相关方法代码解析

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
    //当 keyPath 字符串长度为 0 或者 block 为空时,会产生断言,程序会 crash
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    //如果 “被观察对象” 为 nil,同样会直接返回
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

    // create info _FBKVOInfo
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // observe object with info (利用存储的信息对 “被观察对象” 进行观察!)
  [self _observe:object info:info];
}

上述代码中,出现了一个前面提及到的 _FBKVOInfo 类,其存储的信息包括了 FBKVOController、keypath、options、block。

context 参数使用的是 (void *)info 的指针,这样可以保证 context 的唯一性。

总结

1 FBKVOController 持有 NSMapTable

以 object 为 key 得到相对应的 NSMutableSet。NSMutableSet 中存储了不同的 _FBKVOInfo。这套数据结构的主要作用是防止开发人员重复添加相同的 KVO。当检查到其中已存在相同的 _FBKVOInfo 对象时,不再执行后面的代码。

2 _FBKVOSharedController 持有 NSHashTable

NSHashTable 以弱引用的方式持有不同的 _FBKVOInfo。此处实际执行 KVO 代码。_FBKVOInfo 有一个重要的成员变量 _FBKVOInfoState,根据这个枚举值(_FBKVOInfoStateInitial、_FBKVOInfoStateObserving、_FBKVOInfoStateNotObserving) 来决定新增或者删除 KVO。

3 NSSet / NSHashTable 、NSDictionary/ NSMapTable 的学习

NSSet 是过滤掉重复 object 的集合类,NSHashTable 是 NSSet 的升级版容器,并且只有可变版本,允许对添加到容器中的对象是弱引用的持有关系, 当NSHashTable 中的对象销毁时,该对象也会从容器中移除。

NSMapTable 同 NSDictionary 类似,唯一区别是多了个功能:可以设置 key 和 value 的 NSPointerFunctionsOptions 特性! NSDictionary的 key 策略固定是 copy,考虑到开销问题,一般使用简单的数字或者字符串为 key。但是如果碰到需要用 object 作为 key 的应用场景呢?NSMapTable 就可以派上用场了!可以通过 NSFunctionsPointer 来分别定义对 key 和 value 的内存管理策略,简单可以分为 strong,weak以及 copy。

4 几个比较有用的宏

NS_ASSUME_NONNULL_BEGIN、NS_ASSUME_NONNULL_END,如果需要每个属性或每个方法都去指定 nonnull 和 nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了这两个宏。在这两个宏之间的代码,所有比较简单指针对象都被假定为 nonnull,因此我们只需要去指定那些 nullable 的指针。如果我们强行通过点语法将一个非空指针置空,编译器会报 warning。

NS_UNAVAILABLE 当我们不想要其他开发人员,用普通的 init 方法去初始化一个类,我们可以在.h 文件里这样写:

  • (instancetype)init NS_UNAVAILABLE;

编译器不但不会提示补全 init 方法,就算开发人员强制发送 init 消息,编译器会直接报错。

  • NS_DESIGNATED_INITIALIZER 指定的初始化方法。当一个类提供多种初始化方法时,所有的初始化方法最终都会调用这个指定的初始化方法。比较常见的有:

(instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;

5 断言的使用

NSAssert(x,y);:x 为 BOOL 值,y 为 字符串类型。当 x = YES,则不产生断言。当 x = NO,则产生断言,app 会 crash,并在控制台中打印 y 字符串内容。合理利用断言,可以保证 app 的健壮性。

6 互斥锁的使用

pthread_mutex_init(&_lock, NULL);(初始化)&_lock 是互斥锁的指针,第二个参数是互斥锁的属性。缺省值是:当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。

pthread_mutex_destroy(&_lock);(销毁)

pthread_mutex_lock(&_lock);(加锁)

pthread_mutex_unlock(&_lock);(解锁)

涉及到数据的读写操作时,都需要加锁来保证避免数据竞争。

顺便复习一下死锁的概念:如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。

  • 我是楚简约,感谢您的阅读,

  • 喜欢就点个赞呗,“?喜欢”,

  • 鼓励又不花钱,你在看,我就继续写~

  • 非简书用户,可以点右上角的三个“...”,然后"在Safari中打开”,就可以点赞咯~

正文到此结束
Loading...