0%

一个 KVO 引起的 Crash

一个由不规范的使用 KVO 引起的 crash,出现的有些诡异。

前情提要

我们在一个 ViewController 中先后声明两个属性 ViewAViewB

1
2
3
4
5
@interface ViewController
// 注意这里声明的先后顺序
@property (nonatomic, strong) ViewA *viewA;
@property (nonatomic, strong) ViewB *viewB;
@end

ViewA 中想要监听 ViewB 的 frame 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface ViewA : UIView
- (void)observeView:(UIView *)view;
// 注意这个 weak
@property (nonatomic, weak) UIView *observedView;
@end

@implementation ViewA
- (void)dealloc {
// 谁监听,谁移除
[_observedView removeObserver:self forKeyPath:@"frame"];
}
- (void)observeView:(UIView *)view {
_observedView = view;
[_observedView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
}
@end

在 ViewController 中执行这个监听操作

1
2
3
4
5
6
7
- (void)viewDidLoad {
_viewA = [[ViewA alloc] init];
_viewB = [[ViewB alloc] init];
[_viewA observeView:_viewB];
// 由于所有的 View 都会被加载到 UITableView 上,
// 所以此处不会直接被加载到 self.view 上
}

OK,大致项目结构就是这样子。让我们愉快的运行起来,直到 ViewController 被 pop 的时候。
好像也没啥问题😄。。。嗯,别高兴太早,让我们来找个 iOS10 的古董机子跑一下。。。

Crash 的堆栈信息
Stack

Crash 的异常原因
Crash

问题查找

给 ViewB 实现一个 dealloc 方法,运行后 pop ViewController,发现 ViewB 的 dealloc 方法比 ViewA 的 dealloc 方法先执行。并且在 ViewB 的 dealloc 中发生了 Crash。因为 ViewB 这个时候仍被 ViewA 监听着。

猜测:对象被释放的时候,内部属性按照定义的顺序从下往上释放

修改 ViewController 中 ViewA、ViewB 的定义顺序

1
2
3
4
5
@interface ViewController
// 注意这里声明的先后顺序
@property (nonatomic, strong) ViewB *viewB;
@property (nonatomic, strong) ViewA *viewA;
@end

在 iOS 10 上运行以后,ViewA 的 dealloc 先于 ViewB 的 dealloc 方法运行,且不发生 Crash。

为何 TableView 完全展示 ViewA、ViewB 以后,pop ViewController的时候不再发生 Crash?

将 ViewA、ViewB 添加到 ViewController 的 view 上

1
2
3
4
5
6
7
8
- (void)viewDidLoad {
_viewA = [[ViewA alloc] init];
_viewB = [[ViewB alloc] init];
[_viewA observeView:_viewB];

[self.view addSubview:_viewA];
[self.view addSubview:_viewB];
}

发现,在 pop 的时候,ViewA 的 dealloc 方法先于 ViewB 的 dealloc 方法执行。
ViewController 在释放的时候,先从下往上释放(减少引用计数)本类的属性,之后释放基类的 view 属性。此时,view.subviews 被依次释放。

1
2
[self.view addSubview:_viewB];
[self.view addSubview:_viewA];

当我们调换 ViewA 和 ViewB 的添加顺序以后,发现确实是 ViewB 先于 ViewA 释放。

解决

修改 ViewA 中定义的 observedView 的引用,将 weak 改为 strong,确保 ViewB 在被释放的时候,肯定没有其他视图仍对其监听

1
2
3
4
5
@interface ViewA : UIView
- (void)observeView:(UIView *)view;
// 注意这个 weak
@property (nonatomic, strong) UIView *observedView;
@end

为何 iOS 11 及以上没有出现问题

swift4(对应 iOS 11)之后 Apple 对 KVO 做了改进,如同 iOS 9 中的 NSNotification 一样,已经不需要主动移除了。参考 Swift 4 前后 KVO 的变化