UIWindow詳解

weixin_34050427發表於2017-09-18

前言

最近在做一個通知彈框的需求,應用到了UIWindow,之前沒有研究過,趁著這次機會了解下UIWindow。簡書上發現一篇好文章,特轉載過來,感謝作者。
作者:xx_cc連結:http://www.jianshu.com/p/af2a6a438a0a
應用內通知的一個demo: https://github.com/terryworona/TWMessageBarManager

1.UIWindow簡介

1、UIWindow是一種特殊的UIView,通常在一個app中至少會有一個UIWindow。
2、iOS程式啟動完畢後,建立的第一個檢視控制元件就是UIWindow,接著建立控制器的View,最後將控制器的View新增到UIWindow上,於是控制器的View就顯示在螢幕上了。
3、一個iOS程式之所以能顯示在螢幕上,完全是因為它有UIWindow,也就是說,沒有UIWindow就看不到任何UI介面。
4、狀態列和鍵盤都是特殊的UIWindow。

那麼UIWindow是如何將View顯示到螢幕上的呢

這裡有三個重要的物件UIScreen,UIWindow,UIView。
UIScreen物件識別物理螢幕連線到裝置
UIWindow物件提供繪畫支援給螢幕
UIView執行繪畫,當視窗要顯示內容的時候,UIView繪畫出他們的內容並附加到視窗上。

這樣View就顯示在視窗上了

2.UIWindow的建立

1.UIWindow是什麼時候建立的?

我們可以發現,當我們新建一個專案,直接在stroyboard為view設定一個背景顏色,然後執行專案,就能看到換了背景顏色的view,這說明系統已經幫我們建立了一個UIWindow,那麼這個UIWindow是什麼時候建立的?

我們找到程式的入口main函式,來看程式的啟動過程

int main(int argc, char * argv[])  {
 @autoreleasepool {
      return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

此時我們可以根據UIApplicationMain函式了解程式啟動的過程

1、根據傳遞的類名建立UIApplication物件,這是第一個物件
2、建立UIApplication代理物件,並給UIApplicaiton物件設定代理
3、開啟主執行迴圈 main events loop處理事件,保持程式一直執行
4、載入info.plist,判斷是否指定mian(xib 或者 storyboard)如果指定就去載入

當我們把指定的Main Interface 中mian給刪除的時候,重新執行程式,就會發現我們之前設定的view沒有辦法顯示了。


2139357-aee1f38fa85c85fb.png
Main Interface 中 Main刪除

此時我們基本可以想到,UIWindow應該是在載入storyboard的時候系統建立的,那麼系統是如何載入storyboard的呢?
系統在載入storyboard的時候會做以下三件事情

1、建立視窗
2、載入mian.storyboard 並例項化view controller
3、分配新檢視控制器到視窗root viewcontroller,然後使視窗顯在示螢幕上。

因此,當系統載入完info.plist,判斷後發現沒有main,就不會載入storyboard,也就不會幫我們建立UIWindow,那麼我們需要自己在程式啟動完成的時候也就是在didFinishLaunchingWithOptions方法中建立。

2.如何建立UIWindow?

首先根據系統載入storyboard時做的三件事情,我們可以總結出UIWindow建立步驟

1、建立視窗物件
2、建立視窗的根控制器,並且賦值
3、顯示視窗

並且我們在AppDelegate.h中發現屬性window

@property (strong, nonatomic) UIWindow *window;

那麼我們來看一下如何建立UIWindow

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  //建立視窗物件
  self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds]; 
  //建立視窗的根控制器,並且賦值 
  UIViewController *rootVc = [[UIViewController alloc]init];    
  self.window.rootViewController = rootVc; 
  //顯示視窗 
  [self.window makeKeyAndVisible];
  return YES;
}

