UIWindow詳解及踩坑

一、問題背景

最近需求量放緩,想起了以前曾經(jīng)later的小需求,也就是彈出來的AlertView中間的文本框輸入一些信息,如果輸入的信息為空,則把確定按鈕置灰。而UIKit里沒有開放修改AlertView中subview的API,且在iOS8以后也沒辦法通過subViews屬性拿到AlertView的子view。所以現(xiàn)在想寫一個(gè)AlerView,可以開放出一些項(xiàng)目中可能用的到的接口,并且最大程度的保留原UIAlertView的接口不變。
開始寫沒多久,在自測show方法的時(shí)候出現(xiàn)了一點(diǎn)問題(調(diào)用show函數(shù)可以直接將AlertView顯示出來,而不用傳入當(dāng)前的view參數(shù),即不加入當(dāng)前的View hierarchy)。因?yàn)楫?dāng)時(shí)的實(shí)現(xiàn)方案跟在項(xiàng)目中寫圖片預(yù)覽控件的思路一樣,通過UIApplication拿到window列表,然后取第0個(gè)window,直接添加AlertView上去,結(jié)果發(fā)現(xiàn)顯示不出來,被當(dāng)前ViewController給蓋住了(棕色的一塊就是蓋在AlertView上方的ViewController的rootview)。

</img>

那之前一直使用的方案為什么在這里行不通了呢?再加上前不久出現(xiàn)的測試包上點(diǎn)擊crash問題(問題描述:從聊天管理或者卡片設(shè)置背面的+號進(jìn)入邀請人界面,選擇分享到發(fā)現(xiàn)。然后選擇取消發(fā)布,回到聊天管理或者卡片設(shè)置頁,此時(shí)再點(diǎn)擊+號,發(fā)生crash)。都跟UIWindow這個(gè)玩意兒有關(guān)。于是最近便圍繞了以下三個(gè)問題進(jìn)行了研究。
(1)UIWindow究竟是什么?
(2)為什么在項(xiàng)目里使用的window直接添加View的方案在這里行不通了?
(3)為什么只有選擇分享到發(fā)現(xiàn),并取消發(fā)布之后會出現(xiàn)crash,而在其他的頁面跳轉(zhuǎn)不會出現(xiàn)UIWindow問題?

二、UIWindow概念

UIWindw定義了一個(gè)負(fù)責(zé)管理,協(xié)調(diào)一個(gè)App的View是如何顯示在設(shè)備屏幕上的窗口類,除非一個(gè)App可以顯示在一個(gè)外部的設(shè)備屏幕上,那么一個(gè)App只擁有一個(gè)窗口。UIWindow本身沒有標(biāo)題欄,關(guān)閉操作欄等任何的裝飾物,用戶不會看見,移動(dòng)或者是關(guān)閉它,這跟Mac OS上的window有很大的差別。
UIWindow的兩大主要功能是提供了一塊給View的顯示區(qū)域,并且負(fù)責(zé)分發(fā)各種事件給View,比如傳遞觸摸事件給各項(xiàng)View或者其它對象。而改變App的顯示內(nèi)容,可以改變UIWindow的rootView,而不需要去創(chuàng)建一個(gè)新的UIWindow。同時(shí),它還負(fù)責(zé)與ViewController協(xié)同去處理設(shè)備旋轉(zhuǎn)時(shí)的情況。
講到Window還必須要提的兩個(gè)概念是UIWindowLevel以及KeyWindow。UIWindowLevel是一個(gè)CGFloat值,現(xiàn)在UIKit定義了三種Level:

    UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;
    UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;
    UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar

