开发过iOS的app已经不计其数了,在不同的项目中采用的架构也各不相同,有传统的MVC,简化的VIPER,以及一些简单的MVVM。
这其中,我最不推荐的就是VIPER,谁写谁知道,,绝对是增加了项目的复杂性。MVVM由于自己总是受限于传统的Object-Oriented
的思路,总是想不出真正的Functional Programming的代码,因此,绝大多数情况,写着写着都回归到了MVC。
其实,相较于网上大家总喜欢提到的Massive View Controller问题,我更想说的是这种传统架构中对于信息流的不友好。
在一个典型的iOS的问题中,我们的代码执行流程,通常都是从View Controller的生命周期开始,如果是一个完全基于顺序执行的应用,那整个app的信息流是单向可跟踪的。但是往往事情并不会那么简单,我们会包含至少如下这些潜在打乱信息流的坏蛋
- Delegate回调
- NSNotification
- UIView控件的Target-Action
- KVO
在这里,你可能会以为我想谈谈ReactiveCocoa和RxSwift,那你错啦,那个开源项目我暂时还没有能力去深究,所以我想从KVO事件入手,读一读Facebook出品的FBKVOController。
FBKVOController
简单来说,FBKVOController是对KVO机制的一层封装,同时提供了线程安全的特性和并对如下这个臭名昭著的函数进行了封装,提供了干净的block的回调,避免了处理这个函数的逻辑散落的到处都是。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
源码分析
整个项目的结构非常简单,包含如下四个文件:
- FBKVOController.h/.m
- NSObject+FBKVOController.h/.m
其中,NSObject+FBKVOController
只是通过AssociateObject
给NSObject提供了一个retain
及一个非retain
型的KVOController。
这两种不同类型的KVOController有啥区别,我们稍后再提,我们将重点投向FBKVOController
这个文件。
打开这个FBKVOController.m
文件,哎呀,600多行文件,有点蛋疼。没事,配合头文件粗略扫一眼以后,可以发现其中很多方法都是convenience method
。
简单剥离一下数据结构以后,我们可以发现,主要的数据结构有如下三个。
- FBKVOInfo
- FBKVOSharedController
- FBKVOController
FBKVOController
既然我们前面通过NSObject+FBKVOController
知道了每个对象都会有其对应的FBKVOController
,那我们就先来看看这个类吧。
//1.
@implementation FBKVOController
{
NSMapTable *_objectInfosMap;
OSSpinLock _lock;
}
//2.
- (instancetype)initWithObserver:(id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
// 2.
_observer = observer;
// 3.
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
// 4.
_lock = OS_SPINLOCK_INIT;
}
return self;
}
首先我们看到,这个对象持有一个
OSSpinLock
及一个NSMapTable
。其中OSSpinLock
即为自旋锁,当多个线程竞争相同的critical section时,起到保护作用。NSMapTable
可能大家接触不是很多,我们在后文会详细介绍,这里大家可以先理解为一个高级的NSDictionary。在构造函数中,首先将传入的observer进行
weak
持有,这主要为了避免Retain Cycle。这一段的内容可能大家不太熟悉,
NSPointerFunctionsOptions
简单来说就是定义NSMapTable
中的key和value采用何种内存管理策略,包括strong
强引用,weak
弱引用以及copy
(要支持NSCopying协议)初始化自旋锁
接下来,使我们通过FBKVOController
来对一个对象的某个或者某些keypath进行观察。
- (void)observe:(id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// 1. create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// 2. observe object with info
[self _observe:object info:info];
}
- 对于传入的参数,构建一个内部的FBKVOInfo数据结构
- 调用
[self _observe:object info:info];
接下来,我们来跟踪一下[self _observe:object info:info];
,内容如下:
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
OSSpinLockLock(&_lock);
// 1.
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// 2.
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
NSLog(@"observation info already exists %@", existingInfo);
// unlock and return
OSSpinLockUnlock(&_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
OSSpinLockUnlock(&_lock);
// 3.
[[_FBKVOSharedController sharedController] observe:object info:info];
}
抛开Facebook自身标记的注释,有三处比较值得我们注意:
根据被观察的object获取其对应的infos set。这个主要作用在于避免多次对同一个keyPath添加多次观察,避免crash。因为每调用一次
addObserverForKeyPath
就要有一个对应的removeObserverForKey
。从infos set判断是不是已经有了与此次info相同的观察。
如果以上都顺利通过,将观察的信息及关系注册到
_FBKVOSharedController
中。
至此,FBKVOController的任务基本都结束,unObserve
相关的任务逻辑大同小异,不再赘述。
FBKVOSharedController
初次看到这个类的时候,我的脑海中浮现了两个问题,FBKVOSharedController是干嘛的?为什么FBKVOController还需要将观察的信息转交呢?
其实我个人觉得这一层不是必要的,但是按照Facebook的理念来说就是将所有的观察信息统一交由一个FBKVOSharedController
的单例进行维护。如果大家读过Facebook出品的Flux架构,也会发现,Facebook经常喜欢维护一个类似于中间件的注册表,在这里,FBKVOSharedController
承担的也是类似的职责。
于是,通过如下方法,我们像使用注册表一样将对KVOInfo注册。
- (void)observe:(id)object info:(_FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register info
OSSpinLockLock(&_lock);
[_infos addObject:info];
OSSpinLockUnlock(&_lock);
// 1.
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
}
- 代表所有的观察信息都首先由
FBKVOSharedController
进行接受,随后进行转发。
实现observeValueForKeyPath:ofObject:Change:context
来接收通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
// 1.
OSSpinLockLock(&_lock);
info = [_infos member:(__bridge id)context];
OSSpinLockUnlock(&_lock);
}
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) {
info->_block(observer, object, change);
} 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];
}
}
}
}
}
- 根据context上下文获取对应的KVOInfo
- 判断当前
info
的observer
和controller
,是否仍然存在(因为之前我们采用的weak持有) - 根据
info
的block
或者selector
或者override
进行消息转发。
到这里,FBKVOController
整体的实现就介绍完了,怎么样,是不是局部看自己都会实现,但是一结合起完整的设计思路,就觉得,不亏是Facebook呢。
NSMapTable
之前我们在前文中提到了NSMapTable
,现在我们来详细介绍他一下。
我们在平常的开发中都使用过NSDictionary
或者NSMutableDictionary
,但是这两种数据结构有其的局限性。
以NSDictionary
为例,NSDictionary
将key
的hash
值作为索引,存储对应的value
。因此,key
的要求是不能更改。所以,NSDictionary
为了确保安全,对于key
采用了copy的策略。
默认情况下,支持NSCopying协议的类型都可以作为key。但是考虑到copy带来的开销,一般情况下我们都使用简单的诸如数字或者字符串作为key。
那么,如果要使用Object
作为key,想构建Object to Object的关系怎么办呢?这个时候就用到NSMapTable
。我们可以通过NSFunctionsPointer来分别定义对key和value的储存关系,简单可以分类为strong
,weak
以及copy
。而当利用object
作为key的时候,可以定义评判相等的标准,如:use shifted pointer hash and direct equality, object description或者size。
具体你需要去override如下几种方法:
// pointer personality functions
@property (nullable) NSUInteger (*hashFunction)(const void *item, NSUInteger (* __nullable size)(const void *item));
@property (nullable) BOOL (*isEqualFunction)(const void *item1, const void*item2, NSUInteger (* __nullable size)(const void *item));
@property (nullable) NSUInteger (*sizeFunction)(const void *item);
@property (nullable) NSString * __nullable (*descriptionFunction)(const void *item);
在FBKVOController
自定义的可以作为key的结构FBKVOInfo
,就复写了
- (NSUInteger)hash
{
return [_keyPath hash];
}
- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}