視窗顯示注意點:
1、我們看到系統為我們建立的window屬性是strong強引用,是為了不讓視窗銷燬,所以需要強引用
2、視窗的尺寸必須設定,一般設定為螢幕大小。
3、[self.window addsubview:rootVc.view];可直接將控制器的view新增到UIWindow中,並不理會它對應的控制器,但是這種方法違背了MVC原則,當我們需要處理一些業務邏輯的時候就很麻煩了。
4、當發生螢幕旋轉事件的時候,UIapplication物件會將旋轉事件傳遞給UIWindow,UIWindow又會將旋轉事件傳遞給它的根控制器,由根控制器決定是否需要旋轉。UIapplication物件 -> UIWindow -> 根控制器。([self.window addsubview:rootVc.view];沒有設定根控制器,所以不能跟著旋轉)。
5、設定根控制器可以將對應介面的事情交給對應的控制器去管理。

那麼[self.window makeKeyAndVisible];這個方法為什麼就能顯示視窗呢?我們來看一下[self.window makeKeyAndVisible];的底層實現了哪些功能

1、可以顯示視窗
2、成為應用程式的主視窗

當我們不呼叫這個方法,列印self.window。

UIWindow: 0x7f920503cc80; frame = (0 0; 414 736); hidden = YES; gestureRecognizers = <NSArray: 0x7f92050332a0>; layer = <UIWindowLayer: 0x7f920503ad50>>

我們可以看到hidden = YES;那麼hidden = NO就可以顯示視窗了另外,我們在[self.window makeKeyAndVisible];前後分別輸出一下application.keyWindow

NSLog(@"%@",application.keyWindow);
[self.window makeKeyAndVisible]; 
NSLog(@"%@",application.keyWindow);

列印內容

UIWindow[6259:1268399] (null)
UIWindow[6259:1268399] <UIWindow: 0x7fefdb529b30; frame = (0 0; 414 736); gestureRecognizers = <NSArray: 0x7fefdb529e40>; layer = <UIWindowLayer: 0x7fefdb529ae0>>

我們可以看到呼叫[self.window makeKeyAndVisible];方法之後application.keyWindow就有值了,那麼[self.window makeKeyAndVisible];的底層實現就很明顯了。

1、可以顯示視窗self.window.hidden = NO;
2、成為應用程式的主視窗application.keyWindow = self.window
,這個會報錯,因為application.keyWindow是readonly,所以我們沒有辦法直接賦值。

3.通過storyboard載入控制器

剛才我們提到過系統在載入storyboard的時候會做以下三件事情

1、建立視窗
2、載入mian.storyboard 並例項化view controller
3、分配新檢視控制器到視窗root viewcontroller,然後使視窗顯在示螢幕上。

那麼我們用程式碼來模擬實現一下

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
     // 1.建立視窗 
     self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 
     
     // 2.載入main.storyboard,建立main.storyboard描述的控制器 
     // UIStoryboard專門用來載入stroyboard
     // name:storyboard名稱不需要字尾 
     UIStoryboard *stroyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; 
     
     // 載入sotryboard描述的控制器 
     // 載入箭頭指向的控制器 
     UIViewController *vc = [stroyboard instantiateInitialViewController];
     //根據繫結標識載入 
     //UIViewController *vc = [stroyboard instantiateViewControllerWithIdentifier:@"red"]; 
     
     // 設定視窗的根控制器
     self.window.rootViewController = vc;

     // 3.顯示視窗
     [self.window makeKeyAndVisible]; 
     
     return YES;
}

4.通過xib載入控制器

通過xib載入控制器和通過storyboard載入控制器類似,直接上程式碼

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
     self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 
     // 建立視窗的根控制器 
     // 通過xib建立控制器
     ViewController *vc = [[ViewController alloc] initWithNibName:@"VC" bundle:nil];
     //vc.view.backgroundColor = [UIColor redColor]; 
     self.window.rootViewController = vc;
     [self.window makeKeyAndVisible];
     return YES;
}

3.UIWindow的層級

UIWindow是有層級的,層級高的顯示在最外面,當層級相同時,越靠後呼叫的顯示在外面。

UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal; //預設,值為0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert; //值為2000 
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar ; // 值為1000

所以UIWindowLevelNormal < UIWindowLevelStatusBar< UIWindowLevelAlert
並且層級是可以做加減的self.window.windowLevel = UIWindowLevelAlert+1;

關於UIApplication的介紹可以看這篇文章iOS-UIApplication詳解