小心!KVO重复添加引起Crash

KVO是个好东西,但使用不当将导致巨坑——甚至应用崩溃都不在话下。下面就是一个典型案例。

1 问题描述

使用第三方框架ViewDeck实现侧滑出现隐藏View的功能。具体步骤为:

①创建IIViewDeckController(带有leftController和centerController)

1
2
3
4
5
6
7
8
9
10
11
12
13
// ViewController.m
- (void)setupNextController
{
IIViewDeckController *deckController = [[IIViewDeckController alloc] init];
LeftViewController *leftController = [[LeftViewController alloc] init];
CenterViewController *centerController = [[CenterViewController alloc] init];

deckController = [[IIViewDeckController alloc] initWithCenterViewController:centerController leftViewController:leftController];

deckController.centerhiddenInteractivity = IIViewDeckCenterHiddenNotUserInteractiveWithTapToClose;

[self.navigationController pushViewController:deckController animated:YES];
}

②从当前控制器的navigationController使用pushViewController方法进入新创建的控制器
③使用向右拖动左侧边缘的手势pop这个控制器,注意不要松手,向左拖动取消这个手势
④使用手势或返回按钮pop这个控制器,应用崩溃,如下图
重现步骤

2 分析

根据错误信息可以初步判断Crash是由于页面Dealloc时KVO没有remove导致的。但查看ViewDeck源码,发现在IIViewDeckController.mdealloc方法中已经对KVO做了remove处理。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// IIViewDeckController.m
- (void)dealloc {
[self cleanup];

self.centerController.viewDeckController = nil;
self.centerController = nil;
self.leftController.viewDeckController = nil;
self.leftController = nil;
self.rightController.viewDeckController = nil;
self.rightController = nil;

[self removePanners];
self.panners = nil;

// observations related to UIViewController properties
if (self.isObservingSelf) {
@try {
[self removeObserver:self forKeyPath:@"parentViewController"];
[self removeObserver:self forKeyPath:@"presentingViewController"];
self.isObservingSelf = NO;
} @catch(id anException) {

}
}
if (self.isObservingView) {
@try {
[self.view removeObserver:self forKeyPath:@"bounds"];
self.isObservingView = NO;
} @catch(id anException) {

}
}
#if !II_ARC_ENABLED
[super dealloc];
#endif
}

既然KVO是有remove的,那么会不会是多次添加KVO造成remove不彻底呢?果然,经过调试,发现在使用手势Pop页面时,如果取消手势,viewWillAppear方法会触发,而“bounds”这个KVO是在这个方法中addObserver的,所以就造成了KVO重复添加的现象。

3 总结

警惕viewWillAppear的执行时机
viewWillAppear并不只在页面出现时触发,如上文,还可能在手势pop页面取消时触发。所以:
尽量不要在viewWillAppear中进行页面初始化的操作,如果非做不可,建议使用全局变量判断页面是否已经加载。这个问题也适用于以下几个方法
viewDidAppear
viewWillDisappear
viewDidDisappear

举一反三,在使用NotificationCenter也需要注意不能多次添加,并注意及时remove。

这个Bug已经通过PullRequest方式反馈给了ViewDeck作者
https://github.com/ViewDeck/ViewDeck/pull/521