本文導(dǎo)讀:
1.系統(tǒng)KVO的問題
2.FBKVOController優(yōu)點
3.FBKVOController的架構(gòu)設(shè)計圖
4.FBKVOController源碼詳讀
5.FBKVOController總結(jié)
一.系統(tǒng)KVO的問題
- 當觀察者被銷毀之前枫疆,需要手動移除觀察者馍惹,否則會出現(xiàn)程序異常(向已經(jīng)銷毀的對象發(fā)送消息)燎猛;
- 可能會對同一個被監(jiān)聽的屬性多次添加監(jiān)聽曹抬,這樣我們會接收到多次監(jiān)聽的回調(diào)結(jié)果溉瓶;
- 當觀察者對多個對象的不同屬性進行監(jiān)聽,處理監(jiān)聽結(jié)果時谤民,需要在監(jiān)聽回調(diào)的方法中堰酿,作出大量的if判斷;
- 當對同一個被監(jiān)聽的屬性進行兩次removeObserver時张足,會導(dǎo)致程序crash触创。這種情況通常出現(xiàn)在父類中有一個KVO,在父類的dealloc中remove一次为牍,而在子類中再次remove哼绑。
二. FBKVOController優(yōu)點
- 可以同時對一個對象的多個屬性進行監(jiān)聽,寫法簡潔碉咆;
- 通知不會向已釋放的觀察者發(fā)送消息抖韩;
- 增加了block和自定義操作對NSKeyValueObserving回調(diào)的處理支持;
- 不需要在dealloc 方法中手動移除觀察者疫铜,而且移除觀察者不會拋出異常茂浮,當FBKVOController對象被釋放時, 觀察者被隱式移除壳咕;
三.FBKVOController架構(gòu)設(shè)計圖
四.FBKVOController源碼詳解
FBKVOController源碼詳解分四部分:分別是對兩個私有類_FBKVOInfo席揽,_FBKVOSharedController和兩個公開類FBKVOController,NSObject+FBKVOController的源碼解讀:
(一)FBKVOController
首先我們創(chuàng)建一個FBKVOController的實例對象時谓厘,有以下三種方法驹尼,一個類方法和兩個對象方法,
//該方法是一個全能初始化的對象方法庞呕,其他初始化方法內(nèi)部均調(diào)用該方法
//參數(shù):observer是觀察者新翎,retainObserved:表示是否強引用被觀察的對象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
//該初始化方法內(nèi)部調(diào)用上一個初始化方法,默認強引用被觀察的對象
- (instancetype)initWithObserver:(nullable id)observer;
//該初始化方法內(nèi)部調(diào)用上一個初始化方法住练,默認強引用被觀察的對象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;
我們先來看全能初始化方法內(nèi)部的實現(xiàn)地啰,該方法對三個實例變量_observer(觀察者),_objectInfosMap(NSMapTable讲逛,被監(jiān)聽對象->被監(jiān)聽屬性集合之間的映射關(guān)系)亏吝,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];
//對于靜態(tài)分配的互斥量, 可以把它設(shè)置為PTHREAD_MUTEX_INITIALIZER
//對于動態(tài)分配的互斥量, 在申請內(nèi)存(malloc)之后, 通過pthread_mutex_init進行初始化, 并且在釋放內(nèi)存(free)前需要調(diào)用pthread_mutex_destroy
pthread_mutex_init(&_lock, NULL);
}
return self;
}
這里請先思考以下問題:
- 屬性observer為何使用weak盏混,它和哪個對象之間會導(dǎo)致循環(huán)引用問題蔚鸥,是如何導(dǎo)致循環(huán)引用問題的惜论?
- 為何不使用字典來保存被監(jiān)聽對象和被監(jiān)聽屬性集合之間的關(guān)系?
- NSDictionary的局限性有哪些止喷?NSMapTable相對字典馆类,有哪些優(yōu)點?
- 互斥鎖是為了保證哪些數(shù)據(jù)的線程安全弹谁?
帶著這些問題我們來看FBKVOController內(nèi)部是如何實現(xiàn)監(jiān)聽的乾巧,這里我們只看帶Block回調(diào)的一個監(jiān)聽方法,其他幾個方法和這個方法內(nèi)部實現(xiàn)是相同的预愤。下面的方法內(nèi)部做了如下工作:
1.傳入的參數(shù)keyPath沟于,block為空時,程序閃退植康,同時報出誤提示旷太;
2.對傳入?yún)?shù)為空的判讀;
3.利用傳入的參數(shù)創(chuàng)建_FBKVOInfo對象销睁;
4.調(diào)用內(nèi)部私有方法實現(xiàn)注冊監(jiān)聽供璧;
//觀察者監(jiān)聽object中健值路徑(keyPath)所對應(yīng)屬性的變化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一個預(yù)處理宏, 它可以讓開發(fā)者比較便捷的捕獲錯誤, 讓程序閃退, 同時報出錯誤提示
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
//首先判斷被監(jiān)聽的對象是否為空,被監(jiān)聽的健值路徑是否為空榄攀,回調(diào)的block是否為空
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// 根據(jù)傳進來的參數(shù)創(chuàng)建_FBKVOInfo對象嗜傅,將這些參數(shù)封裝到_FBKVOInfo對象中
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
// 監(jiān)聽對象object的屬性信息(_FBKVOInfo對象)
[self _observe:object info:info];
}
該私有方法內(nèi)部并沒有實現(xiàn)真正的注冊監(jiān)聽金句,這里使用NSMapTable保存了被監(jiān)聽對象object-> _FBKVOInfo對象集合的關(guān)系檩赢,具體的監(jiān)聽是在_FBKVOSharedController類中實現(xiàn)的。觀察者可以監(jiān)聽多個對象违寞,而每個對象中可能有多個屬性被監(jiān)聽贞瞒,其關(guān)系如下圖:
內(nèi)部實現(xiàn)思路:
- 對當前線程訪問的數(shù)據(jù)_objectInfosMap進行加鎖;
- 根據(jù)被監(jiān)聽對象object到_objectInfosMap取出被監(jiān)聽的屬性信息對象集合infos趁曼;
- 判斷被監(jiān)聽的屬性對象info是否存在集合中军浆;
- 如果已經(jīng)存在,則不需要再次添加監(jiān)聽挡闰,防止多次監(jiān)聽乒融;
- 如果獲取的集合infos為空,則建存放_FBKVOInfo對象的集合infos摄悯,保存映射關(guān)系:object->infos赞季;
- 將被監(jiān)聽的信息_FBKVOInfo對象存到集合infos中;
- 解鎖奢驯,其他線程可以訪問該數(shù)據(jù)申钩;
- 調(diào)用_FBKVOSharedController 的方法實現(xiàn)監(jiān)聽;
//該方法是內(nèi)部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
//先加鎖瘪阁,訪問_objectInfosMap
pthread_mutex_lock(&_lock);
//到_objectInfosMap中根據(jù)key(被監(jiān)聽的對象)獲取被監(jiān)聽的屬性信息集合
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
//判斷infos集合中是否存在被監(jiān)聽屬性信息對象info
_FBKVOInfo *existingInfo = [infos member:info];
//被監(jiān)聽對象的屬性已經(jīng)存在撒遣,不需要再次監(jiān)聽邮偎,防止多次添加監(jiān)聽
if (nil != existingInfo) {
//解鎖,其他線程可以再次訪問_objectInfosMap中的數(shù)據(jù)
pthread_mutex_unlock(&_lock);
return;
}
//根據(jù)被監(jiān)聽對象在_objectInfosMap獲取的被監(jiān)聽屬性信息的集合為空
if (nil == infos) {
//懶加載創(chuàng)建存放_FBKVOInfo對象的set集合infos
infos = [NSMutableSet set];
//保存被監(jiān)聽對象和被監(jiān)聽屬性信息的映射關(guān)系object->infos
[_objectInfosMap setObject:infos forKey:object];
}
// 將被監(jiān)聽的信息_FBKVOInfo對象存到集合infos中
[infos addObject:info];
//解鎖
pthread_mutex_unlock(&_lock);
//最終的監(jiān)聽方法是通過_FBKVOSharedController中的方法來實現(xiàn)
//_FBKVOSharedController內(nèi)部實現(xiàn)系統(tǒng)KVO方法
[[_FBKVOSharedController sharedController] observe:object info:info];
}
(二)_FBKVOInfo
_FBKVOInfo私有類的內(nèi)部很簡單义黎,沒有任何業(yè)務(wù)邏輯禾进,只是一個簡單的Model,主要是將以下的實例變量封裝到對象中轩缤,方便訪問:
{
@public
//weak命迈,防止循環(huán)引用
__weak FBKVOController *_controller;
//被監(jiān)聽屬性的健值路徑
NSString *_keyPath;
//NSKeyValueObservingOptionNew:觀察修改前的值
// NSKeyValueObservingOptionOld:觀察修改后的值
//NSKeyValueObservingOptionInitial:觀察最初的值(在注冊觀察服務(wù)時會調(diào)用一次觸發(fā)方法)
//NSKeyValueObservingOptionPrior:分別在值修改前后觸發(fā)方法(一次修改有兩次觸發(fā))
NSKeyValueObservingOptions _options;
//被監(jiān)聽屬性值變化時的回調(diào)方法
SEL _action;
//上下文信息(void * 任何類型)
void *_context;
//被監(jiān)聽屬性值變化時的回調(diào)block
FBKVONotificationBlock _block;
//監(jiān)聽狀態(tài)
_FBKVOInfoState _state;
}
_FBKVOInfo私有類提供了一個全能初始化方法,來初始化以上實例變量火的。其他幾個部分初始化方法內(nèi)部均調(diào)用該全能初始化方法壶愤。
//全能初始化方法
- (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;
}
同時_FBKVOInfo私有類還重寫了isEqual:和hash方法,用來進行_FBKVOInfo對象的判等性馏鹤。當我們在自定義對象時征椒,需要重寫isEqual:和hash方法,作為自定義對象相等性的判斷湃累。
- 優(yōu)化判斷對象相等性的效率:
1.首先判斷hash值是否相等勃救,若相等則進行第2步;若不等治力,則直接判斷不等蒙秒;hash值是對象判等的必要非充分條件;(即沒它一定不行宵统,有它不一定行)
2.在hash值相等的情況下晕讲,再進行對象判等, 作為判等的結(jié)果;
關(guān)于對象相等性判斷马澈,請看大神Mattt Thompson的一篇博客 Equality瓢省。
//當重寫hash方法時,我們可以將關(guān)鍵屬性的hash值進行位或運算來作為hash值
- (NSUInteger)hash
{
return [_keyPath hash];
}
/**
對于基本類型, ==運算符比較的是值;
對于對象類型, ==運算符比較的是對象的地址(即是否為同一對象)
*/
- (BOOL)isEqual:(id)object
{
//判斷對象是否為空痊班,若為空勤婚,則不相等
if (nil == object) {
return NO;
}
//判斷對象的地址是否相等,若相等涤伐,則為同一個對象(即是否為同一個對象)
if (self == object) {
return YES;
}
//判斷是否是同一類型馒胆,這樣可以提高判等的效率, 還可以避免隱式類型轉(zhuǎn)換帶來的潛在風(fēng)險
if (![object isKindOfClass:[self class]]) {
return NO;
}
//對各個屬性分別使用默認判等方法進行判斷
//返回所有屬性判等的與結(jié)果
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}
//輸出對象的調(diào)試信息
//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對象之間的循環(huán)引用環(huán)是如何形成的凝果?
(三)_FBKVOSharedController
_FBKVOSharedController私有類內(nèi)部實現(xiàn)了系統(tǒng)KVO的方法祝迂,用來接收和轉(zhuǎn)發(fā)KVO的通知。接口中提供了監(jiān)聽和移除監(jiān)聽的方法豆村。其接口如下:
@interface _FBKVOSharedController : NSObject
// 單例初始化方法
+ (instancetype)sharedController;
// 監(jiān)聽object的屬性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;
//移除對object中屬性的監(jiān)聽
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;
// 移除對object中多個屬性的監(jiān)聽
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;
@end
_FBKVOSharedController私有類內(nèi)部有兩個私有成員變量液兽,_infos是用來存放_FBKVOInfo對象,_infos可以對其中的成員變量弱引用,這也是為何使用NSHashTable四啰,而不使用NSSet來存放_FBKVOInfo對象的原因宁玫。_mutex是互斥鎖:
{
//存放被監(jiān)聽屬性的信息對象
NSHashTable<_FBKVOInfo *> *_infos;
//互斥鎖
pthread_mutex_t _mutex;
}
_FBKVOSharedController私有類的初始化方法,支持iOS 系統(tǒng)和Mac系統(tǒng)柑晒,初始化實例變量_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 系統(tǒng)下:hashTable中的對象是弱引用佛掖,對象的判等方式:位移指針的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
//MAC系統(tǒng)下
#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在這個方法中涌庭,調(diào)用系統(tǒng)KVO方法芥被,將自己注冊為觀察者,思路如下:
1.首先將被監(jiān)聽的信息對象_FBKVOInfo保存到_infos中坐榆;
2.然后調(diào)用系統(tǒng)KVO方法將自己注冊為被監(jiān)聽對象object的觀察者拴魄;
3.最后修改監(jiān)聽的狀態(tài);當不再監(jiān)聽時席镀,安全移除觀察者匹中;
//添加監(jiān)聽
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
//被監(jiān)聽的屬性信息_FBKVOInfo對象為空時,直接返回
if (nil == info) {
return;
}
// 加鎖豪诲,防止多線程訪問時顶捷,出現(xiàn)數(shù)據(jù)競爭
pthread_mutex_lock(&_mutex);
// 將被監(jiān)聽的屬性信息info對象添加到_infos中,_infos對成員變量info是弱引用
[_infos addObject:info];
//添加完成之后屎篱,解鎖服赎,其他線程可以訪問
pthread_mutex_unlock(&_mutex);
// 添加監(jiān)聽
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
//修改監(jiān)聽狀態(tài)
if (info->_state == _FBKVOInfoStateInitial) {
info->_state = _FBKVOInfoStateObserving;
} else if (info->_state == _FBKVOInfoStateNotObserving) {
//不再監(jiān)聽時安全移除觀察者
// 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];
}
}
實現(xiàn)系統(tǒng)KVO監(jiān)聽回調(diào)的方法
//被監(jiān)聽屬性更改時的回調(diào)
- (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);
}
//通過上下文參數(shù)context傳過來的被監(jiān)聽的_FBKVOInfo對象堪侯,已經(jīng)存在_infos中
if (nil != info) {
//_FBKVOSharedController對象強引用FBKVOController對象,防止被提前釋放
//因為在_FBKVOInfo中荔仁,對FBKVOController對象是弱引用
FBKVOController *controller = info->_controller;
if (nil != controller) {
//強引用觀察者伍宦,在FBKVOController中,F(xiàn)BKVOController對象弱引用觀察者observer乏梁,防止在使用時已經(jīng)被釋放
id observer = controller.observer;
if (nil != observer) {
//使用自定義block回傳監(jiān)聽結(jié)果
if (info->_block) {
NSDictionary<NSString *, id> *changeWithKeyPath = change;
//將keyPath添加到字典中以便在觀察多個keyPath時次洼,能夠清晰知道監(jiān)聽的是哪個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) {
//使用自定義方法回傳監(jiān)聽結(jié)果
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
//使用系統(tǒng)默認方法回傳監(jiān)聽結(jié)果
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
_FBKVOSharedController實現(xiàn)了移除觀察者的方法,思路如下:
1.首先從_infos中移除被監(jiān)聽的屬性信息對象info遇骑;
2.然后根據(jù)監(jiān)聽狀態(tài)卖毁,通過調(diào)用系統(tǒng)的方法,移除正在被監(jiān)聽的屬性信息對象info;
3.最后修改監(jiān)聽狀態(tài)亥啦;
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
//先從HashTable中移除被監(jiān)聽的屬性信息對象
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
// 當正在監(jiān)聽時炭剪,則移除監(jiān)聽
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
//修改被監(jiān)聽的狀態(tài)
info->_state = _FBKVOInfoStateNotObserving;
}
(四)NSObject+FBKVOController
NSObject+FBKVOController 分類比較簡單,它主要通過runtime方法翔脱,以懶加載的形式給 NSObject 奴拦,創(chuàng)建并關(guān)聯(lián)一個 FBKVOController 的對象。
@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
五.FBKVOController總結(jié)
FBKVOController是線程安全的届吁,相對于系統(tǒng)的KVO而言错妖,使用起來更方便,安全疚沐,簡潔暂氯。
1.NSHashTable和NSMapTable的使用;
2.互斥鎖pthread_mutex_t的使用
3.FBKVOController和Observer之間循環(huán)引用的形成和解決亮蛔;
4.FBKVOController和_FBKVOInfo之間循環(huán)引用的形成和解決株旷;