【iOS】应用程序启动原理及UIWindow的使用

iOS应用程序(App)从开始运行到界面展示经历了哪些方法?界面是怎么显示到屏幕上的?理解了这些对我们后续的开发很有帮助。

App从启动到我们熟知的AppDelegate代理方法经历了一下几个步骤

load方法

主要讲的是控制器的load方法,App执行main函数之前会优先执行load方法。我们可以在这里交换一些控制器的生命周期及方法,经常用到的埋点基本上都是通过该方式进行处理的。

1
2
3
+ (void)load {
// 自定义处理
}

main函数

main函数在工程根目录的main.m文件中。也是程序的唯一入口函数。

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

我们看下UIApplicationMain的介绍。该函数的作用就是创建一个应用程序对象并设置代理,然后设置主事件循环。如果应用程序的Info.plist指定了一个nib作为程序主要入口,这个nib将会被加载到程序中。最后还补充到,尽管声明了返回类型,但该函数永远不会返回(这也意味着主循环是一个死循环,直到程序被杀死才会停止)。

1
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);

参数说明:

  • argcargv中参数的个数,通常是main对应的参数。
  • argv: 参数列表,通常也是main对应的参数。
  • principalClassName
    • UIApplication或子类名称
    • 如果设置为空,将会从Info.plist中查找是否指定了该值(查找key:Main storyboard file base name)。
    • 如果在Info.plist找到配置的值(即Main),就会创建一个UIWindow,把Main.storyboard中箭头指向的控制器view添加到UIWindow上;
    • 如果在Info.plist没有找到,将会直接使用UIApplication,同样会创建上面描述的UIWindow
  • delegateClassNameprincipalClassName的代理类名称,初始化principalClassName完成后代理类会被实例化(开始走AppDelegate的代理方法)。

UIWindow

UIWindow是一个特殊的UIView,一般情况下一个应用程序至少有一个UIWindow

iOS程序启动结束后,会自动创建一个UIWindow,然后设置跟控制器,把跟控制器的View添加到这个UIWindow上,最后展示在屏幕上。UIWindow更像是一棵树的根节点,所有的子节点都从根节点或根的子节点出发。所以说如果没有UIWindow,用户就看不到任何界面。

如果设置了Main.storyboard启动,程序就会把Main.storyboard中初始化箭头指向的控制器view添加到UIWindow上。UIWindow的跟控制器就不需要我们自己设置了,系统会自动关联。

UIWindow为什么要设置rootViewController?

设置rootViewController从程序设计角度来讲会更加合理,并且能够起到解耦的效果。从内存角度看的话,就是一个强引用指针。

手动创建UIWindow

  1. Info.plist中的Main storyboard file base name对应的value值Main清空,或者直接把这行移除。

  2. AppDelegate加载完成的代理方法中创建UIWindow,并显示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 创建UIWindow
    UIWindow *window = [[UIWindow alloc] init];
    window.backgroundColor = [UIColor blueColor];

    // 设置UIWindow的根控制器
    UIViewController *vc = [[UIViewController alloc] init];
    vc.view.backgroundColor = [UIColor redColor];
    window.rootViewController = vc;

    // 让当前window成为主window并显示到屏幕上
    [window makeKeyAndVisible];

    // 强引用,否则会被释放
    self.window = window;

    return YES;
    }

makeKeyAndVisible原理

UIApplicaton属性keyWindow指向的就是makeKeyAndVisible的最后一次调用者。

我们通过[UIApplication sharedApplication].keyWindow查看下创建的UIWindow做了哪些操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
UIWindow *window = [[UIWindow alloc] init];
UIViewController *vc = [[UIViewController alloc] init];
window.rootViewController = vc;

NSLog(@"window:%@", window);
NSLog(@"before:%@", [UIApplication sharedApplication].keyWindow);

[window makeKeyAndVisible];

NSLog(@"after:%@", [UIApplication sharedApplication].keyWindow);

self.window = window;

return YES;
}

控制台输出:
window:<UIWindow: 0x7f9d1c506eb0; frame = (0 0; 375 667); hidden = YES; gestureRecognizers = <NSArray: 0x600002a42a90>; layer = <UIWindowLayer: 0x60000244c800>>

before:(null)

after:<UIWindow: 0x7f9d1c506eb0; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x600002a42a90>; layer = <UIWindowLayer: 0x60000244c800>>

通过控制台输出可以看到,keyWindow和自己创建的window是同一个对象,只是调用makeKeyAndVisible之前windowhidden属性是YES,调用之后hidden属性没有了(其实就是NO)。

总结makeKeyAndVisible作用:

  • 设置调用者window为当前应用程序的keyWindow
  • 显示到屏幕上;
  • 把根控制器的view添加到自己上面。

