接力篇:UIWindow!

前言

前面写了篇有关于 UIApplication 的博客,后面谈了一些关于程序的启动过程,对象的创建。到后面提及了 UIWindow,考虑到篇幅的问题,就搁置到这一篇博客来写。主要也是因为前几天在项目里面,有一BUG困扰我挺久的,内容与我今天要写的 UIWindow 相关~文末会分享给大家!

什么是 UIWindow

UIWindow 继承于 UIView,通常在一个程序中只会有一个 UIWindow,UIWindow在程序中主要起到三个作用:

  1. 作为容器,包含app所要显示的所有视图

  2. 传递触摸消息到程序中view和其他对象

  3. UIViewController 协同工作,方便完成设备方向旋转的支持

创建 UIWindow

使用 storyboard

如今使用 xcode 开发 iOS app,xcode 会为我们自动创建带有 storyboard 的项目。这往往会让新手程序员,忽略了我们今天的主角—- UIWindow ,因为关于 UIWindow 的创建过程已经是自动的了,并且很容易被我们给忽略了。

当用户点击应用程序图标的时候:

  • 先执行Main函数,执行 UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给 application

  • info.plist 文件中我们可以看这样的一项参数 “Main storyboard file base name”,指定它为 main storyboard,开启一个事件循环。

  • 当程序加载完毕,他会调用代理的 didFinishLaunchingWithOptions: 方法。在调用 didFinishLaunchingWithOptions: 方法之前,会加载 storyboard,在加载的时候实例化一个 UIWindow,接下来会创建箭头所指向的控制器(root),把该控制器设置为 UIWindow 的根控制器

  • 将 window 显示出来,即看到了运行后显示的界面

说明:因为一个项目里面可能会有多个 storyboard 文件,所以必须指定哪个是 main;一个 storyboard 文件里又可能有多个 viewController,所以 storyboard 里也要指明哪个是root。

使用nib文件:

如果是 iOS5 之前,还没有 storyboard,是使用 nib 文件来代替的。将一个window对象拖拽到Interface Builder 文件中,并将这个文件指定为app的 main interface。那么在 app 启动的时候,iOS也会自动创建 window 对象。
为了确保 window 的大小与屏幕大小吻合,需要在 Interface Builder 中对 window 对象勾选 Full Screen at Launch 这个属性。

手动创建

AppDelegate.m 文件的方法 - (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

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

ViewController *rootViewController = [[ViewController alloc] init];
rootViewController.view.frame = self.window.bounds;
self.window.rootViewController = rootViewController;

[self.window makeKeyAndVisible];

return YES;
}

上面的代码,我创建实例化了一个 ViewController,将他赋值给 windowrootViewController,实际上,将 view 添加到 UIWindow 上是有一下两种方法的:

  1. 直接将控制器的 view 添加到 UIWindow 中,并不理会它对应的控制器
1
[self.window  addsubview:vc.view];
  1. 设置 UIWindow 的根控制器,自动将 rootviewcontroller 的 view 添加到 window 中,负责管理rootviewcontroller 的生命周期
1
self.window.rootviewcontroller = vc;

上面第一种方法我们很少在代码里见过,因为它有一个很明显的弊端,直接将 view 通过 addSubview 方式添加到 window 中,程序负责维护 view 的生命周期以及刷新,但是并不会为去理会 view 对应的 ViewController,因此采用这种方法将 view 添加到 window 以后,我们还要保持 view 对应的 ViewController 的有效性,不能过早释放。

而第二种方法,在官方文档里有这样一张示意图,证明了 设置 UIWindow 的根控制器,自动将 rootviewcontroller 的 view 添加到 window 中

之前的博客:《我所知道的UIApplication》 也提及过app的启动:应用程序启动之后,先创建 Application,再创建它的代理,之后创建 UIWindow。UIWindow 继承自UIview

而,UIWindow 的创建代码是在 delegate 里实现的。

windowLevel 与 keyWindow

windowLevel

UIWindow 在显示的时候会根据 UIWindowLevel 进行排序的,即 Level 高的将排在所有 Level比他低的层级的前面。下面我们来看UIWindowLevel的定义:

UIWindow 有三个层级,分别是 NormalStatusBarAlert
打印输出他们三个这三个层级的值我们发现从左到右依次是0,1000,2000.

也就是说 Normal 级别是最低的,StatusBar 处于中等水平,Alert 级别最高。

而通常我们的程序的界面都是处于 Normal 这个级别上的,系统顶部的状态栏应该是处于 StatusBar 级别,UIActionSheetUIAlertView 这些通常都是用来中断正常流程,提醒用户等操作,因此位于 Alert 级别。

keyWindow

这一概念有点难以理解,先看一下官方文档是怎么说的:

A window is considered the key window when it is currently receiving keyboard and non touch-related events. Whereas touch events are delivered to the window in which the touch occurred, events that don’t have an associated coordinate value are delivered to the key window. Only one window at a time can be key.

翻译一下,大概是:

  • keyWindow 是指定的用来接收键盘以及非触摸类的消息

  • 触摸事件发生哪个 window,就交由那个 window 来进行处理。

  • 如果事件不知道是在哪个 window 发生的,直接交给 keyWindow 处理。

  • 程序中每一个时刻只能有一个 window 是 keyWindow。

细思极恐,KeyWindow 还真是管得多:

获取 UIWindow

展示 UIWindow

  1. 让窗口成为主窗口,并且显示出来。有这个方法,才能把信息显示到屏幕上。

    1
    [self.window makekeyandvisible];

