在本文中,我們將了解到如下內(nèi)容:
- 明晰KVO中的觀察者和被觀察者
- KVO導致崩潰的情況一覽
- 破除KVO崩潰的方案
前言
KVO(Key Value Observing) 也就是鍵值對觀察,它是iOS中觀察者模式的一種實現(xiàn)。KVO方便了我們做很多事情,但是在提供方便的時候,同樣給我們帶來了麻煩-最最最煩人的崩潰問題。
本文,我們將討論KVO導致崩潰的各種情況,以及給出解決這些崩潰問題的方案。
觀察者和被觀察者
在進行具體內(nèi)容的討論之前,我們先對觀察者和被觀察者這兩個在KVO中的角色進行明晰。
我們看如下代碼:
1. [self addObserver:self.myView forKeyPath:@"myLabel.text" options:NSKeyValueObservingOptionNew context:nil];
2. [self.myView.myLabel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
對于代碼 1 而言,其中觀察者是self.myView,被觀察者是self。
對于代碼 2 而言,其中觀察者是self,被觀察者是self.myView.myLabel。
簡單來講,在addObserver左邊的是被觀察者,在右邊的是觀察者。keyPath必須是在被觀察者中的有效路徑。被觀察者的被觀察的屬性發(fā)生變化時,將會由觀察者中的observeValueForKeyPath:ofObject:change:context:方法進行響應。
KVO導致崩潰的情況一覽
我們現(xiàn)在先來把KVO導致崩潰的原因挨個擼出來,然后再想辦法解決掉所有的這些問題(思路清晰,沒毛病。我們解決所有問題的思路都應該是這樣的)。
下面我們先列舉出我們了解到的所有引起崩潰的原因:
- 添加或移除觀察時,
keypath長度為0。 - 觀察者忘記寫監(jiān)聽回調(diào)方法
observeValueForKeyPath:ofObject:change:context:。 - 添加和移除觀察的次數(shù)不匹配
- 觀察者
dealloc后沒有移除監(jiān)聽。 - 移除未添加監(jiān)聽的觀察者。
- 多次添加和移除觀察者,但添加和移除的次數(shù)不相同。
- 觀察者
- 觀察者和被觀察者生命周期不一致,其中一個被釋放,而另一個未被釋放(比如兩個局部變量之間添加觀察)
- 被觀察者被提前釋放,iOS10及以前會崩潰(筆者未能復現(xiàn))。
- 觀察者提前被釋放,如果未移除觀察,則會崩潰。
PS:對于上面列舉到的各種情況,筆者在這里說明一下。觀察者dealloc后沒有移除監(jiān)聽* 這一情況應該是在iOS9中就被修復了,但是我找不到書面證據(jù)(略顯尷尬)。被觀察者被提前釋放,iOS10及以前會崩潰 這一情況我沒有弄出來,所以不是很確定其導致崩潰的原因,本文中將不會對其進行討論。*
破除KVO崩潰的思路
我們的目標是解決掉上述所有問題,并且要保證無侵入性。
基于這樣的目的,我們有如下的思路:
- 在
NSObject的分類中,使用Method Swizzling攔截addObserver:forKeyPath:options:context:和removeObserver:forKeyPath:方法。removeObserver:forKeyPath:context:會在判斷context是否一致之后,再調(diào)用removeObserver:forKeyPath:移除監(jiān)聽。所以我們不置換removeObserver:forKeyPath:context:方法。 - 我們創(chuàng)建一個
KVOProxy作為中間者,目的是使用KVOProxy代替對象完成所有的觀察和分發(fā)通知的功能。 -
觀察者添加觀察時,我們使KVOProxy作為真正的觀察者去添加對被觀察者的觀察,當被觀察者的屬性值有變化時,KVOProxy接收observeValueForKeyPath:ofObject:change:context:,然后再根據(jù)keypath和ofObject兩個參數(shù)去找到并通知觀察者。 -
觀察者移除觀察時,我們在KVOProxy找到需要移除的觀察,再對觀察進行移除。
整體思路如上,這樣述說給我們的感覺很模糊,我們還是回我們最熟悉的方式:看代碼。
破除KVO崩潰的實現(xiàn)
首先我們先定義我們會使用到的類,中間者KVOProxy:
@interface KVOProxy : NSObject
- (void)proxy_addObserverWithProxyItem:(KVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock;
- (void)proxy_removeObserved:(NSObject *)observed keyPath:(NSString *)keyPath didRemoveBlock:(dispatch_block_t)didRemoveBlock;
- (void)proxy_removeAllObserver;
@end
KVOProxy有一些方法,我們先不做解釋,后續(xù)使用到時,我們再進行詳細討論。
NSObject分類會添加類型為KVOProxy的成員變量kvoProxy,代碼如下:
@interface NSObject (KVOProxy)
@property (nonatomic, readonly, strong) KVOProxy *kvoProxy;
@end
我們再定義NSObject使用的用于保存通知相關信息的對象KVOProxyItem,代碼如下:
@interface KVOProxyItem : NSObject
@property (nonatomic, weak) id observed; // 弱引用被觀察者,防止循環(huán)引用
@property (nonatomic, weak) id observer; // 弱引用觀察者,如果觀察者被釋放,這里將會變?yōu)閚il
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
@property (nonatomic, assign) void *context;
- (instancetype)initWithObserver:(id)observer observed:(id)observed keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
我們在KVOProxyItem保存了通知相關的所有屬性,其中觀察者(observer)和被觀察者(observed)都是使用的弱引用。當觀察者或被觀察者被釋放后,observeValueForKeyPath:ofObject:change:context:如果被調(diào)起,我們可以判斷是否需要發(fā)送通知到觀察者。
KVOProxy的私有屬性定義如下:
@interface KVOProxy ()
@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableSet<KVOProxyItem *> *> *proxyItemMap;
@end
互斥鎖mutex,考慮到可能會在多線程中添加或移除通知,所以我們需要做一些同步操作。
數(shù)據(jù)結構proxyItemMap,該字典中的key是KVO的keyPath。字典中的value是KVOProxyItem組成的集合,該集合保存了觀察者的kvoProxy是當前KVOProxyItem對象的所有KVO對應的KVOProxyItem對象。
上面這段話讀起來可能比較繞口(表達能力就在這里,大家擔待點~~),我再對這段話做進一步的解釋:
我們會在addObserver:forKeyPath:options:context:時,創(chuàng)建一個KVOProxyItem對象:
// 創(chuàng)建KVOProxyItem
KVOProxyItem *item = [[KVOProxyItem alloc] initWithObserver:observer observed:self keyPath:keyPath options:options context:context];
那么這個item就是這次KVO對應的KVOProxyItem對象。
我們再以下面這行代碼為例:
[self addObserver:self.myView forKeyPath:@"myLabel.text" options:NSKeyValueObservingOptionNew context:nil];
這行代碼執(zhí)行之后,會在self.myView的kvoProxy的proxyItemMap中以@"myLabel.text"為key的集合中,添加一個這次KVO對應的KVOProxyItem對象。
上面說了這么多,我們還是先把相關的代碼貼出來,大家一起過過眼吧。
NSObject的代碼如下:
@implementation NSObject (KVOProxy)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self kvo_swizzleSelector:@selector(addObserver:forKeyPath:options:context:)
toSelector:@selector(kvo_addObserver:forKeyPath:options:context:)];
[self kvo_swizzleSelector:@selector(removeObserver:forKeyPath:)
toSelector:@selector(kvo_removeObserver:forKeyPath:)];
});
}
/*
removeObserver:forKeyPath:context: 會在判斷context是否一致之后,再調(diào)用removeObserver:forKeyPath:移除監(jiān)聽
*/
- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
if (keyPath.length <= 0) {
NSLog(@"keyPath is empty for observer:%@...", observer);
return;
}
if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) {
[self kvo_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
// 創(chuàng)建KVOProxyItem
KVOProxyItem *item = [[KVOProxyItem alloc] initWithObserver:observer observed:self keyPath:keyPath options:options context:context];
// 向觀察者的kvoProxy添加KVOProxyItem,如果成功則在self作為被觀察者添加觀察者observer.kvoProxy
[observer.kvoProxy proxy_addObserverWithProxyItem:item didAddBlock:^{
[self kvo_addObserver:observer.kvoProxy forKeyPath:keyPath options:options context:context];
}];
}
- (void)kvo_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
[observer.kvoProxy proxy_removeObserved:self keyPath:keyPath didRemoveBlock:^{
[self kvo_removeObserver:self.kvoProxy forKeyPath:keyPath];
}];
}
- (KVOProxy *)kvoProxy {
id proxy = objc_getAssociatedObject(self, _cmd);
if (proxy == nil) {
proxy = [KVOProxy new];
objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return proxy;
}
@end
- 首先,在
load中進行方法交換。 - 在
kvo_addObserver:forKeyPath:options:context:中進行判斷,如果keyPath.length <= 0,則直接返回,避免出現(xiàn)添加觀察時,keypath長度為0的問題。 - 創(chuàng)建
KVOProxyItem對象。 - 向
觀察者的kvoProxy中添加KVOProxyItem對象,在添加成功的回調(diào)中,執(zhí)行真正的添加觀察的操作。但是此時的觀察者已經(jīng)換成了kvoProxy。 - 在
kvo_removeObserver:forKeyPath:中根據(jù)keyPath和被觀察者從觀察者的kvoProxy中移除對應的KVOProxyItem對象。在移除成功的回調(diào)中執(zhí)行真正的移除操作。
在步驟4中,我們可以避免多次添加觀察。
在步驟5中,我們可以避免移除不存在的觀察。
細心的我們肯定看到了這樣的代碼:
if ([observer isKindOfClass:NSClassFromString(@"NSKeyValueObservance")]) {
[self kvo_addObserver:observer forKeyPath:keyPath options:options context:context];
return;
}
這是因為我們在添加KVO時,如果keyPath是多級,那么系統(tǒng)會自動拆分成多級進行監(jiān)聽。我們打印了整個過程,得到如下數(shù)據(jù):
observer:MyView - keyPath:myView.myLabel.text
observer:NSKeyValueObservance - keyPath:myLabel.text
observer:NSKeyValueObservance - keyPath:text
keyPath逐級變化,而系統(tǒng)添加的后續(xù)步驟的觀察者是NSKeyValueObservance對象,所以我們需要進行一次過濾。
KVOProxy的代碼如下:
@implementation KVOProxy
- (void)dealloc {
// 被釋放前移除所有觀察
[self proxy_removeAllObserver];
pthread_mutex_destroy(&(_mutex));
}
- (instancetype)init {
self = [super init];
if (self) {
pthread_mutex_init(&(_mutex), NULL);
self.proxyItemMap = @{}.mutableCopy;
}
return self;
}
#pragma mark - KVO Handle
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
if (keyPath.length <= 0 || object == nil) {
return;
}
[self lock];
__block KVOProxyItem *item = nil;
NSSet *set = self.proxyItemMap[keyPath];
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
if (object == obj.observed &&
self == [obj.observer kvoProxy]) {
*stop = YES;
item = obj;
}
}];
[self unlock];
if (item == nil) {
return;
}
// 如果觀察者被提前釋放,則打印錯誤信息
if (item.observer == nil) {
NSLog(@"observer is nil when %@ observe keyPath:%@", [item.observed class], item.keyPath);
return;
}
// 判斷當前觀察者是否實現(xiàn)了方法observeValueForKeyPath:ofObject:change:context:
// 這個地方用respondsToSelector:檢測,沒有用(未實現(xiàn),也返回YES)
SEL selector = @selector(observeValueForKeyPath:ofObject:change:context:);
BOOL exist = NO;
unsigned int count = 0;
Method *methoList = class_copyMethodList([item.observer class], &count);
for (int i = 0; i < count; i++) {
Method method = methoList[i];
if (method_getName(method) == selector) {
exist = YES;
break;
}
}
if (!exist) {
/*
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
*/
NSLog(@"observer:%@ can not respond observeValueForKeyPath:ofObject:change:context:", item.observer);
return;
}
// 發(fā)送事件
[item.observer observeValueForKeyPath:keyPath ofObject:object change:change context:item.context];
}
#pragma mark - Public Methods
- (void)proxy_addObserverWithProxyItem:(KVOProxyItem *)proxyItem didAddBlock:(dispatch_block_t)didAddBlock {
if (proxyItem == nil) {
return;
}
if (proxyItem.keyPath.length <= 0) {
NSLog(@"keyPath is empty for observer:%@...", proxyItem.observer);
return;
}
[self lock];
__block BOOL added = NO;
NSMutableSet *set = self.proxyItemMap[proxyItem.keyPath];
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
if (obj.observer == proxyItem.observer &&
obj.observed == proxyItem.observed) {
*stop = YES;
added = YES;
}
}];
if (added) {
NSLog(@"observer:%@ for keyPath:%@ is added", [proxyItem.observer class], proxyItem.keyPath);
[self unlock];
return;
}
if (set == nil) {
set = [NSMutableSet set];
[self.proxyItemMap setObject:set forKey:proxyItem.keyPath];
}
[set addObject:proxyItem];
[self unlock];
// 必須解鎖之后再進行回調(diào),否則會導致啟動后屏幕不顯示內(nèi)容
didAddBlock();
}
- (void)proxy_removeObserved:(NSObject *)observed keyPath:(NSString *)keyPath didRemoveBlock:(dispatch_block_t)didRemoveBlock {
if (observed == nil || keyPath.length <= 0) {
return;
}
[self lock];
NSMutableSet *set = self.proxyItemMap[keyPath];
__block KVOProxyItem *item = nil;
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
// 這里可能因為observed已經(jīng)被釋放掉,導致判斷出錯
// 但是判斷出錯對邏輯不影響,因為如果要向observed發(fā)送變更通知,observed必須不為nil
if (observed == obj.observed) {
item = obj;
*stop = YES;
}
}];
if (item) {
[set removeObject:item];
didRemoveBlock();
}
[self unlock];
}
- (void)proxy_removeAllObserver {
[self.proxyItemMap enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableSet<KVOProxyItem *> * _Nonnull obj, BOOL * _Nonnull stop) {
[obj enumerateObjectsUsingBlock:^(KVOProxyItem * _Nonnull obj, BOOL * _Nonnull stop) {
[obj.observed removeObserver:self forKeyPath:obj.keyPath];
}];
}];
}
#pragma mark - Private Methods
- (void)lock {
pthread_mutex_lock(&(_mutex));
}
- (void)unlock {
pthread_mutex_unlock(&(_mutex));
}
/// 根據(jù)指定的keyPath和observed在proxyItemMap查找KVOProxyItem
- (KVOProxyItem *)proxyItemForKeyPath:(NSString *)keyPath observed:(id)observed {
NSMutableSet *set = self.proxyItemMap[keyPath];
__block KVOProxyItem *item = nil;
[set enumerateObjectsUsingBlock:^(KVOProxyItem *obj, BOOL * _Nonnull stop) {
if (observed == obj.observed) {
item = obj;
*stop = YES;
}
}];
return item;
}
@end
- 在
proxy_addObserverWithProxyItem:didAddBlock:中。- 我們對
keyPath不合法的監(jiān)聽進行過濾。并且如果發(fā)現(xiàn)有keyPath、observer、observed的監(jiān)聽,則認為是重復添加,我們則不再添加新的監(jiān)聽。
- 我們對
- 在
proxy_removeObserved:keyPath:didRemoveBlock:中。- 我們對
keyPath、observer、observed進行匹配。如果發(fā)現(xiàn)有一致的,則移除監(jiān)聽。如果沒有,則不做移除操作。從而避免過多移除監(jiān)聽而造成的崩潰。
- 我們對
- 在
dealloc中,我們移除self的所有監(jiān)聽,防止出現(xiàn)對象被釋放,但是未移除監(jiān)聽的問題。 - 在
observeValueForKeyPath:ofObject:change:context:中。- 我們
keyPath、observer、observed進行匹配,只有在匹配到之后,才會進行通知的分發(fā)。
此時,當observer或observed為nil時,是無法進行分發(fā)的,從而避免了observer或observed被填釋放導致崩潰的問題。 - 在真正分發(fā)之前,我們需要判斷
observer是否實現(xiàn)了方法observeValueForKeyPath:ofObject:change:context:,如果未實現(xiàn),則不進行分發(fā)。從而避免其引起的崩潰問題。
- 我們
以上,就是關于KVO崩潰破除的所有解釋了,文章寫得可能不夠清晰。但是筆者已經(jīng)很努力了,如果不夠好,只能請大家諒解了(手動臉紅)。