FBKVOController - 面试聊到KVO如何有效的怒怼面试官!
- 1.系统KVO的问题
- 2.FBKVOController优点
- 3.FBKVOController的架构设计图
- 4.FBKVOController源码详读
- 5.FBKVOController总结
一.系统KVO的问题
- 当观察者被销毁之前,需要手动移除观察者,否则会出现程序异常(向已经销毁的对象发送消息);
- 可能会对同一个被监听的属性多次添加监听,这样我们会接收到多次监听的回调结果;
- 当观察者对多个对象的不同属性进行监听,处理监听结果时,需要在监听回调的方法中,作出大量的if判断;
- 当对同一个被监听的属性进行两次removeObserver时,会导致程序crash。这种情况通常出现在父类中有一个KVO,在父类的dealloc中remove一次,而在子类中再次remove。
二. FBKVOController优点
- 可以同时对一个对象的多个属性进行监听,写法简洁;
- 通知不会向已释放的观察者发送消息;
- 增加了block和自定义操作对NSKeyValueObserving回调的处理支持;
- 不需要在dealloc 方法中手动移除观察者,而且移除观察者不会抛出异常,当FBKVOController对象被释放时, 观察者被隐式移除;
三.FBKVOController架构设计图
四.FBKVOController源码详解
FBKVOController源码详解分四部分:
- 私有类_FBKVOInfo,
- 私有类_FBKVOSharedController
- FBKVOController,
- NSObject+FBKVOController的源码解读:
(一)FBKVOController
首先我们创建一个FBKVOController的实例对象时,有以下三种方法,一个类方法和两个对象方法,
//该方法是一个全能初始化的对象方法,其他初始化方法内部均调用该方法
//参数:observer是观察者,retainObserved:表示是否强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer;
//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;
我们先来看全能初始化方法内部的实现,
- 该方法对三个实例变量_observer(观察者)
- _objectInfosMap(NSMapTable,被监听对象->被监听属性集合之间的映射关系)
- pthread_mutex_init(互斥锁)
//全能初始化方法
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
//观察者
_observer = observer;
//NSMapTable中的key可以为对象,而且可以对其中的key和value弱引用
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
//对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER
//对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy
pthread_mutex_init(&_lock, NULL);
}
return self;
}
这里请先思考以下问题:
- 属性observer为何使用weak,它和哪个对象之间会导致循环引用问题,是如何导致循环引用问题的?
- 为何不使用字典来保存被监听对象和被监听属性集合之间的关系?
- NSDictionary的局限性有哪些?NSMapTable相对字典,有哪些优点?
- 互斥锁是为了保证哪些数据的线程安全?
带着这些问题我们来看FBKVOController内部是如何实现监听的,这里我们只看带Block回调的一个监听方法,其他几个方法和这个方法内部实现是相同的。下面的方法内部做了如下工作:
- 1.传入的参数keyPath,block为空时,程序闪退,同时报出误提示;
- 2.对传入参数为空的判读;
- 3.利用传入的参数创建_FBKVOInfo对象;
- 4.调用内部私有方法实现注册监听;
//观察者监听object中健值路径(keyPath)所对应属性的变化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一个预处理宏, 它可以让开发者比较便捷的捕获错误, 让程序闪退, 同时报出错误提示
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
//首先判断被监听的对象是否为空,被监听的健值路径是否为空,回调的block是否为空
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// 根据传进来的参数创建_FBKVOInfo对象,将这些参数封装到_FBKVOInfo对象中
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// 监听对象object的属性信息(_FBKVOInfo对象)
[self _observe:object info:info];
}
该私有方法内部并没有实现真正的注册监听,这里使用NSMapTable保存了被监听对象object-> _FBKVOInfo对象集合的关系,具体的监听是在_FBKVOSharedController类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听
内部实现思路:
- 对当前线程访问的数据_objectInfosMap进行加锁;
- 根据被监听对象object到_objectInfosMap取出被监听的属性信息对象集合infos;
- 判断被监听的属性对象info是否存在集合中;
- 如果已经存在,则不需要再次添加监听,防止多次监听;
- 如果获取的集合infos为空,则建存放_FBKVOInfo对象的集合infos,保存映射关系:object->infos;
- 将被监听的信息_FBKVOInfo对象存到集合infos中;
- 解锁,其他线程可以访问该数据;
- 调用_FBKVOSharedController 的方法实现监听;
//该方法是内部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
//先加锁,访问_objectInfosMap
pthread_mutex_lock(&_lock);
//到_objectInfosMap中根据key(被监听的对象)获取被监听的属性信息集合
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
//判断infos集合中是否存在被监听属性信息对象info
_FBKVOInfo *existingInfo = [infos member:info];
//被监听对象的属性已经存在,不需要再次监听,防止多次添加监听
if (nil != existingInfo) {
//解锁,其他线程可以再次访问_objectInfosMap中的数据
pthread_mutex_unlock(&_lock);
return;
}
//根据被监听对象在_objectInfosMap获取的被监听属性信息的集合为空
if (nil == infos) {
//懒加载创建存放_FBKVOInfo对象的set集合infos
infos = [NSMutableSet set];
//保存被监听对象和被监听属性信息的映射关系object->infos
[_objectInfosMap setObject:infos forKey:object];
}
// 将被监听的信息_FBKVOInfo对象存到集合infos中
[infos addObject:info];
//解锁
pthread_mutex_unlock(&_lock);
//最终的监听方法是通过_FBKVOSharedController中的方法来实现
//_FBKVOSharedController内部实现系统KVO方法
[[_FBKVOSharedController sharedController] observe:object info:info];
}
(二)_FBKVOInfo
_FBKVOInfo私有类的内部很简单,没有任何业务逻辑,只是一个简单的Model,主要是将以下的实例变量封装到对象中,方便访问:
{
@public
//weak,防止循环引用
__weak FBKVOController *_controller;
//被监听属性的健值路径
NSString *_keyPath;
//NSKeyValueObservingOptionNew:观察修改前的值
// NSKeyValueObservingOptionOld:观察修改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(一次修改有两次触发)
NSKeyValueObservingOptions _options;
//被监听属性值变化时的回调方法
SEL _action;
//上下文信息(void * 任何类型)
void *_context;
//被监听属性值变化时的回调block
FBKVONotificationBlock _block;
//监听状态
_FBKVOInfoState _state;
}
_FBKVOInfo私有类提供了一个全能初始化方法,来初始化以上实例变量。其他几个部分初始化方法内部均调用该全能初始化方法。
//全能初始化方法
- (instancetype)initWithController:(FBKVOController *)controller
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(nullable FBKVONotificationBlock)block
action:(nullable SEL)action
context:(nullable void *)context
{
self = [super init];
if (nil != self) {
_controller = controller;
_block = [block copy];
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}
优化判断对象相等性的效率:
- 1.首先判断hash值是否相等,若相等则进行第2步;若不等,则直接判断不等;hash值是对象判等的必要非充分条件;(即没它一定不行,有它不一定行)
- 2.在hash值相等的情况下,再进行对象判等, 作为判等的结果
//当重写hash方法时,我们可以将关键属性的hash值进行位或运算来作为hash值
- (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];
}
//输出对象的调试信息
//description: 使用NSLog从控制台输出对象的信息
//debugDescription:通过断点po打印输出对象的信息
- (NSString *)debugDescription
{
NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
if (0 != _options) {
[s appendFormat:@" options:%@", describe_options(_options)];
}
if (NULL != _action) {
[s appendFormat:@" action:%@", NSStringFromSelector(_action)];
}
if (NULL != _context) {
[s appendFormat:@" context:%p", _context];
}
if (NULL != _block) {
[s appendFormat:@" block:%p", _block];
}
[s appendString:@">"];
return s;
}
- 请分析如果将实例变量__weak FBKVOController *_controller前的 __weak去掉,它和_FBKVOInfo对象之间的循环引用环是如何形成的?
(三)_FBKVOSharedController
_FBKVOSharedController私有类内部实现了系统KVO的方法,用来接收和转发KVO的通知。接口中提供了监听和移除监听的方法。其接口如下:
@interface _FBKVOSharedController : NSObject
// 单例初始化方法
+ (instancetype)sharedController;
// 监听object的属性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;
//移除对object中属性的监听
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;
// 移除对object中多个属性的监听
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;
@end
_FBKVOSharedController私有类内部有两个私有成员变量,_infos是用来存放_FBKVOInfo对象,_infos可以对其中的成员变量弱引用,这也是为何使用NSHashTable,而不使用NSSet来存放_FBKVOInfo对象的原因。_mutex是互斥锁:
{
//存放被监听属性的信息对象
NSHashTable<_FBKVOInfo *> *_infos;
//互斥锁
pthread_mutex_t _mutex;
}
_FBKVOSharedController私有类的初始化方法,支持iOS 系统和Mac系统,初始化实例变量_infos,指定了_infos对存放在其中的成员变量弱引用,及判等性方式:
//提供全局的单例初始化方法,该单例对象的生命周期与程序的生命周期相同
+ (instancetype)sharedController
{
static _FBKVOSharedController *_controller = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_controller = [[_FBKVOSharedController alloc] init];
});
return _controller;
}
//初始化成员变量_infos和_mutex
- (instancetype)init
{
self = [super init];
if (nil != self) {
//初始化实例变量
NSHashTable *infos = [NSHashTable alloc];
// iOS 系统下:hashTable中的对象是弱引用,对象的判等方式:位移指针的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
//MAC系统下
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}
#endif
//初始化互斥锁
pthread_mutex_init(&_mutex, NULL);
}
return self;
}
- (void)dealloc
{
//对象被销毁时,销毁互斥锁
pthread_mutex_destroy(&_mutex);
}
_FBKVOSharedController在这个方法中,调用系统KVO方法,将自己注册为观察者,思路如下:
1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;
1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;
//添加监听
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
//被监听的属性信息_FBKVOInfo对象为空时,直接返回
if (nil == info) {
return;
}
// 加锁,防止多线程访问时,出现数据竞争
pthread_mutex_lock(&_mutex);
// 将被监听的属性信息info对象添加到_infos中,_infos对成员变量info是弱引用
[_infos addObject:info];
//添加完成之后,解锁,其他线程可以访问
pthread_mutex_unlock(&_mutex);
// 添加监听
[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];
}
}
实现系统KVO监听回调的方法//被监听属性更改时的回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
_FBKVOInfo *info;
{
pthread_mutex_lock(&_mutex);
//确定_infos是否包含给定的对象context,若存在返回该对象,否则返回nil;
//所使用的相等性比较取决于所选择的选项
//例如,使用NSPointerFunctionsObjectPersonality选项将使用isEqual:方法来判断相等。
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
//通过上下文参数context传过来的被监听的_FBKVOInfo对象,已经存在_infos中
if (nil != info) {
//_FBKVOSharedController对象强引用FBKVOController对象,防止被提前释放
//因为在_FBKVOInfo中,对FBKVOController对象是弱引用
FBKVOController *controller = info->_controller;
if (nil != controller) {
//强引用观察者,在FBKVOController中,FBKVOController对象弱引用观察者observer,防止在使用时已经被释放
id observer = controller.observer;
if (nil != observer) {
//使用自定义block回传监听结果
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
//将keyPath添加到字典中以便在观察多个keyPath时,能够清晰知道监听的是哪个keyPath
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];
}
}
}
}
}
_FBKVOSharedController实现了移除观察者的方法,思路如下:
- 1.首先从_infos中移除被监听的属性信息对象info;
- 2.然后根据监听状态,通过调用系统的方法,移除正在被监听的属性信息对象info;
- 3.最后修改监听状态;
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
//先从HashTable中移除被监听的属性信息对象
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
// 当正在监听时,则移除监听
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
//修改被监听的状态
info->_state = _FBKVOInfoStateNotObserving;
}
(四)NSObject+FBKVOController
NSObject+FBKVOController 分类比较简单,它主要通过runtime方法,以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。
@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
五.FBKVOController总结
FBKVOController是线程安全的,相对于系统的KVO而言,使用起来更方便,安全,简洁。
- 1.NSHashTable和NSMapTable的使用;
- 2.互斥锁pthread_mutex_t的使用
- 3.FBKVOController和Observer之间循环引用的形成和解决;
- 4.FBKVOController和_FBKVOInfo之间循环引用的形成和解决;
作者:Cooci