因为 UIWindowmakekeyandvisible 这个方法,可以让这个 Window 凭空的显示出来,而其他的 view 没有这个方法,所以它只能依赖于 UIWindow ,Window 显示出来后,view 才依附在Window上显示出来。

  1. 让uiwindow成为主窗口,但不显示。

    1
    [self.window make keywindow];

获取 UIWindow 信息

  1. 在本应用中打开的UIWindow列表,这样就可以接触应用中的任何一个 UIView 对象(平时输入文字弹出的键盘,就处在一个新的 UIWindow 中)

    1
    [UIApplication sharedApplication].windows
  2. (获取应用程序的主窗口)用来接收键盘以及非触摸类的消息事件的 UIWindow,而且程序中每个时刻只能有一个 UIWindow 是 keyWindow。

    1
    [UIApplication sharedApplication].keyWindow
  3. 获得某个 UIView 所在的 UIWindow

    1
    view.window

相关的 notification

观察 UIWindow 的文档,我们可以发现里面有四个关于 window 变化的通知:

  • UIWindowDidBecomeVisibleNotification
  • UIWindowDidBecomeHiddenNotification
  • UIWindowDidBecomeKeyNotification
  • UIWindowDidResignKeyNotification

  这四个通知对象中的 object 都代表当前已显示(隐藏),已变成 keyWindow(非keyWindow)的window 对象,其中的 userInfo 则是空的。于是我们可以注册这个四个消息,再打印信息来观察keyWindow 的变化以及 window 的显示,隐藏的变动。

分享一个我近期解决的bug

这个bug是这样的:

我在公司项目里面自己写了一个封装好的 PickerViewController,我定义了一个 showPickerView:(UIView *)pickerView completion:(void (^)(void))completion,最最最神奇的地方就是,当我直接在代码里调用,或者是将它作为 buttonselector,都能正常的显示出pickerView。

但是,当我想从 alertview 的按钮调用该方法将 pickerView 显示出来却一直不成功。

忙活了一个下午才知道了原因,还是太嫩了啊。

原来我在方法 showPickerView:(UIView *)pickerView completion:(void (^)(void))completion 中:

1
UIViewController *topController = [[[UIApplication sharedApplication] keyWindow] rootViewController];

用这样的方法去获取 根控制器,这本来在正常情况下没有什么错误。

但是偏偏到了 alertview 底下就不行,原因是:当 alertview非触摸事件 触发显示时,keywindow发生了改变!!!所以不能通过它去获得根控制器!!!

我们来看一段代码:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

//
// ViewController.m
// TestForWindow
//
// Created by lz on 16/7/10.
// Copyright © 2016年 lz. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()

// observer
- (void)registerObserver;
- (void)unregisterObserver;

// observer handler
- (void)windowBecomeKey:(NSNotification*)noti;
- (void)windowResignKey:(NSNotification*)noti;
- (void)windowBecomeVisible:(NSNotification*)noti;
- (void)windowBecomeHidden:(NSNotification*)noti;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
[self registerObserver];
}

- (void)viewDidUnload
{
[super viewDidUnload];
[self unregisterObserver];
}

- (void) viewDidAppear:(BOOL)animated
{
[self presentAlertView];
}

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];

}

- (void)presentAlertView
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alert View"
message:@"Hello Wolrd, i'm AlertView!!!"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:@"Cancel", nil];
[alertView show];

}

- (void)registerObserver
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowBecomeKey:) name:UIWindowDidBecomeKeyNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowResignKey:) name:UIWindowDidResignKeyNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowBecomeVisible:) name:UIWindowDidBecomeVisibleNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowBecomeHidden:) name:UIWindowDidBecomeHiddenNotification object:nil];
}

- (void)unregisterObserver
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

#pragma mark -
#pragma mark Notification handler

- (void)windowBecomeKey:(NSNotification*)noti
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSLog(@"current window count %ld", windows.count);
NSLog(@"keyWindow:%@",[[UIApplication sharedApplication] keyWindow]);
}

- (void)windowResignKey:(NSNotification*)noti
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSLog(@"current window count %ld", windows.count);
NSLog(@"keyWindow:%@",[[UIApplication sharedApplication] keyWindow]);
}

- (void)windowBecomeVisible:(NSNotification*)noti
{
NSArray *windows = [UIApplication sharedApplication].windows;
NSLog(@"current window count %ld", windows.count);
NSLog(@"keyWindow:%@",[[UIApplication sharedApplication] keyWindow]);
}
- (void)windowBecomeHidden:(NSNotification*)noti
{

NSArray *windows = [UIApplication sharedApplication].windows;
NSLog(@"current window count %ld", windows.count);
NSLog(@"keyWindow:%@",[[UIApplication sharedApplication] keyWindow]);
}

@end

运行结果如下:

在iOS中的 UIActionSheetUIAlertView 其实是显示在另一个 window 上的。并看出 keywindow 已经从 UIWindow 变为 _UIAlertControllerShimPresenterWindow了。
而当 UIAlertView 消失之后,keywindow 又会重新变为 UIWindow

我所遇到的bug在于,还没有在 keywindow 切换回 UIWindow 时,就利用他来获取 rootViewController,所以没能获取到,也就不能正常的 show 了。

解决的办法其实不难,只需换个方法来获取 rootViewController

1
UIViewController *topController = [[UIApplication sharedApplication].delegate.window rootViewController];

补充一点:项目里面是为了兼容更低版本的iOS系统,使用了 UIAlertView;如果我们使用的是 UIAlertController,就不会有所谓 UIWindow 的切换了。上面也就不会出现找不到 rootViewController 的情况了。

最后的话

大约数了数,我竟然有大概10天没玩游戏了~ 简直不敢相信!!