莫名的穿透
之前版本中出現(xiàn)一個bug配椭,個別的QA同事反映APP中的部分彈框(UIView
)彈出來之后就沒法點擊了慎恒,然而這個彈框蒙層背后的界面依然可以正常交互和悦,好像出現(xiàn)了一種“穿透”效果:
因為復(fù)現(xiàn)的次數(shù)很少赋焕,規(guī)律也沒找到耙厚,聽得我也是一頭霧水强挫,我在想這是什么高級效果,既然不能復(fù)現(xiàn)那只能從源頭颜曾,也就是代碼層面排查了纠拔,因為不管所謂的“高級特效”是不是自己寫出來的,但代碼可是永遠(yuǎn)不會對你說謊的泛豪。
既然是彈出來以后就無法交互了稠诲,我第一反應(yīng)是想看看這幾個
UIView
是以什么樣的方式彈出的侦鹏,結(jié)果在他們中間都找到了同一段代碼:
- (void)show {
[[UIApplication sharedApplication].keyWindow addSubview:self];
}
這幾個UIView
都是被加到了keyWindow
上,當(dāng)時覺得問題也就出在這里了臀叙,因為加在UIWindow
上的東西始終是展示在最上面的略水,可能某種情況下導(dǎo)致這些UIView
無法被銷毀了,但至于是什么情況我也猜不到劝萤,最后將他們的展示方式都改為了:
[self.tabBarController.view addSubview:self];
后來就沒有人反饋過類似的問題了渊涝,我的心里在還為自己又解決了一個“靈異”問題而自嗨了短暫的幾秒。
后來無意中被我找到了復(fù)現(xiàn)的規(guī)律床嫌,我發(fā)現(xiàn)只要我在某一個頁面彈出過一次UIAlertView
以后跨释,其他只要是add到keyWindow
上的彈框,100%會出現(xiàn)這種“穿透的效果”厌处,這也是我上面的demo中有一個按鈕是alert的原因鳖谈,后面會講到。
我跟旁邊的dj_rose同學(xué)提到了這個現(xiàn)象阔涉,他捕捉到了window這個關(guān)鍵字缆娃,告訴我在上個版本我們接入過一個第三方的推送組件,這個組件就是在UIWindow
的基礎(chǔ)上做的瑰排,會不會是這個組件產(chǎn)生了影響贯要,這個線索很關(guān)鍵,于是我順著這條線椭住,在這個第三方組件的源碼里找到了蛛絲馬跡:
源碼中自定義了兩個
UIWindow
用來作為推送消息彈框的載體崇渗,分別是EBBanerWindow
類型和EBEmptyWindow
類型,至于這兩個UIWindow
到底是怎樣分工的這里就不分析了函荣,我也沒細(xì)看显押。圖中我圈出來的地方引起了我的注意,因為他給這兩個自定義的UIWindow
設(shè)置的frame
都是CGectZero
,并且這兩個UIWindow
的windowLevel
都是UIWindowLevelAlert
傻挂,這兩個屬性便讓我和“穿透”這一特性掛上了鉤,我在想項目的彈框是不是被無意間add到這兩個自定義UIWindow
中的其中一個上面了挖息,因為這兩個UIWindow
的frame
都是CGectZero
金拒,如果父view
的frame
是CGectZero
的話那子view
肯定是不會響應(yīng)到各種點擊事件的,而且這兩個UIWindow
如果出現(xiàn)的話肯定是在層級的最上面套腹,所以會出現(xiàn)一直浮在上面無法銷毀的現(xiàn)象绪抛,但我又一想,不對啊电禀,我都是add在keyWindow
上的啊幢码,同時我看到了下面這幾句代碼:
sharedWindow = [[self alloc] initWithFrame:CGRectZero];
sharedWindow.windowLevel = UIWindowLevelAlert;
sharedWindow.layer.masksToBounds = NO;
UIWindow *originKeyWindow = UIApplication.sharedApplication.keyWindow;
[sharedWindow makeKeyAndVisible];
[originKeyWindow makeKeyAndVisible];
他會將原來的keyWindow
保存到originKeyWindow
這個臨時變量中,最后再向它發(fā)送一遍makeKeyAndVisible
消息尖飞,這樣理應(yīng)keyWindow
是不會被影響到的症副,作者之所以加上這步操作我猜他也是為了防止keyWindow
被替換而帶來的不必要的麻煩店雅,這就奇怪了,也就是說在某種情況下我的keyWindow
還是被偷偷替換了贞铣。
keyWindow和UIAlertView
那到底是誰替換了keyWindow呢闹啦?答案我在上面已經(jīng)留下過伏筆了,我復(fù)現(xiàn)的規(guī)律是只要在APP中任何一個頁面彈出來過一次UIAlertView
之后辕坝,這個“穿透”就會發(fā)生窍奋,那肯定就是UIAlertView
影響到了keyWindow
,之前對UIAlertView
的了解也就停留在“其實它也是一個Window”的級別上酱畅,至于它為什么會改變keyWindow
并沒有研究過琳袄,隨即百度一番(百度雖然有點low但卻能解決現(xiàn)階段我的大部分問題,原因可能是我太low了吧)纺酸,找到了幾篇有用的文章挚歧,將他們的觀點總結(jié)一下:
1.使用UIAlertView
的show
時,系統(tǒng)使用了一個臨時的并且層級最高的UIWindow
來展現(xiàn)UIAlertView
吁峻,所以當(dāng)show
彈窗時滑负,keyWindow
已經(jīng)被替換為_UIAlertControllerShimPresenterWindow
,打印了一下也的確是這樣用含。
2.當(dāng)
UIAlertView
消失后keyWindow
將會轉(zhuǎn)向另一個UIWindow
矮慕,至于這個UIWindow
是哪一個,取決于在[UIApplication sharedApplication].windows
數(shù)組中的位置的先后啄骇。
windows數(shù)組的排列順序
一般的痴鳄,我們項目中在AppDelegate
里都會有這樣幾句代碼:
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self.window makeKeyAndVisible];
其實寫了這么多遍也沒細(xì)細(xì)分析過這句代碼,它其實就是給APP設(shè)置一個keyWindow
缸夹,用來作為之后呈現(xiàn)的一切UIView
的載體痪寻,這個window會排在windows
數(shù)組里的第一個,一般項目中如果沒有特殊需求去自定義UIWindow
的話虽惭,這個windows
數(shù)組里的firstObject
通常情況下始終會是我們在AppDelegate
中創(chuàng)建的這個window(當(dāng)然也不排除有firstObject
被系統(tǒng)偷偷替換的情況橡类,這也是我們另一個線上反饋很多次無法復(fù)現(xiàn)的bug的原因):
所以在這種情況下不管我們用
keyWidow
,appdelegate.window
還是windows.firstobject
去取出來的window都應(yīng)該是同一個芽唇,這也是我們沒有加入這個推送組件之前為什么不會發(fā)生“穿透”問題的原因顾画。
當(dāng)用戶收到過一次推送后可以看到這個時候
windows
數(shù)組中已經(jīng)有3個window了,在圖中最上面的UIWindow
也是windows
數(shù)組的第一個匆笤,后面兩個是EBBannerWindow
和EBEmptyWindow
研侣,那為什么這兩個自定義的window在windows
數(shù)組中會排在UIWindow
的后面呢?第一個決定因素是UIWindowLevel
炮捧,它代表window的一個級別庶诡,共有三個類型,分別是:這是三個
CGFloat
類型的常量咆课,通過打印得到這個三個值分別是0.000000 2000.000000 1000.000000
末誓。因為EBBannerWindow
和EBEmptyWindow
的windowLevel
都是UIWindowLevelAlert
的扯俱,而UIWindow
的windowLevel
是UIWindowLevelNormal
,由此可以推斷影響windows
數(shù)組排列的順序的第一個因素是level低的在前面基显,level高的在后面蘸吓。那么當(dāng)level相同的時候呢,這就和
window
的展現(xiàn)方式有關(guān)系了撩幽,首先來看看這個方法库继。
makeKeyAndVisible和setHidden:
討論windows
數(shù)組中元素的排列順序之前我們先來看一下蘋果官方文檔對于makeKeyAndVisible
這個方法的解釋:
This is a convenience method to show the current window and position it in front of all other windows at the same level or lower. If you only want to show the window, change its hidden property to
NO
.
意思也就是這個方法可以讓一個window在跟它級別相等或者級別比它低的window中凸現(xiàn)出來,讓這個window中的view展示在其他window的上面窜醉,如果你只是想讓一個window展現(xiàn)出來宪萄,將它的hidden
屬性設(shè)為NO
就可以了,因為一個UIWindow
被創(chuàng)建出來的時候hidden
屬性是默認(rèn)為YES
的榨惰。
其實在開發(fā)中我們也只需要將那個在AppDelegate
中創(chuàng)建的window設(shè)為keyWindow
拜英,其他的自定義window需要展現(xiàn),只需要將它們的hidden
屬性設(shè)為NO
琅催,makeKeyAndVisible
雖然是一個“convenience method”居凶,也能讓window展現(xiàn),但是因為它會改變keyWindow
藤抡,所以我個人是不建議使用侠碧。
回到windows
數(shù)組的排列順序,前面說了缠黍,第一個是取決于windowLevel
的大小弄兜,大的排后面,小的排前面瓷式,那么如果數(shù)組中兩個window的level相同替饿,那么誰排在前面,誰排在后面呢贸典?我在網(wǎng)上看到的一個答案說是最后一次調(diào)用makeKeyAndVisible
或者setHidden:YES
方法的那個window排在后面视卢,因為這兩個方法都會將一個window的hidden
屬性改為YES
,所以會影響這個window在數(shù)組中的順序瓤漏,最后一次調(diào)用這兩個方法中任何一個的window它也就類似于“最后一個被影響過”腾夯,所以它排在別人后面。
demoOne
我將原來EBBanerWindow.m
中兩個自定義window的展現(xiàn)方式都改為了通過setHidden:
的方式蔬充,并且代碼的順序改為emptyWindow
在前,bannerWindow
在后班利,因為這兩個window的level是相同的饥漫,按上面的觀點那bannerWindow
肯定會在windows
數(shù)組中排在emptyWindow
的后面,我們來看看運行結(jié)果:
UIWindow *originKeyWindow = UIApplication.sharedApplication.keyWindow;
emptyWindow = [[EBEmptyWindow alloc] initWithFrame:CGRectZero];
emptyWindow.windowLevel = UIWindowLevelAlert;
[emptyWindow setHidden:NO];
[originKeyWindow makeKeyAndVisible];
bannerWindow = [[self alloc] initWithFrame:CGRectZero];
bannerWindow.windowLevel = UIWindowLevelAlert;
[bannerWindow setHidden:NO];
[originKeyWindow makeKeyAndVisible];
可以發(fā)現(xiàn)
windows
數(shù)組的順序也跟著變化了罗标,bannerWindow
是排在了emptyWindow
的后面庸队,這么看來上面的觀點好像有點道理积蜻,別急,我們接著往下看彻消。
demoTwo
為了進一步證明這個觀點我將源碼中給emptyWindow
和bannerWindow
設(shè)置windowLevel
的代碼注釋掉竿拆,這樣他們兩的level也變成了默認(rèn)值UIWindowLevelNormal
,這樣[originKeyWindow makeKeyAndVisible]
是最后調(diào)用的宾尚,那么按照最后調(diào)用排后面的原則是不是windows
數(shù)組中UIWindow
會跑到數(shù)組的最后一個去呢丙笋,let us see see:
UIWindow *originKeyWindow = UIApplication.sharedApplication.keyWindow;
emptyWindow = [[EBEmptyWindow alloc] initWithFrame:CGRectZero];
// emptyWindow.windowLevel = UIWindowLevelAlert;
[emptyWindow setHidden:NO];
[originKeyWindow makeKeyAndVisible];
bannerWindow = [[self alloc] initWithFrame:CGRectZero];
// bannerWindow.windowLevel = UIWindowLevelAlert;
[bannerWindow setHidden:NO];
[originKeyWindow makeKeyAndVisible];
可以看到
windows
數(shù)組并沒有發(fā)生變化,那就證明上面的觀點是不正確的煌贴,那為毛這個UIWindow
始終會排在第一個呢御板?突然一個想法冒了出來,決定它崇高地位的只有它創(chuàng)建的時間了牛郑,因為它是一啟動就被創(chuàng)建的怠肋,所以才會排在第一個,由此我假設(shè)決定windows
數(shù)組中元素順序的是window們被創(chuàng)建的先后順序淹朋。
demoThree
根據(jù)這個假設(shè)我先改變了emptyWindow
和bannerWindow
的創(chuàng)建順序笙各,將bannerWindow
放在了emptyWindow
之前,但是他們調(diào)用setHidden:
的順序還是不變础芍,bannerWindow
在emptyWindow
之后:
UIWindow *originKeyWindow = UIApplication.sharedApplication.keyWindow;
bannerWindow = [[self alloc] initWithFrame:CGRectZero];
emptyWindow = [[EBEmptyWindow alloc] initWithFrame:CGRectZero];
emptyWindow.windowLevel = UIWindowLevelAlert;
[emptyWindow setHidden:NO];
[originKeyWindow makeKeyAndVisible];
bannerWindow.windowLevel = UIWindowLevelAlert;
[bannerWindow setHidden:NO];
[originKeyWindow makeKeyAndVisible];
這樣如果按先創(chuàng)建的順序bannerWindow
應(yīng)該排在emptyWindow
前面杈抢,如果按照后調(diào)用setHidden:
的順序bannerWindow
應(yīng)該排在emptyWindow
后面,剛好形成了一個互斥者甲,也就是說決定因素只有一個春感,我們來看看結(jié)果:
結(jié)果是
bannerWindow
排在了windows
前面,那就證明在level相同的前提下虏缸,決定數(shù)組中前后順序的是window創(chuàng)建的順序鲫懒,再來個demoFour佐證一下:
demoFour
我在
AppDelegate
中self.window
之前先創(chuàng)建了這兩個自定義的window,這里有個點要注意下就是創(chuàng)建一個window的同時就要為它設(shè)置一個rootViewController
刽辙,不然會crash的窥岩,運行的結(jié)果在左邊。
demoFive
那么windowlevel的高低和創(chuàng)建的順序這兩者哪一個優(yōu)先級更高呢宰缤?我將bannerWindow
和emptyWindow
的優(yōu)先級都改為了UIWindowLevelNormal
颂翼,然后將AppDelegate
中的self.window
的level提高到了UIWindowLevelAlert
,但創(chuàng)建順序self.window
還是在兩個自定義window的后面慨灭,看一下運行結(jié)果:
UIViewController *vc = [UIViewController new];
self.bannerWindow = [[EBBannerWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.bannerWindow.rootViewController = vc;
[self.bannerWindow setHidden:NO];
self.emptyWindow = [[EBEmptyWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.emptyWindow.rootViewController = vc;
[self.emptyWindow setHidden:NO];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.windowLevel = UIWindowLevelAlert;
可以看出來因為
self.window
因為level最高朦乏,所以它還是排在了數(shù)組最后一個,而上面兩個window因為level相同氧骤,則根據(jù)創(chuàng)建的先后順序排列呻疹,bannerWindow
是先創(chuàng)建的,所以排在前面筹陵,代碼和運行結(jié)果基本可以證實我這個觀點了刽锤。到這里搞清楚了windows
數(shù)組的排列順序镊尺,再來看看這個bug產(chǎn)生的原因吧。
穿透的原因
前面說了當(dāng)UIAlertView
彈出來后keyWindow
被替換成一個臨時的window并思,當(dāng)UIAlertView
dismiss之后這個臨時的window也隨即被銷毀庐氮,那么系統(tǒng)會去尋找一個新的keyWindow
人選,這個候選人自然是來自windows
數(shù)組宋彼,至于競選規(guī)則就是上面幾個demo總結(jié)出來的先優(yōu)先級后創(chuàng)建順序規(guī)則弄砍,按我們項目中最初的寫法,這個新的keyWindow
人選是第三方組件的中的emptyWindow
宙暇,因為它優(yōu)先級最高并且是最后一個被創(chuàng)建的输枯,同時這個emptyWindow
的frame
是CGRectZero
,所以加上去的彈框的所有點擊事件都無法響應(yīng)了占贫,這個地方其實我仍然有點疑惑的是桃熄,為什么這個window的frame
是CGRectZero
添加到這個window上的view依然可以展示出來,只是不能響應(yīng)點擊事件而已型奥,但也正因為它的frame
是CGRectZero
瞳收,彈出來的被添加到emptyWindow
上的這些UIView
骇塘,并沒有阻斷其他window上控件的事件傳遞沐序,蓋在它們下面的控件依然可以正常點擊瘦癌,這也就是所謂的“穿透”效果眼俊。
解決方案
找到問題的原因之后我們對這個推送第三方組件進行了修改,因為他是一個單例荷辕,沒法在沒有推送的情況下去銷毀這兩個自定義的UIWindow
伟阔,只能在彈出和消失的時候加以控制办桨。討論出來的方案是首先將bannerWindow
和emptyWindow
的windowLevel
降為了默認(rèn)值UIWindowLevelNormal
搭综,因為一個推送彈框只要讓用戶看見即可垢箕,它不存在交互,所以不需要那么高的級別兑巾,而且windowLevel
會影響一個window在windows
數(shù)組中的順序条获,而數(shù)組中的順序是影響keyWindow
的關(guān)鍵因素。其次將這兩個自定義window的展現(xiàn)方式從makeKeyAndVisible
改為setHidden:NO
蒋歌,我猜想到原作者寫這句[originKeyWindow makeKeyAndVisible];
代碼原因就只是想讓他的自定義window展現(xiàn)出來而不影響到keyWindow
帅掘,所以調(diào)用setHidden:
方法足矣,改了這兩個地方后堂油,emptyWindow
修档、bannerWindow
和appdelegate.window
都是UIWindowLevelNormal
級別的,但因為emptyWindow
是最后被創(chuàng)建的府框,所以它還是會排在數(shù)組的最后面萍悴,只要UIAlertView
彈出并消失后,它依然是keyWindow
的最佳人選寓免,所以我們需要在推送框的的hide
方法里癣诱,在hide
動畫完成之后,手動的調(diào)用一次[appdelegate.window makeKeyAndVisible];
方法袜香,將appdelegate.window
置回keyWindow
撕予,也就是讓emptyWindow
把keyWindow
的席位交出去,我們雖然不能改變它最佳候選人的位置蜈首,但卻可以讓它自己讓位实抡,這樣就避免了keyWindow
的影響,另外我們也把所有add
到keyWindow
上的這類彈框的展現(xiàn)方式統(tǒng)一做了替換欢策,分別添加到對應(yīng)的VC.view
或者tabbarVC.view
上吆寨,對于UIAlertView
,因為在iOS 9.0
以后也被蘋果廢棄了踩寇,我猜蘋果可能也是發(fā)現(xiàn)了這種用UIWindow
承載UIAlertView
這種方式會對keyWindow
產(chǎn)生影響啄清,另一方面UIAlertView
的代理方法對代碼邏輯也是一種拆散,所以我們也將項目中使用UIAlertView
的地方逐步的替換為更好的UIAlertController
俺孙,畢竟UIAlertController
是不會去改變keyWindow
的辣卒。
面向bug開發(fā)
踩過這一個坑后我想說在使用國內(nèi)非大神寫的第三方組件上時一定要“三思”,其實這個組件確實寫的蠻厲害睛榄,替我們節(jié)省了很多開發(fā)時間荣茫,不過拋開這個組件其他地方不談,我能在初始化window這段源碼中看到作者的防御機制场靴,但感覺并沒有吃透UIWindow
的一些用法和原理啡莉,才導(dǎo)致在和UIAlertView
一起的時候出現(xiàn)了問題。其實也是因為這個bug才迫使我去一點一點了解UIWindow
的東西旨剥,可能bug才是推動技術(shù)進步的第一要素咧欣,以上是我自己總結(jié)出來的一些心得,看到這些觀點的同學(xué)也務(wù)必要“三思”而后行泞边。
參考文章:
EBBannerView只需一行代碼:展示跟 iOS 系統(tǒng)一樣的推送通知橫幅
添加多個UIWindow時该押,使用keyWindow要注意一點
iOS 關(guān)于UIAlertController、UIAlertView彈窗問題
UIWindow的windowLevel屬性