UIWindowLevel為2D的iOS世界引入了Z軸的概念,它相當(dāng)于以屏幕為原地,以使用者為正方向的一根軸。值越小代表離使用者越遠(yuǎn),越大代表越靠近使用者。高Level的Window會蓋住低Level的window,若是兩者Level一樣則根據(jù)添加順序來決定,這類似于我們添加子View(UIWindow本來也就是UIView的子類)。而上面三個(gè)值分別是0.0,2000.0,1000.0,而大部分在App上使用的都是UIWindowLevelNormal,這也是每個(gè)Window被創(chuàng)建出來時(shí)的默認(rèn)值。
我們在創(chuàng)建一個(gè)新的window的時(shí)候,要讓它顯示出來必須要調(diào)用makeKeyAndVisible方法,讓window顯示出來,并讓它成為一個(gè)KeyWindow。KeyWindow是UIApplication的一個(gè)開放屬性,它是當(dāng)前App的主window,用來接收鍵盤輸入以及非觸摸事件(觸摸事件是傳遞給觸摸事件發(fā)生的window,不一定是keyWindow),或者是跟坐標(biāo)值無關(guān)的事件都會被傳遞給keyWindow。并且在同一時(shí)刻,只有一個(gè)window會成為keyWindow。但是需要注意一件事情,成為keywindow與windowLevel無關(guān),并不是windowLevel最高的window會成為keywindow。

三、UIWindow在App啟動(dòng)時(shí)扮演的角色

人這一輩子主要要回答三個(gè)問題,一是從哪里來,二是到哪里去,三是你是誰。那上面介紹了UIWindow是誰,那么現(xiàn)在就要介紹UIWindow從哪里來,并且它要到哪里去了。那我這兒就從Application啟動(dòng)開始講起。

(1)The Main Function

所有以C語言為基礎(chǔ)的程序的入口都是main函數(shù),iOS App也不例外。以下程序就是iOS App的main函數(shù):

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
 
int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

唯一不同的是你不用去寫main函數(shù),這是Xcode自動(dòng)創(chuàng)建的。這段代碼也很簡單,它唯一做的工作就是把控制權(quán)移交給UIKit framework,根據(jù)第三個(gè)參數(shù)principalClassName創(chuàng)建一個(gè)UIApplication對象,根據(jù)第四個(gè)參數(shù)創(chuàng)建一個(gè)AppDelegate對象。

(2) UIApplication

UIApplication是一個(gè)App的核心,它主要的職能是負(fù)責(zé)方便系統(tǒng)和App的交互,管理Event Loop進(jìn)行各項(xiàng)事件的處理,以及向自己的Delegate,即AppDelegate進(jìn)行一些關(guān)鍵事件的傳遞。
一個(gè)App只有一個(gè)UIApplication單例對象,可以通過[UIApplication sharedApplication]來獲得單例。它還能做一些應(yīng)用級別的事,比如設(shè)置桌面上App圖標(biāo)右上角的紅點(diǎn)數(shù)字,或者是使用openURL直接撥電話,發(fā)短信等。在此不做延伸。


(3)UIWindow

UIWindow是iOS啟動(dòng)之后,被創(chuàng)建的第一個(gè)視圖控件。它有可能是通過Interface Builder被創(chuàng)建出來的,也有可能是我們在AppDelegate中自定義創(chuàng)建出來的。當(dāng)它被創(chuàng)建,添加了rootView之后,一個(gè)App的界面最終被展示在用戶面前。而如果是自定義創(chuàng)建window時(shí),我們通常會使用window.rootViewController來為它添加rootView,值得注意的是,這句代碼僅僅是給UIWindow添加了rootViewController的view,或者說這是一種更加便利的方式來為UIWindow添加rootView,而這個(gè)rootViewController屬性并不是用來讓controller與UIWindow之間進(jìn)行通信的。
除此之外,UIWindow還負(fù)責(zé)與UIApplication一起負(fù)責(zé)傳遞Event給View以及ViewController。

四、問題解決

(1)AlertView的show方法問題

當(dāng)我第一次出現(xiàn)這個(gè)問題的時(shí)候,代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor brownColor];
    TDAlertView *alertView = [[TDAlertView alloc]initWithTitle:@"TDAlertView" message:@"test" delegate:self cancelButtonTitle:@"取消" textFieldPlaceHolders:@[@"賬號",@"密碼"] otherButtonTitles:@"圣誕",@"快樂",nil];
    [alertView show];
}

而TDAlertView的show方法代碼如下:

- (void)show{
    if (!self){
        return;
    }
    UIWindow *showWindow = [[[UIApplication sharedApplication]windows] objectAtIndex:0];
    [showWindow addSubview:self];
}

當(dāng)時(shí)的第一個(gè)猜測,認(rèn)為是不是不應(yīng)該放在viewDidLoad里,因?yàn)榇藭r(shí)viewController的rootView尚未生成,當(dāng)rootview生成之后,自然而然的就會蓋在AlertView上。于是后面把顯示代碼放在viewDidAppear里面,果然AlertView就正常顯示了。這樣問題算是初步解決。
但是我很快發(fā)現(xiàn)問題解決的并不徹底,因?yàn)槲矣衷嚵艘幌?,讓剛剛那個(gè)controller一出現(xiàn)就跳轉(zhuǎn)到controllerB,然后在controllerB里面的viewDidLoad里面添加顯示AlertView的代碼,發(fā)現(xiàn)AlertView竟然能顯示出來。除此之外,我還發(fā)現(xiàn)如果把當(dāng)前Window的rootViewController改為UINavgationController的話,那么在第一個(gè)ViewController的ViewDidLoad里面顯示AlertView也可以正常顯示了。
這說明問題viewDidLoad只是出現(xiàn)這問題的一個(gè)因素,還有一些因素影響著最終的顯示效果。而通過分析兩個(gè)UIViewController的不同顯示行為,可以分析得出很有可能是rootViewController的原因讓顯示出錯(cuò)。
而經(jīng)過上面第三節(jié)UIWindow中的描述,rootViewController是提供了UIWindow的rootView,后面顯示內(nèi)容的更改都是在rootView在這個(gè)層級的更改。而show方法則是在UIWindow上直接添加,則不屬于rootView這個(gè)層級,它與rootView屬于平級關(guān)系。所以當(dāng)在rootViewController的viewDidLoad里直接添加View時(shí),rootView是后面添加到UIWindow上的,所以它的層級高于AlertView。而在ViewControllerB的viewDidLoad里顯示AlertView時(shí),盡管ViewControllerB里面的view是后面加上去的,但是它屬于本來就低于AlertView的rootView hierarchy,所以AlertView會按預(yù)想的顯示出來。



上面兩張圖展示了View的層級關(guān)系,前面是沒有正常顯示的,后面的是正常顯示的。這也驗(yàn)證了上面的想法。而對于UINavgationController,它屬于container viewController。它會自己向UIWindow提供rootView,所以這保證了它的rootView肯定在最底層,故也可以正常顯示。
總結(jié):要分清楚UIWindow的rootView hierarchy與UIWindow作為UIView子類本身的view hierarchy,以免出現(xiàn)顯示的層級錯(cuò)誤。

(2)測試包點(diǎn)擊按鈕crash問題

這個(gè)問題很早就被發(fā)現(xiàn),并定位到是使用了topViewController。但是當(dāng)時(shí)在測試的時(shí)候發(fā)現(xiàn)很奇怪,只要一跳到發(fā)現(xiàn)的發(fā)布頁,再回來就會出問題,但是只要不跳到這個(gè)頁面,隨便跳哪也不會出這個(gè)問題。下面用一個(gè)GIF來演示一下這個(gè)bug。



看錯(cuò)誤信息發(fā)現(xiàn)是UIViewController沒有g(shù)oToPage方法所導(dǎo)致的crash。而取topViewController的方法摘要如下:

  if ([UIApplication sharedApplication].windows.count == 0) {
        return nil;
    }
    
    // find root view contoller from window
    UIWindow *window = [UIApplication sharedApplication].keyWindow;
    UIViewController *rootViewController = window.rootViewController;
    if (rootViewController == nil) {
        window = [UIApplication sharedApplication].windows[0];
        rootViewController = window.rootViewController;
    }

