一个程序从main函数开始启动。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
可以看到main函数会调用UIApplicationMain
函数,它的四个参数的意思是:
argc: 代表程序在进入main函数时的参数的个数。默认为1。
argv: 代表包含的各个参数。默认为程序的名字。
principalClassName: UIApplication或者它的子类的名字, 如果传入的是nil, 则表示UIApplication的名字, 即@"UIApplication"。
delegateClassName: UIApplication的代理的名字。
在UIApplicationMain函数中,根据传入的UIApplication名称和它的代理的名称,会主要做下面的事情:
- 根据传入的名称创建UIApplication对象。
- 根据传入的代理名称创建UIApplication代理对象。
- 开启事件循环(如果不进行循环,那么在main函数结束后程序就结束了。要保证程序创建后可以一直存在)。
- 解析Info.plist文件。
- 会在Info.plist文件里查找
Main storyboard file base name
这个Key对应的Value是否有值。如果有值,则表示之后会通过Storyboard加载控制器,AppDelegate会接收到didFinishLaunchingWithOptions消息(程序启动完成的时候),此时Storyboard会进行一系列的加载操作(后面会具体说);如果没有值,则不会通过Storyboard加载控制器,接着AppDelegate会接收到didFinishLaunchingWithOptions消息(程序启动完成的时候),在这个时候需要我们通过代码的方式加载控制器。 - 注意Info.plist中
Main storyboard file base name
这个Key并不是真正的Key,而是苹果为了增强可读性才这样写的,真正的Key为UIMainStoryboardFile
(可以通过Info.plist文件的源代码查看)。 - 这就是在想要用代码方式创建控制器而不是Storyboard创建控制器的时候为什么先要将
Main Interface
设置为空白,这样在解析Info.plist文件的时候才会知道不通过Storyboard创建控制器。 - 由此可以知道,解析Info.plist文件这一操作主要是看我们用的是Storyboard方式加载还是代码的方式加载。默认
Main storyboard file base name
为Main
,也就是通过Storyboard方式加载控制器。
- 会在Info.plist文件里查找
现在具体分析一下,通过Storyboard方式加载控制器和代码方式加载控制器。
通过Storyboard
通过Storyboard,主要做了下面的事情(这些事情不需要我们做,是系统自动完成的,在程序启动完成的时候):
-
创建窗口。
创建一个UIWindow的实例用来显示界面。
-
设置窗口的根控制器。
- 根据Storyboard的设置,创建一个控制器。
- 并且设置这个控制器为之前创建的window的根控制器。
-
显示窗口。(相当于后面提到的makeKeyAndVisible)
设置self.window可见并且设置UIApplication的keyWindow。
在这一步中将根控制器的view添加到window上。
通过代码方式
通过代码的方式,需要我们在didFinishLaunchingWithOptions
方法中进行加载控制器的相关操作。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *viewController = [[UIViewController alloc] init];
self.window.rootViewController = viewController;
// 此时根控制器的view还没有加到self.window上
[self.window makeKeyAndVisible];
// 此时根控制器的view加到self.window上
return YES;
}
其实这里所做和系统所做是一样的。(相当于系统的做法)
首先创建窗口,得到一个正确的UIWindow实例对象用来显示界面。(self.window是系统自带的属性)
-
接着设置窗口的根控制器。
- 不再根据Storyboard中的设置加载,此时需要我们自己创建控制器。
- 设置这个控制器为self.window的根控制器。
- 注意这个时候根控制器的view还没有加到self.window上,当窗口要显示的时候,才会把窗口的根控制器的view添加到窗口。(可以输出self.window.subViews来验证)
-
显示窗口。
[self.window makeKeyAndVisible]
实际上做了下面的事:首先,将self.window设置为UIApplication的
keyWindow
,这么做是方便我们以后查看UIApplication的主窗口是哪一个。接着,让self.window可见,相当于执行的代码是:
self.window.hidden = NO;
这么做的原因是self.window默认hidden = YES,所以需要让其显示出来。
那么既然
makeKeyAndVisible
执行的是以上的操作,实际上将[self.window makeKeyAndVisible]
替换为self.window.hidden = NO
,那么界面也会正常显示出来,因为makeKeyAndVisible
内部就是这么做的。但是此时并没有设置UIApplication的keyWindow
,为了以后方便访问,还是用makeKeyAndVisible
更好一点。经过这一步,界面将要显示,此时根控制器的view会加到self.window上以正常显示。
-
这里有一点要注意:
系统创建的AppDelegate自带一个属性位于.h文件中:
@property (strong, nonatomic) UIWindow *window;
当用Storyboard的方式加载控制器,在应用启动完成的时候(didFinishLaunchingWithOptions)需要一个UIWindow的实例来显示界面,所以Apple提供了这个window属性。系统根据storyboard自动创建一个window,然后将window赋值给这个window属性,以保证完成之后的工作。
当用代码的方式加载控制器,同样的,首先也需要一个UIWindow的实例来显示界面,因为不使用Storyboard所以这次要我们自己创建window。此时有两种做法,第一种是在didFinishLaunchingWithOptions方法中创建一个UIWindow对象:
UIWindow *myWindow = [[UIWindow alloc] initWithFrame:...];
但是如果用这种方法运行程序会发现界面依然无法显示出来,因为此时
myWindow
是一个局部变量
,当didFinishLaunchingWithOptions方法执行完毕这个变量就会销毁。所以更好的办法是直接使用系统提供的window属性:self.window = [[UIWindow alloc] initWithFrame:...];
之前的例子也是这么做的。
另外,仔细观察会发现这个window属性的修饰符是
strong
,而不是weak
。想想之前使用weak
来修饰一个控件是因为这个控件会被加到一个view中,这个view的subViews数组会有强引用指向控件,所以用weak
是没有问题的。现在这种情况,因为window控件不会被加到其他view中,即没有其他的强指针指向这个对象,所以在创建的时候需要将修饰符设置成strong
以保证创建出的window不会被销毁。(Apple创建的window属性的修饰符是strong
)
UIWindow的补充
window是有层级的,并且可以有多个window同时存在。比如:状态栏就是一个window,键盘也是一个window。
可以通过设置UIWindow的对象的windowLevel
属性来调整层级。
self.window.windowLevel = UIWindowLevelStatusBar;
window共有三种等级:UIWindowLevelNormal
,UIWindowLevelStatusBar
UIWindowLevelAlert
。如果三种等级同时出现在屏幕上,那么alert在最上面,statusBar在中间,normal则在最下面。
注意:如果一个程序中有多个window,控制器默认会把状态栏隐藏。
解决办法:关闭控制器对状态栏的控制,(为Info.plist增加View controller-based status bar appearance
这个key并设置为NO)这样这些window以及状态栏就可以按层级关系正常显示。