伪代码:

1
2
3
4
5
6
// UIWindow.m
- (void)makeKeyAndVisible {
[UIApplication sharedApplication].keyWindow = self;
self.hidden = NO;
[self addSubview:self.rootViewController.view];
}

不调用makeKeyAndVisible,设置windowhiddenNO时也可以显示到屏幕,只是keyWindow为空

伪代码:

1
2
3
4
5
6
7
8
9
// UIWindow.m
- (void)setHidden:(BOOL)hidden {
_hidden = hidden;
if (_hidden) {
[self.rootViewController.view removeFromSuperview];
} else {
[self addSubview:self.rootViewController.view];
}
}

注意:UIWindow的根控制器必须设置且不能给空,否则报错。iOS9之后创建的UIWindow不需要设置frame了。

多个UIWindow

系统给我们提供UIWindowwindowLevel层级有三个常量:

1
2
3
UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar

windowLevelCGFloat类型,默认值是0.0,我们可以通过他设置多个window之间的层级。

三个常量之间的层级优先级是:UIWindowLevelNormal(值:0.0) > UIWindowLevelStatusBar(值:1000.0) > UIWindowLevelAlert(值:2000.0)

  • UIWindowLevelNormal:创建的window默认都是这个层级;
  • UIWindowLevelStatusBar:状态栏的层级;
  • UIWindowLevelAlert:键盘、系统弹框都属于这个层级。

新建window作为状态栏背景(UIWindowLevelNormal

不需要设置windowLevel,或设置windowLevel小于UIWindowLevelStatusBar即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] init];
self.window.backgroundColor = [UIColor blueColor];
UIViewController *vc = [[UIViewController alloc] init];
vc.view.backgroundColor = [UIColor redColor];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];

UIWindow *statusBarWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 20.0)];
UIViewController *statusBarVC = [[UIViewController alloc] init];
statusBarVC.view.backgroundColor = [UIColor yellowColor];
statusBarWindow.rootViewController = statusBarVC;
[statusBarWindow makeKeyAndVisible];
self.statusBarWindow = statusBarWindow;
return YES;
}

新建window覆盖状态栏(UIWindowLevelStatusBar

只需设置windowLevel大于等于UIWindowLevelStatusBar即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] init];
self.window.backgroundColor = [UIColor blueColor];
UIViewController *vc = [[UIViewController alloc] init];
vc.view.backgroundColor = [UIColor redColor];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];

UIWindow *statusBarWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 20.0)];
UIViewController *statusBarVC = [[UIViewController alloc] init];
statusBarVC.view.backgroundColor = [UIColor yellowColor];
statusBarWindow.rootViewController = statusBarVC;
statusBarWindow.windowLevel = UIWindowLevelStatusBar;
[statusBarWindow makeKeyAndVisible];
self.statusBarWindow = statusBarWindow;
return YES;
}

window覆盖(UIWindowLevelAlert

只需设置windowLevel大于等于UIWindowLevelAlert即可。

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
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] init];
self.window.backgroundColor = [UIColor blueColor];
UIViewController *vc = [[UIViewController alloc] init];
vc.view.backgroundColor = [UIColor redColor];
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];

UIWindow *statusBarWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 20.0)];
UIViewController *statusBarVC = [[UIViewController alloc] init];
statusBarVC.view.backgroundColor = [UIColor yellowColor];
statusBarWindow.rootViewController = statusBarVC;
statusBarWindow.windowLevel = UIWindowLevelStatusBar;
[statusBarWindow makeKeyAndVisible];
self.statusBarWindow = statusBarWindow;

UIWindow *alertWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 10, [UIScreen mainScreen].bounds.size.width, 200.0)];
UIViewController *alertVC = [[UIViewController alloc] init];
alertVC.view.backgroundColor = [UIColor blueColor];
alertWindow.rootViewController = alertVC;
alertWindow.windowLevel = UIWindowLevelAlert;
[alertWindow makeKeyAndVisible];
self.alertWindow = alertWindow;

UIWindow *alertWindow2 = [[UIWindow alloc] initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 200.0)];
UIViewController *alertVC2 = [[UIViewController alloc] init];
alertVC2.view.backgroundColor = [UIColor greenColor];
alertWindow2.rootViewController = alertVC2;
alertWindow2.windowLevel = UIWindowLevelAlert;
[alertWindow2 makeKeyAndVisible];
self.alertWindow2 = alertWindow2;
return YES;
}

总结:如果多个window设置了同级别,后添加的会覆盖到之前添加的上面。在实际开发中,除了首次创建的视图载体window,建议直接使用UIWindowhidden属性,少用makeKeyAndVisible,这样可以有效避免UIApplication属性keyWindow的混乱。