能看出來它是通過UIApplication拿到當(dāng)前的keyWindow,然后通過拿到keyWindow上的rootViewController,如果為空,再去拿windows列表底部的window,獲得它的rootViewController。然后通過打斷點(diǎn)發(fā)現(xiàn),在crash的時(shí)候,獲得的keyWindow是AlitripMonitorStatusBar,而這個(gè)東西就是那個(gè)左上角負(fù)責(zé)顯示上傳,下載數(shù)據(jù)的黑框,而這時(shí)再拿到的rootViewController就會出現(xiàn)問題。(順便提一句,通過[UIApplication sharedApplication]獲得的windows列表包含了所有可見或者不可見的非系統(tǒng)UIWindow,系統(tǒng)window包括最上面的statusBar等等。而windows列表的排序是按照windowLevel升序排列。)
那到底keyWindow是為什么會被變更呢?我最開始的想法是可能因?yàn)榭绻こ趟宰屪钌厦娴母佑种匦沦N了一層,并變成了keyWindow,但是整個(gè)工程里發(fā)現(xiàn)AlitripMonitorStatusBar只有在初始化的時(shí)候會調(diào)用makeKeyAndVisible,而且通過實(shí)驗(yàn),跳到其他工程并沒有出現(xiàn)這種crash,于是放棄了這種想法。然后我又想會不會是發(fā)布頁在退出時(shí)做了一些特殊的事情,可是通過看代碼也沒發(fā)現(xiàn)什么特殊的地方。
最終我采用跟蹤window變化的方法來確定問題所在,即在每個(gè)節(jié)點(diǎn),比如viewController的進(jìn)入,退出時(shí),而且由于第一次踩的坑,還在viewDidLoad和viewDidAppear里做了區(qū)分。最終發(fā)現(xiàn)在退出到邀請列表頁時(shí),viewDidLoad里的keyWindow變成了_UIAlertControllerShimPresenterWindow。根據(jù)名字很明顯能發(fā)現(xiàn)這是警告框所在的window,也就是發(fā)現(xiàn)發(fā)布頁唯一特殊的地方在于因?yàn)橐未_認(rèn),它喚起了UIActionSheet,而此時(shí)這個(gè)View出現(xiàn)在了_UIAlertControllerShimPresenterWindow,并讓它變成了keyWindow。然后由于這個(gè)window的消失,這個(gè)keyWindow被AlitripMonitorStatusBar給“繼承”了。從此之后打印出來的keyWindow都變成了AlitripMonitorStatusBar。
但是由于我們看不到UIActionSheet消失時(shí)的具體代碼,于是只能通過一些對比實(shí)驗(yàn)來確定它的keyWindow繼承順序。通過對AlitripMonitorStatusBar與AtomEntryView兩個(gè)window進(jìn)行windowLevel變更進(jìn)行實(shí)驗(yàn),最終確定當(dāng)AlertWindow被移除時(shí),它的keyWindow的繼承順序是按照windowLevel降序繼承,當(dāng)windowLevel相同時(shí),則按照添加順序降序繼承。
最后我修改了一下獲得topViewController的代碼,將獲取window的順序改為先拿UIWindow列表底部的window,再去獲取keyWindow。因?yàn)锳pp上的ViewController都是在最底部的UIWindow,這樣就不會出現(xiàn)keyWindow繼承的問題導(dǎo)致拿UIViewController錯(cuò)誤的問題,然后進(jìn)行試驗(yàn),發(fā)現(xiàn)crash問題消失,問題解決。
總結(jié):當(dāng)使用的警告框等需要顯示在另外一個(gè)window上的控件時(shí),要保證接下來的keyWindow的繼承正確,否則會出現(xiàn)拿不到正確的rootViewController的問題。

五、拾遺

在發(fā)現(xiàn)的問題的過程中,還發(fā)現(xiàn)了一些特殊的UIWindow,也一并分享出來。


(1)UITextEffectsWindow

這是iOS8引入的一個(gè)新window,是鍵盤所在的window。它的windowLevel是10,高于UIWindowLevelNormal。

(2)UIRemoteKeyboardWindow

iOS9之后,新增了一個(gè)類型為 UIRemoteKeyboardWindow 的窗口用來顯示鍵盤按鈕。目前對這個(gè)研究還不是很多,以后有了新發(fā)現(xiàn)再與大家分享。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容