iOS KVO崩潰全情景列舉+解決方案分析

timg.gif
****** 19.10.30 更新

被觀察者在銷毀前,要移除所有的觀察者,iOS10以下會崩潰,iOS11以上不會崩潰

先上結(jié)果

崩潰原因總結(jié)

1、observe忘記寫監(jiān)聽回調(diào)方法 observeValueForKeyPath
2、add和remove次數(shù)不匹配
3、監(jiān)聽者和被監(jiān)聽者dealloc之前沒有remove(其實也原因2,但是監(jiān)聽者和被監(jiān)聽者的生命周期不同)

KVO 是iOS開發(fā)著常用的鍵值模式,但是使用不好經(jīng)常會帶來崩潰。在復(fù)雜的業(yè)務(wù)邏輯中遇到KVO相關(guān)的偶發(fā)崩潰,原因?qū)ふ移饋肀容^困難(必現(xiàn)的的還好),下面將由簡到繁介紹KVO的崩潰情形和崩潰原因。情形六有助于更好的了解kvo的崩潰。
情型一、obsever沒有實現(xiàn)observeValueForKeyPath方法
- (void)viewDidLoad {
    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib
    
    [self.titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    _titleLabel.backgroundColor = [UIColor blueColor];
}
屏幕快照43.40.png
情型二、沒有移除KVO 比如observer是VC在pop的時候沒有在dealloc中removeKVO
//- (void)dealloc{
//    [_titleLabel removeObserver:self //forKeyPath:@"backgroundColor"];
//}

- (void)viewDidLoad {
    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib
    
    [self.titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    _titleLabel.backgroundColor = [UIColor blueColor];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if ([keyPath isEqualToString:@"backgroundColor"]) {
        NSLog(@"顏色改變了");
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
}
屏幕快照44.png
情型三、多次移除KVO
- (void)dealloc{
    //第二次remove
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    [_titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    _titleLabel.backgroundColor = [UIColor blueColor];
    
    //第一次remove
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    if ([keyPath isEqualToString:@"backgroundColor"]) {
        NSLog(@"顏色改變了");
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
屏幕快照50.png
情型四、多次添加相同KVO但是remove次數(shù)不同(如果add 和remove相匹配不會崩潰 例子中在dealloc中remove兩次)
- (void)dealloc{
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
    // [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [_titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    if ([keyPath isEqualToString:@"backgroundColor"]) {
        NSLog(@"顏色改變了");
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
屏幕快照.36.png
情形六 監(jiān)聽者和被監(jiān)聽者的生命周期不同
- (void)dealloc{
    [_titleLabel removeObserver:_titleView forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [_titleLabel addObserver:_titleView forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    _titleLabel.backgroundColor = [UIColor blueColor];

///全都注釋不會崩潰  任意打開下面一個都會崩潰  這也是一些分業(yè)務(wù)邏輯復(fù)雜的功能,會有不易查出崩潰的原因, 監(jiān)聽對象和被監(jiān)聽對象的釋放時機(jī)沒有掌握好就會導(dǎo)致類似的崩潰。
// 比如對AVPlayerItem 的監(jiān)聽,在切換AVPlayerItem 若果邏輯不嚴(yán)謹(jǐn)可能會導(dǎo)致kvo的崩潰。
    //_titleLabel = nil;
    // _titleView = nil;

}

///_titleView 自定義的實現(xiàn) observeValueForKeyPath 方法
#import "ObserView.h"

@implementation ObserView

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if ([keyPath isEqualToString:@"backgroundColor"]) {
        NSLog(@"顏色改變了");
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
崩潰原因總結(jié)

1、忘記寫監(jiān)聽回調(diào)方法 observeValueForKeyPath
2、add和remove次數(shù)不匹配
3、監(jiān)聽者和被監(jiān)聽者dealloc之前沒有remove

解決方案分析

方案一 cc_addObserver 代碼在下面
情形一會崩潰
- (void)dealloc{
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
  
    [_titleLabel cc_addObserver:self forKeyPath:@"backgroundColor"];
    _titleLabel.backgroundColor = [UIColor blueColor];

}

情形二、不會崩潰

- (void)dealloc{
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [_titleLabel cc_addObserver:self forKeyPath:@"backgroundColor"];
    _titleLabel.backgroundColor = [UIColor blueColor];
    _titleLabel = nil;

}

cc_addObserver 只能防治生命周期不匹配的崩潰形式,不能防止add remove 不匹配的情形

方案二 使用JJException

情形一、不會崩潰

- (void)dealloc{
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
    //[_titleLabel removeObserver:self 
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [_titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    _titleLabel.backgroundColor = [UIColor blueColor];
    
    _titleLabel = nil;
    //_titleView = nil;
}

情形二、會崩潰

- (void)dealloc{
    [_titleLabel removeObserver: _titleView forKeyPath:@"backgroundColor"];
    //[_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _titleLabel = [UILabel new];
    _titleLabel.backgroundColor = [UIColor redColor];
    _titleLabel.frame = CGRectMake(20, 100, 50, 50);
    [self.view addSubview:_titleLabel];
    
    _titleView = [UIView new];
    _titleView.backgroundColor = [UIColor yellowColor];
    _titleView.frame = CGRectMake(100, 100, 50, 50);
    [self.view addSubview:_titleView];
    

    [_titleLabel addObserver:_titleView forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    //_titleLabel
    _titleLabel.backgroundColor = [UIColor blueColor];
    
    _titleLabel = nil;
    //_titleView = nil;
}

情形三、會崩潰

- (void)dealloc{
    [_titleLabel removeObserver: _titleView forKeyPath:@"backgroundColor"];
    //[_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _titleLabel = [UILabel new];
    _titleLabel.backgroundColor = [UIColor redColor];
    _titleLabel.frame = CGRectMake(20, 100, 50, 50);
    [self.view addSubview:_titleLabel];
    
    _titleView = [UIView new];
    _titleView.backgroundColor = [UIColor yellowColor];
    _titleView.frame = CGRectMake(100, 100, 50, 50);
    [self.view addSubview:_titleView];
    

    [_titleLabel addObserver:_titleView forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    //_titleLabel
    _titleLabel.backgroundColor = [UIColor blueColor];
    
    //_titleLabel = nil;
    //_titleView = nil;

}

情形四、不會崩潰

- (void)dealloc{
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
    //[_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [_titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    //_titleLabel
    _titleLabel.backgroundColor = [UIColor blueColor];
    
    //_titleLabel = nil;
    //_titleView = nil;
}

情形五、不會崩潰

- (void)dealloc{
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
    [_titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [_titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    //_titleLabel
    _titleLabel.backgroundColor = [UIColor blueColor];
    
    //_titleLabel = nil;
    //_titleView = nil;
}

總結(jié):

cc_addObserver 和 JJException 都不會完全解決KVO使用不嚴(yán)謹(jǐn)引發(fā)的崩潰問題,推薦使用 cc_addObserver,并且不自己寫removeObserve的方法,防止監(jiān)聽者為nil時crash

下面是 cc_addObserver 代碼

#import <Foundation/Foundation.h>

@interface NSObject (ObserverHelper)

- (void)cc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

#import "NSObject+ObserverHelper.h"
#import <objc/message.h>

@interface ObserverHelper : NSObject
@property (nonatomic, unsafe_unretained) id target;
@property (nonatomic, unsafe_unretained) id observer;
@property (nonatomic, strong) NSString *keyPath;
@property (nonatomic, weak) ObserverHelper *factor;
@end

@implementation ObserverHelper
- (void)dealloc {
    if ( _factor ) {
        [_target removeObserver:_observer forKeyPath:_keyPath];
    }
}
@end

@implementation NSObject (ObserverHelper)

- (void)cc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];
    
    ObserverHelper *helper = [ObserverHelper new];
    ObserverHelper *sub = [ObserverHelper new];
    
    sub.target = helper.target = self;
    sub.observer = helper.observer = observer;
    sub.keyPath = helper.keyPath = keyPath;
    helper.factor = sub;
    sub.factor = helper;
    
    const char *helpeKey = [[keyPath mutableCopy] UTF8String];
    const char *subKey = [[keyPath mutableCopy] UTF8String];
    // 關(guān)聯(lián)屬性  舉例 self 和 helper 關(guān)聯(lián) 當(dāng)self釋放的時候 helper釋放 即可釋放self的kvo 觀察者和sub關(guān)聯(lián) 當(dāng)觀察者釋放的時候 調(diào)用sub的移除同樣也能刪除self的kvo   factor是同一個對象 是為防止多次移除導(dǎo)致的崩潰
    objc_setAssociatedObject(self, helpeKey, helper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(observer, subKey, sub, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容