【iOS】系统是怎么实现 UIAlertView 的?

前段时间在写 LCActionSheet 这个库的时候,收到一个老兄 weiwei1035 提到的一个 UIWindow 相关的 Issue,说:当 statusBarStyleUIStatusBarStyleLightContent 时,背景不会遮挡 statusBar。这位老兄还很热情地写了解决方案,并 pr 给了我,我简单验证过就愉快的合并了。

我就喜欢这种提了问题自己想解决方案最后还能让我少写些代码的人,再次表扬。

但,凡事要知其然,也要知其所以然。

作为一名具有高度责(hao)任(qi)感(xin)的 Coder,我会把这事就这么简单放过吗?当然不会!后来我仔细想了想这个 Issue,思考了这个 window 的必要性:就为了一个 UI 效果搞这么复杂真的有必要吗?

开始我其实也觉得没必要。但是我觉得吧,我说了不算,系统说了才算。毕竟这些代码都是运行在 iOS 系统上,照系统来肯定不会错。那怎么办呢?那就来研究研究 UIAlertView 的实现好了。

目的

探索系统 UIAlertView 的实现,重点:

  1. alloc init 的时候干了些什么?
  2. - (void)show; 的时候干了些什么?
  3. 当 alertView dismiss 的时候又干了些什么?

思路

做任何事,最好是先想好思路。思路完善了,做起事情来才能游刃有余。

首先,我觉得系统 UIAlertView 的实现过程是这样的:

  1. UIAlertView alloc init 的时候,只是创建了一个 UIAlertView 实例(如:myAlertView),它并没有被添加到父 view 上。
  2. 当调用 [myAlertView show] 的时候,系统创建一个新的 UIWindow 实例(如:alertWindow),设为当前 application 的 keyWindow(如:originKeyWindow),并保存原有的 keyWindow 指针,然后把 alertView 添加到这个新建的 alertWindow 上,此时 alertView 就显示在设备屏幕上了。(动画过程略)
  3. alertView 需要消失的时候,系统先把 alertViewalertWindow 上移除:[alertView removeFromSuperView],然后再设置 application 的 keyWindow 为之前保存的 originKeyWindow,再设置 alertWindow.hide = YES 来销毁 alertWindow

Over!

那么我猜想的到底对不对呢?让我们用代码检验下就知道了。
(其实我当时根本就没检验因为我就是这么自信哈哈哈)

代码检验

1、首先新建一个项目,就叫 UIAlertViewDemo 好了。
2、打开 ViewController.m,编辑代码如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

#import "ViewController.h"

#define KEY_WINDOW [UIApplication sharedApplication].keyWindow

@interface ViewController () <UIAlertViewDelegate>

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

self.view.backgroundColor = [UIColor whiteColor];

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Title"
message:@"message"
delegate:self
cancelButtonTitle:@"ok"
otherButtonTitles:nil, nil];

NSLog(@"origin key window: %p", KEY_WINDOW);

[alertView show];
}

#pragma mark - UIAlertView Delegate

/**
* 点 alertView 上面的 btn 时调用,alertView 还在
*/
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {

NSLog(@"when clicked key window: %p", KEY_WINDOW);
}

/**
* alertView 将要消失时调用
*/
- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {

NSLog(@"alert view will dismiss key window: %p", KEY_WINDOW);
}
/**
* alertView 已经消失时调用
*/
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {

NSLog(@"alert view did dismiss key window: %p", KEY_WINDOW);
}

@end

**
3、Run,点击 alertView 的 ok 按钮,看看输出结果:

1
2
3
4
2015-12-29 17:23:02.397 UIAlertViewDemo[4057:484684] origin key window: 0x0
2015-12-29 17:23:04.564 UIAlertViewDemo[4057:484684] when clicked key window: 0x7faa12840080
2015-12-29 17:23:04.564 UIAlertViewDemo[4057:484684] alert view will dismiss key window: 0x7faa12840080
2015-12-29 17:23:04.978 UIAlertViewDemo[4057:484684] alert view did dismiss key window: 0x7faa1151a720

咦,后面的打印确实比较符合猜想,但是这个开始的 origin key window: 0x0 是什么鬼?!噢,差点忘了,当代码走到这里的时候,还没有 keyWindow。

4、那么我们这么改一下:

  1. 去项目的 General 中,删除 Main Interface 的值(之前值为 Main)
  2. AppDelegate.m 中,修改如下代码:
1
2
3
4
5
6
7
8
9
10
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

[self.window makeKeyAndVisible];

self.window.rootViewController = [[ViewController alloc] init];

return YES;
}

这样,当我们创建这个 ViewController 的实例的时候,application 的 keyWindow 就是存在的了。

5、然后我们再重复第 3 步:

1
2
3
4
2015-12-29 17:38:08.024 UIAlertViewDemo[4227:502055] origin key window: 0x7ff742c2b910
2015-12-29 17:38:09.543 UIAlertViewDemo[4227:502055] when clicked key window: 0x7ff74540a6f0
2015-12-29 17:38:09.543 UIAlertViewDemo[4227:502055] alert view will dismiss key window: 0x7ff74540a6f0
2015-12-29 17:38:09.956 UIAlertViewDemo[4227:502055] alert view did dismiss key window: 0x7ff742c2b910

通过对比内存地址,我们可以得出:

  1. UIAlertView 实例在 show 之前,并没有添加到父 view 上。
  2. 只有当调用 - (void)show; 方法的时候,系统才会新建一个 UIWindow 实例并把 alertView 实例添加上去,并把这个新建的 window 设为 application 的 keyWindow,并保存 originKeyWindow。
  3. 当 alertView 实例从屏幕上消失时,系统会依次销毁 alertView 和新建的 window,然后把之前保存的 originKeyWindow 重新设为 application 的 keyWindow。

嗯,验证通过。我真是个天才哈哈哈。

结语

我们平时做开发呢,要多思考,多总结,不要一味地埋头苦敲。

iOS SDK 为我们封装了很多属性和方法,我们在调用这些属性和方法的时候,不要只图方便,也要多思考系统方法的实现。

死记知识点这种做法太傻,你也记不住,在理解中学习才是最好的成长方式。

勘误

之前对该问题探讨的结论如上,偶然一回首,发现了当时的一些不正确的认识,现勘误如下(主要是从生命周期的角度来重新认识和学习):

首先,UIWindow 实例如果不被持有,就会被释放。当然任何实例都应该是这样。
当 alertView show 的时候,新的 window 被初始化并被设为 UIApplication 单例的 keyWindow,原有的开发者在 AppDelegate 单例中设置的 window 并没有被释放并不是因为上文所认为的 并保存 originKeyWindow,而是因为该 window 是 AppDelegate 单例的全局变量,被它持有,所有必然不会被释放掉。
当 alertView hide 完成,原有的开发者在 AppDelegate 单例中设置的 window 会被重新设置为 UIApplication 单例的 keyWindow,我们可以知道此时该 window 被 AppDelegate 单例和 UIApplication 单例所共同持有,这是没问题的。而 alertView 所在的 window 因为不被 UIApplication 单例所持有,失去了 keyWindow 的地位,同时它也不再被任何对象所持有,所以会被直接释放。

子曰:“温故而知新,可以为师矣。”

附:Demo 参考

https://github.com/iTofu/UIAlertViewDemo

另附上完整的开源框架:LCActionSheet

联系与捐赠

  • Mail: echo bGVvZGF4aWFAZ21haWwuY29tCg== | base64 -D
  • GitHub: iTofu