前言
进入 iOS 开发一年多,大部分时间都在写业务代码,鲜有对优秀开源代码的学习、总结。深知,是时候开始学习一些。万事开头难,所以我准备从比较简短的开源代码开始学习。第一篇准备写写 Facebook
这个极度热爱开源的公司的一套关于 KVO
的开源代码——FBKVOController
。阅读本篇文章前,希望你对 KVO
已经有一定的了解。
正文
先说说本文主要想讲一下哪些东西。
概述
- FBKVOController
做了什么
- FBKVOController
使用姿势
- FBKVOController
源码解析
- FBKVOController
设计思路总结
- FBKVOController
其它收获
FBKVOController 做了什么?
简单来说,Facebook
开源的这套代码,主要是对我们经常使用的 KVO
机制进行了额外的一层封装。其中最亮眼的特色是提供了一个 block 回调让我们进行处理,避免 KVO
的相关代码四处散落,不再需要使用下面这个方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
使用姿势
利用开源框架,我们这样实现,其中第二种方法可以用一行代码实现 KVO
:
#import "ViewController.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"
@interface KVOModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation KVOModel
@end
NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nonatomic, strong) KVOModel *model;
@property (nonatomic, strong) FBKVOController *kvoController;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//创建被观察的 model 类
KVOModel *model = [[KVOModel alloc] init];
//初始化设置 model 的成员变量值
model.name = @"wo";
model.age = 5;
self.model = model;
//第一种方法:创建 FBKVOController 对象,并被 VC 强引用,否则出了当前作用域,就会被销毁
FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
_kvoController = kvoController;
//添加 观察
[kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"我的旧名字是:%@", change[NSKeyValueChangeOldKey]);
NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
}];
//第二种方法:无需主动创建 FBKVOController 对象,self.KVOController 直接懒加载创建FBKVOController 对象
//可以直接对某个对象的多个成员变量执行 KVO
//------真正实现一行代码搞定 KVO------
[self.KVOController observe:model 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]);
}];
//修改 model 的 name 成员变量
model.name = @"ni";
}
@end
NS_ASSUME_NONNULL_END
相比于原生 API 优势:
- 1 可以以
数组形式
,同时对model
的多个 不同成员变量进行KVO
。 - 2 利用提供的
block
,将KVO
相关代码集中在一块,而不是四处散落。比较清晰,一目了然。 - 3 不需要在
dealloc
方法里取消对 object 的观察,当FBKVOController
对象dealloc
,会自动取消观察。
源码解析
这套源代码主要包括了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<_FBKVOInfo *> 类型
_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
。
接上段代码的最后一句 [self _observe:object info:info];
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock 互斥锁加锁
pthread_mutex_lock(&_lock);
//还记得初始化 FBKVOController 时创建的 NSMapTable 么?
//其结构是以 被观察者 object 为 key。并不像我们常用的 NSDictionary 那样是以 NSString 为 key
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
// 必须重写 _FBKVOInfo hash 以及 isEqual 方法,这样才能使用 NSSet 的 member 方法。
_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;
}
//如果没有 关于这个 object(被观察者)的相关信息,则创建 NSMutableSet,并添加到 NSMapTable 中
// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}
// add info and oberve -- NSMutableSet 加 info
[infos addObject:info];
// unlock prior to callout
pthread_mutex_unlock(&_lock);
//sharedController 是 干嘛的? 将所有观察信息统一交由一个单例来完成
[[_FBKVOSharedController sharedController] observe:object info:info];
}
总结一下上面一段的数据结构。FBKVOController
拥有成员变量 NSMapTable
,NSMapTable
以被观察者
(object)为 key, NSMutableSet
为 value 。在 NSMutableSet
中,存储了不同 info
。其关系图如下图:
追踪一下这句代码
[[_FBKVOSharedController sharedController] observe:object info:info];
_FBKVOSharedController 是会在 app 生命周期一直存在的单例,其职责是:接收并转发 KVO
通知。因此 app 当中所有 KVO
的通知都是由这个单例来完成的。
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info 向 NSHashTable 添加 info
//注意:在 _FBKVOController 类中的 NSMutableSet 已经强引用了 info
//这里是为了弱引用 info,才使用 NSHashTable,当 info dealloc 时,同时会从容器中删除
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
//_FBKVOSharedController 是实际的观察者! 随后会进行转发 ,
//context 是 void * 无类型指针,是 info 的指针!
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
//如果 state 是原始状态,则改为正在观察的状态,表明是在正在观察的状态
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];
}
}
以上代码中想单独说一下下面的代码,其中的 context
参数使用的是 (void *)info
的指针,这样可以保证 context
的唯一性。
接收 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
// 利用 context 查找 info,其中用到了 void * 转换为 id 型变量 (__bridge id)
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];
//字典合并,并重新拷贝一份,
//包含信息有:1、改变了哪个值 mChange 2、 原先的 change 字典
[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
FBKVOController
持有NSMapTable
,以object
为key
得到相对应的NSMutableSet
。NSMutableSet
中存储了不同的_FBKVOInfo
。这套数据结构的主要作用是防止开发人员重复添加相同的KVO
。当检查到其中已存在相同的_FBKVOInfo
对象时,不再执行后面的代码。 - 2
_FBKVOSharedController
持有NSHashTable
。NSHashTable
以弱引用的方式持有不同的_FBKVOInfo
。此处实际执行KVO
代码。_FBKVOInfo
有一个重要的成员变量_FBKVOInfoState
,根据这个枚举值(_FBKVOInfoStateInitial
、_FBKVOInfoStateObserving
、_FBKVOInfoStateNotObserving
) 来决定新增或者删除KVO
。
收获(通读、研究源代码后)
- 1
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
。
-
- 2 几个比较有用的宏
-
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;
-
- 3 断言的使用
NSAssert(x,y);
:x
为BOOL
值,y
为 字符串类型。当x
=YES
,则不产生断言。当x
=NO
,则产生断言,app 会 crash,并在控制台中打印y
字符串内容。合理利用断言,可以保证 app 的健壮性。 - 4 互斥锁的使用
-
pthread_mutex_init(&_lock, NULL);
(初始化)&_lock
是互斥锁的指针,第二个参数是互斥锁的属性。缺省值
是:当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。 -
pthread_mutex_destroy(&_lock);
(销毁) -
pthread_mutex_lock(&_lock);
(加锁) -
pthread_mutex_unlock(&_lock);
(解锁) - 涉及到数据的读写操作时,都需要加锁来保证避免数据竞争。
- 顺便复习一下
死锁
的概念:如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。
-
小尾巴
第一次写源码解析,感觉思路都还比较混乱,认识也还比较浅薄,需要逐渐摸索一下。有什么问题欢迎提给我!
一些相关知识的链接
NSHashTable的特性和使用
互斥锁