前言
你好动知!
歡迎閱讀我的文章。
Window缭召,讀者可能更多的認(rèn)識(shí)是windows系統(tǒng)的窗口栈顷。在windows系統(tǒng)上,我們可以多個(gè)窗口同時(shí)運(yùn)行嵌巷,每個(gè)窗口代表著一個(gè)應(yīng)用程序萄凤。但在安卓上貌似并沒有這個(gè)東西,但讀者可以馬上想到搪哪,不是有小窗口模式嗎靡努,像米UI最新的系統(tǒng),不就是可以隨意創(chuàng)建一個(gè)小窗口晓折,然后兩個(gè)應(yīng)用同時(shí)操作惑朦?是的,那是屬于android中漓概,window的一種表現(xiàn)方式漾月。但是手機(jī)屏幕終究不能和電腦相比,因?yàn)槠聊惶×硕舛〉街荒懿僮饕豢顟?yīng)用栅屏,多個(gè)窗口就顯得非常不習(xí)慣飘千,所以Android上關(guān)于窗口方面的知識(shí)讀者可能接觸不多。那window的意思就只是小米系統(tǒng)中那種小窗口嗎栈雳?
當(dāng)然不是护奈。Android框架層意義上的window和我們認(rèn)識(shí)的window其實(shí)是有點(diǎn)不一樣的。我們?nèi)粘W钪庇^的哥纫,每個(gè)應(yīng)用界面霉旗,都有一個(gè)應(yīng)用級(jí)的window。再例如popupWindow
蛀骇、Toast
厌秒、dialog
、menu
都是需要通過創(chuàng)建window來實(shí)現(xiàn)擅憔。所以其實(shí)window我們一直都見到鸵闪,只是不知道那就是window。了解window的機(jī)制原理暑诸,可以更好地了解window蚌讼,進(jìn)而更好地了解android是怎么管理屏幕上的view。這樣个榕,當(dāng)我們需要使用dialog或者popupWindow的時(shí)候篡石,可以懂得他背后究竟做了什么,才能夠更好的運(yùn)用dialog西采、popupWindow等凰萨。
當(dāng)然,到此如果你有很多的疑問械馆,甚至質(zhì)疑我的理論胖眷,那就希望你可以閱讀完這一篇文章。我會(huì)從window是什么狱杰,有什么用瘦材,內(nèi)部機(jī)制是什么厅须,各種組件是如何創(chuàng)建window等等方面來闡述Android中的window仿畸。文章內(nèi)容非常多,讀者可自選章節(jié)閱讀朗和。
什么是window機(jī)制
先假設(shè)如果沒有window错沽,會(huì)發(fā)生什么:
我們看到的界面ui是view,如我們的應(yīng)用布局眶拉,更簡單是一個(gè)button千埃。假如屏幕上現(xiàn)在有一個(gè)Button,如圖1忆植,現(xiàn)在往屏幕中間添加一個(gè)TextView放可,那么最終的結(jié)果是圖2谒臼,還是圖3:
在上圖的圖2中,如果我要實(shí)現(xiàn)點(diǎn)擊textView執(zhí)行他的監(jiān)聽事件邏輯耀里,點(diǎn)擊不是textView的區(qū)域讓textView消失蜈缤,需要怎么實(shí)現(xiàn)呢?讀者可能會(huì)說冯挎,我們可以在Activity中添加這部分的邏輯底哥,那如果我們需要讓一個(gè)懸浮窗在所有界面顯示呢,如上文我講到的小米懸浮窗房官,兩個(gè)不用應(yīng)用的view趾徽,怎么確定他們的顯示次序?又例如我們需要彈出一個(gè)dialog來提示用戶翰守,怎么樣可以讓dialog永遠(yuǎn)處于最頂層呢孵奶,包括顯示dialog期間應(yīng)用彈出的如popupWindow必須顯示在dialog的低下,但toast又必須顯示在dialog上面蜡峰。
很明顯拒课,我們的屏幕可以允許多個(gè)應(yīng)用同時(shí)顯示非常多的view,他們的顯示次序或者說顯示高度是不一樣的事示,如果沒有一個(gè)統(tǒng)一的管理者早像,那么每一家應(yīng)用都想要顯示在最頂層,那么屏幕上的view會(huì)非常亂肖爵。
同時(shí)卢鹦,當(dāng)我們點(diǎn)擊屏幕時(shí),這個(gè)觸摸事件應(yīng)該傳給哪個(gè)view劝堪?很明顯我們都知道應(yīng)該傳給最上層的view冀自,但是接受事件的是屏幕,是另一個(gè)系統(tǒng)服務(wù)秒啦,他怎么知道觸摸位置的最上層是哪個(gè)view呢熬粗?即時(shí)知道,他又怎么把這個(gè)事件準(zhǔn)確地傳給他呢余境?
為了解決等等這些問題驻呐,急需有一個(gè)管理者來統(tǒng)一管理屏幕上的顯示的view,才能讓程序有條不紊地走下去芳来。而這含末,就是Android中的window機(jī)制
。
window機(jī)制就是為了管理屏幕上的view的顯示以及觸摸事件的傳遞問題即舌。
什么是window?
那什么是window佣盒,在Android的window機(jī)制中,每個(gè)view樹都可以看成一個(gè)window顽聂。為什么不是每個(gè)view呢肥惭?因?yàn)関iew樹中每個(gè)view的顯示次序是固定的盯仪,例如我們的Activity布局,每一個(gè)控件的顯示都是已經(jīng)安排好的蜜葱,對(duì)于window機(jī)制來說磨总,屬于“不可再分割的view”。
什么是view樹笼沥?例如你在布局中給Activity設(shè)置了一個(gè)布局xml蚪燕,那么最頂層的布局如LinearLayout就是view樹的根,他包含的所有view就都是該view樹的節(jié)點(diǎn)奔浅,所以這個(gè)view樹就對(duì)應(yīng)一個(gè)window馆纳。
舉幾個(gè)具體的例子:
我們?cè)谔砑觗ialog的時(shí)候,需要給他設(shè)置view汹桦,那么這個(gè)view他是不屬于antivity的布局內(nèi)的鲁驶,是通過WindowManager添加到屏幕上的,不屬于activity的view樹內(nèi)舞骆,所以這個(gè)dialog是一個(gè)獨(dú)立的view樹钥弯,所以他是一個(gè)window。
popupWindow他也對(duì)應(yīng)一個(gè)window督禽,因?yàn)樗彩峭ㄟ^windowManager添加上去的脆霎,不屬于Activity的view樹。
當(dāng)我們使用使用windowManager在屏幕上添加的任何view都不屬于Activity的布局view樹狈惫,即使是只添加一個(gè)button睛蛛。
view樹(后面使用view代稱,后面我說的view都是指view樹)是window機(jī)制的操作單位胧谈,每一個(gè)view對(duì)應(yīng)一個(gè)window忆肾,view是window的存在形式,window是view的載體菱肖,我們平時(shí)看到的應(yīng)用界面客冈、dialog、popupWindow以及上面描述的懸浮窗稳强,都是window的表現(xiàn)形式场仲。注意,我們看到的不是window键袱,而是view燎窘。window是view的管理者摹闽,同時(shí)也是view的載體蹄咖。他是一個(gè)抽象的概念,本身并不存在付鹿,view是window的表現(xiàn)形式澜汤。這里的不存在蚜迅,指的是我們?cè)谄聊簧鲜强床坏絯indow的,他不像windows系統(tǒng)俊抵,如下圖:
有一個(gè)很明顯的標(biāo)志:看谁不,我就是window。但在Android中我們是無法感知的徽诲,我們只能看到view無法看到window刹帕,window是控制view需要怎么顯示的管理者。每個(gè)成功的男人背后都有一個(gè)女人谎替,每個(gè)view背后都有一個(gè)window偷溺。
window本身并不存在,他只是一個(gè)概念钱贯。舉個(gè)栗子:如班集體挫掏,就是一個(gè)概念,他的存在形式是這整個(gè)班的學(xué)生秩命,當(dāng)學(xué)生不存在那么這個(gè)班集體也就不存在尉共。但是他的好處是得到了一個(gè)新的概念,我們可以以班為單位來安排活動(dòng)弃锐。因他不存在袄友,所以也很難從源碼中找到他的痕跡,window機(jī)制的操作單位都是view霹菊,如果要說他在源碼中的存在形式杠河,筆者目前的認(rèn)知就是在WindowManagerService中每一個(gè)view對(duì)應(yīng)一個(gè)windowStatus。WindowManagerService是什么如果沒了解過可以先忽略后面會(huì)講到浇辜。讀者可以慢慢思考一下這個(gè)抽象的概念券敌,后面會(huì)慢慢深入講源碼幫助理解。
view是window的存在形式柳洋,window是view的載體
window是view的管理者待诅,同時(shí)也是view的載體。他是一個(gè)抽象的概念熊镣,本身并不存在卑雁,view是window的表現(xiàn)形式
思考:Android中不是有一個(gè)抽象類叫做window還有一個(gè)PhoneWindow實(shí)現(xiàn)類嗎,他們不就是window的存在形式绪囱,為什么說window是抽象不存在的测蹲?讀者可自行思考,后面會(huì)講到鬼吵。
Window的相關(guān)屬性
在了解window的操作流程之前扣甲,先補(bǔ)充一下window的相關(guān)屬性。
window的type屬性
前面我們講到window機(jī)制解決的一個(gè)問題就是view的顯示次序問題,這個(gè)屬性就決定了window的顯示次序琉挖。window是有分類的启泣,不同類別的顯示高度范圍不同,例如我把1-1000m高度稱為低空示辈,1001-2000m高度稱為中空寥茫,2000以上稱為高空。window也是一樣按照高度范圍進(jìn)行分類矾麻,他也有一個(gè)變量Z-Order
纱耻,決定了window的高度。window一共可分為三類:
- 應(yīng)用程序窗口:應(yīng)用程序窗口一般位于最底層险耀,Z-Order在1-99
- 子窗口:子窗口一般是顯示在應(yīng)用窗口之上膝迎,Z-Order在1000-1999
- 系統(tǒng)級(jí)窗口:系統(tǒng)級(jí)窗口一般位于最頂層,不會(huì)被其他的window遮住胰耗,如Toast限次,Z-Order在2000-2999。如果要彈出自定義系統(tǒng)級(jí)窗口需要?jiǎng)討B(tài)申請(qǐng)權(quán)限柴灯。
Z-Order越大卖漫,window越靠近用戶,也就顯示越高赠群,高度高的window會(huì)覆蓋高度低的window羊始。
window的type屬性就是Z-Order的值,我們可以給window的type屬性賦值來決定window的高度查描。系統(tǒng)為我們?nèi)恮indow都預(yù)設(shè)了靜態(tài)常量突委,如下(以下常用參數(shù)介紹轉(zhuǎn)自參考文獻(xiàn)第一篇文章):
應(yīng)用級(jí)window
// 應(yīng)用程序 Window 的開始值
public static final int FIRST_APPLICATION_WINDOW = 1;
// 應(yīng)用程序 Window 的基礎(chǔ)值
public static final int TYPE_BASE_APPLICATION = 1;
// 普通的應(yīng)用程序
public static final int TYPE_APPLICATION = 2;
// 特殊的應(yīng)用程序窗口,當(dāng)程序可以顯示 Window 之前使用這個(gè) Window 來顯示一些東西
public static final int TYPE_APPLICATION_STARTING = 3;
// TYPE_APPLICATION 的變體冬三,在應(yīng)用程序顯示之前匀油,WindowManager 會(huì)等待這個(gè) Window 繪制完畢
public static final int TYPE_DRAWN_APPLICATION = 4;
// 應(yīng)用程序 Window 的結(jié)束值
public static final int LAST_APPLICATION_WINDOW = 99;
子window
// 子 Window 類型的開始值
public static final int FIRST_SUB_WINDOW = 1000;
// 應(yīng)用程序 Window 頂部的面板。這些 Window 出現(xiàn)在其附加 Window 的頂部勾笆。
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
// 用于顯示媒體(如視頻)的 Window敌蚜。這些 Window 出現(xiàn)在其附加 Window 的后面。
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
// 應(yīng)用程序 Window 頂部的子面板窝爪。這些 Window 出現(xiàn)在其附加 Window 和任何Window的頂部
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
// 當(dāng)前Window的布局和頂級(jí)Window布局相同時(shí)弛车,不能作為子代的容器
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
// 用顯示媒體 Window 覆蓋頂部的 Window, 這是系統(tǒng)隱藏的 API
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;
// 子面板在應(yīng)用程序Window的頂部蒲每,這些Window顯示在其附加Window的頂部纷跛, 這是系統(tǒng)隱藏的 API
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
// 子 Window 類型的結(jié)束值
public static final int LAST_SUB_WINDOW = 1999;
系統(tǒng)級(jí)window
// 系統(tǒng)Window類型的開始值
public static final int FIRST_SYSTEM_WINDOW = 2000;
// 系統(tǒng)狀態(tài)欄,只能有一個(gè)狀態(tài)欄邀杏,它被放置在屏幕的頂部天试,所有其他窗口都向下移動(dòng)
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
// 系統(tǒng)搜索窗口灵份,只能有一個(gè)搜索欄,它被放置在屏幕的頂部
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
// 已經(jīng)從系統(tǒng)中被移除,可以使用 TYPE_KEYGUARD_DIALOG 代替
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
// 系統(tǒng)對(duì)話框窗口
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
// 鎖屏?xí)r顯示的對(duì)話框
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
// 輸入法窗口宴倍,位于普通 UI 之上,應(yīng)用程序可重新布局以免被此窗口覆蓋
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;
// 輸入法對(duì)話框勋桶,顯示于當(dāng)前輸入法窗口之上
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
// 墻紙
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;
// 狀態(tài)欄的滑動(dòng)面板
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
// 應(yīng)用程序疊加窗口顯示在所有窗口之上
public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;
// 系統(tǒng)Window類型的結(jié)束值
public static final int LAST_SYSTEM_WINDOW = 2999;
Window的flags參數(shù)
flag標(biāo)志控制window的東西比較多散罕,很多資料的描述是“控制window的顯示”,但我覺得不夠準(zhǔn)確状您。flag控制的范圍包括了:各種情景下的顯示邏輯(鎖屏勒叠,游戲等)還有觸控事件的處理邏輯「嗝希控制顯示確實(shí)是他的很大部分功能眯分,但是并不是全部。下面看一下一些常用的flag柒桑,就知道flag的功能了(以下常用參數(shù)介紹轉(zhuǎn)自參考文獻(xiàn)第一篇文章):
// 當(dāng) Window 可見時(shí)允許鎖屏
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;
// Window 后面的內(nèi)容都變暗
public static final int FLAG_DIM_BEHIND = 0x00000002;
// Window 不能獲得輸入焦點(diǎn)弊决,即不接受任何按鍵或按鈕事件,例如該 Window 上 有 EditView魁淳,點(diǎn)擊 EditView 是 不會(huì)彈出軟鍵盤的
// Window 范圍外的事件依舊為原窗口處理飘诗;例如點(diǎn)擊該窗口外的view,依然會(huì)有響應(yīng)界逛。另外只要設(shè)置了此Flag昆稿,都將會(huì)啟用FLAG_NOT_TOUCH_MODAL
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
// 設(shè)置了該 Flag,將 Window 之外的按鍵事件發(fā)送給后面的 Window 處理, 而自己只會(huì)處理 Window 區(qū)域內(nèi)的觸摸事件
// Window 之外的 view 也是可以響應(yīng) touch 事件。
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
// 設(shè)置了該Flag息拜,表示該 Window 將不會(huì)接受任何 touch 事件溉潭,例如點(diǎn)擊該 Window 不會(huì)有響應(yīng),只會(huì)傳給下面有聚焦的窗口少欺。
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
// 只要 Window 可見時(shí)屏幕就會(huì)一直亮著
public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;
// 允許 Window 占滿整個(gè)屏幕
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
// 允許 Window 超過屏幕之外
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
// 全屏顯示喳瓣,隱藏所有的 Window 裝飾,比如在游戲赞别、播放器中的全屏顯示
public static final int FLAG_FULLSCREEN = 0x00000400;
// 表示比FLAG_FULLSCREEN低一級(jí)夫椭,會(huì)顯示狀態(tài)欄
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
// 當(dāng)用戶的臉貼近屏幕時(shí)(比如打電話),不會(huì)去響應(yīng)此事件
public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;
// 則當(dāng)按鍵動(dòng)作發(fā)生在 Window 之外時(shí)氯庆,將接收到一個(gè)MotionEvent.ACTION_OUTSIDE事件蹭秋。
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
@Deprecated
// 窗口可以在鎖屏的 Window 之上顯示, 使用 Activity#setShowWhenLocked(boolean) 方法代替
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
// 表示負(fù)責(zé)繪制系統(tǒng)欄背景。如果設(shè)置堤撵,系統(tǒng)欄將以透明背景繪制仁讨,
// 此 Window 中的相應(yīng)區(qū)域?qū)⑻畛?Window#getStatusBarColor()和 Window#getNavigationBarColor()中指定的顏色。
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;
// 表示要求系統(tǒng)壁紙顯示在該 Window 后面实昨,Window 表面必須是半透明的洞豁,才能真正看到它背后的壁紙
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
window的solfInputMode屬性
這一部分就是當(dāng)軟件盤彈起來的時(shí)候,window的處理邏輯,這在日常中也經(jīng)常遇到丈挟,如:我們?cè)谖⑿帕奶斓臅r(shí)候刁卜,點(diǎn)擊輸入框,當(dāng)軟鍵盤彈起來的時(shí)候輸入框也會(huì)被頂上去曙咽。如果你不想被頂上去蛔趴,也可以設(shè)置為被軟鍵盤覆蓋。下面介紹一下常見的屬性(以下常見屬性介紹選自參考文獻(xiàn)第一篇文章):
// 沒有指定狀態(tài)例朱,系統(tǒng)會(huì)選擇一個(gè)合適的狀態(tài)或者依賴于主題的配置
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
// 當(dāng)用戶進(jìn)入該窗口時(shí)孝情,隱藏軟鍵盤
public static final int SOFT_INPUT_STATE_HIDDEN = 2;
// 當(dāng)窗口獲取焦點(diǎn)時(shí),隱藏軟鍵盤
public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
// 當(dāng)用戶進(jìn)入窗口時(shí)洒嗤,顯示軟鍵盤
public static final int SOFT_INPUT_STATE_VISIBLE = 4;
// 當(dāng)窗口獲取焦點(diǎn)時(shí)箫荡,顯示軟鍵盤
public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
// window會(huì)調(diào)整大小以適應(yīng)軟鍵盤窗口
public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
// 沒有指定狀態(tài),系統(tǒng)會(huì)選擇一個(gè)合適的狀態(tài)或依賴于主題的設(shè)置
public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
// 當(dāng)軟鍵盤彈出時(shí),窗口會(huì)調(diào)整大小,例如點(diǎn)擊一個(gè)EditView渔隶,整個(gè)layout都將平移可見且處于軟件盤的上方
// 同樣的該模式不能與SOFT_INPUT_ADJUST_PAN結(jié)合使用羔挡;
// 如果窗口的布局參數(shù)標(biāo)志包含F(xiàn)LAG_FULLSCREEN,則將忽略這個(gè)值间唉,窗口不會(huì)調(diào)整大小绞灼,但會(huì)保持全屏。
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
// 當(dāng)軟鍵盤彈出時(shí)终吼,窗口不需要調(diào)整大小, 要確保輸入焦點(diǎn)是可見的,
// 例如有兩個(gè)EditView的輸入框镀赌,一個(gè)為Ev1,一個(gè)為Ev2际跪,當(dāng)你點(diǎn)擊Ev1想要輸入數(shù)據(jù)時(shí)商佛,當(dāng)前的Ev1的輸入框會(huì)移到軟鍵盤上方
// 該模式不能與SOFT_INPUT_ADJUST_RESIZE結(jié)合使用
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
// 將不會(huì)調(diào)整大小,直接覆蓋在window上
public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
window的其他屬性
上面的三個(gè)屬性是window比較重要也是比較復(fù)雜 的三個(gè)姆打,除此之外還有幾個(gè)日常經(jīng)常使用的屬性:
- x與y屬性:指定window的位置
- alpha:window的透明度
- gravity:window在屏幕中的位置良姆,使用的是Gravity類的常量
- format:window的像素點(diǎn)格式,值定義在PixelFormat中
如何給window屬性賦值
window屬性的常量值大部分存儲(chǔ)在WindowManager.LayoutParams類中幔戏,我們可以通過這個(gè)類來獲得這些常量玛追。當(dāng)然還有Gravity類和PixelFormat類等。
一般情況下我們會(huì)通過以下方式來往屏幕中添加一個(gè)window:
// 在Activity中調(diào)用
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
windParams.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;
TextView view = new TextView(this);
getWindowManager.addview(view,windowParams);
我們可以直接給WindowManager.LayoutParams
對(duì)象設(shè)置屬性闲延。
第二種賦值方法是直接給window賦值痊剖,如
getWindow().flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;
除此之外,window的solfInputMode屬性比較特殊垒玲,他可以直接在AndroidManifest中指定陆馁,如下:
<activity android:windowSoftInputMode="adjustNothing" />
最后總結(jié)一下:
- window的重要屬性有
type
、flags
合愈、solfInputMode
叮贩、gravity
等- 我們可以通過不同的方式給window屬性賦值
- 沒必要去全部記下來击狮,等遇到需求再去尋找對(duì)應(yīng)的常量即可
Window的添加過程
通過理解源碼之后,可以對(duì)之前的理論理解更加的透徹益老。window的添加過程彪蓬,指的是我們通過WindowManagerImpl
的addView
方法來添加window的過程。
想要添加一個(gè)window捺萌,我們知道首先得有view和WindowManager.LayoutParams對(duì)象档冬,才能去創(chuàng)建一個(gè)window,這是我們常見的代碼:
Button button = new Button(this);
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
// 這里對(duì)windowParam進(jìn)行初始化
windowParam.addFlags...
// 獲得應(yīng)用PhoneWindow的WindowManager對(duì)象進(jìn)行添加window
getWindowManager.addView(button,windowParams);
然后接下來我們進(jìn)入addView
方法中看看互婿。我們知道這個(gè)windowManager的實(shí)現(xiàn)類是WindowManagerImpl
捣郊,上面講過辽狈,進(jìn)入他的addView方法看一看:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
可以發(fā)現(xiàn)他把邏輯直接交給mGlobal
去處理了慈参。這個(gè)mGlobal是WindowManagerGlobal
,是一個(gè)全局單例刮萌,是WindowManager接口的具體邏輯實(shí)現(xiàn)驮配。這里運(yùn)用的是橋接模式。那我們進(jìn)WindowManagerGlobal
的方法看一下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// 首先判斷參數(shù)是否合法
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
// 如果不是子窗口着茸,會(huì)對(duì)其做參數(shù)的調(diào)整
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
synchronized (mLock) {
...
// 這里新建了一個(gè)viewRootImpl壮锻,并設(shè)置參數(shù)
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// 添加到windowManagerGlobal的三個(gè)重要list中,后面會(huì)講到
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// 最后通過viewRootImpl來添加window
try {
root.setView(view, wparams, panelParentView);
}
...
}
}
代碼有點(diǎn)長涮阔,一步步看:
- 首先對(duì)參數(shù)的合法性進(jìn)行檢查
- 然后判斷該窗口是不是子窗口猜绣,如果是的話需要對(duì)窗口進(jìn)行調(diào)整,這個(gè)好理解敬特,子窗口要跟隨父窗口的特性掰邢。
- 接著新建viewRootImpl對(duì)象,并把view伟阔、viewRootImpl辣之、params三個(gè)對(duì)象添加到三個(gè)list中進(jìn)行保存
- 最后通過viewRootImpl來進(jìn)行添加
補(bǔ)充一點(diǎn)關(guān)于WindowManagerGlobal中的三個(gè)list,他們分別是:
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
每一個(gè)window所對(duì)應(yīng)的這三個(gè)對(duì)象都會(huì)保存在這里皱炉,之后對(duì)window的一些操作就可以直接來這里取對(duì)象了怀估。當(dāng)window被刪除的時(shí)候,這些對(duì)象也會(huì)被從list中移除合搅。
可以看到添加的window的邏輯就交給ViewRootImpl
了多搀。viewRootImpl是window和view之間的橋梁,viewRootImpl可以處理兩邊的對(duì)象灾部,然后聯(lián)結(jié)起來康铭。下面看一下viewRootImpl
怎么處理:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
...
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
// 這里調(diào)用了windowSession的方法,調(diào)用wms的方法梳猪,把添加window的邏輯交給wms
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
}
...
}
}
viewRootImpl的邏輯很多麻削,重要的就是調(diào)用了mWindowSession
的方法調(diào)用了WMS的方法蒸痹。這個(gè)mWindowSession很重要重點(diǎn)講一下。
mWindowSession是一個(gè)IWindowSession對(duì)象呛哟,看到這個(gè)命名很快地可以像到這里用了AIDL跨進(jìn)程通信叠荠。IWindowSession是一個(gè)IBinder接口,他的具體實(shí)現(xiàn)類在WindowManagerService扫责,本地的mWindowSession只是一個(gè)Binder對(duì)象榛鼎,通過這個(gè)mWindowSession就可以直接調(diào)用WMS的方法進(jìn)行跨進(jìn)程通信。
那這個(gè)mWindowSession是從哪里來的呢鳖孤?我們到viewRootImpl
的構(gòu)造器方法中看一下:
public ViewRootImpl(Context context, Display display) {
...
mWindowSession = WindowManagerGlobal.getWindowSession();
...
}
可以看到這個(gè)session對(duì)象是來自WindowManagerGlobal
者娱。再深入看一下:
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
...
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
...
});
}
...
}
return sWindowSession;
}
}
這熟悉的代碼格式,可以看出來這個(gè)session
是一個(gè)單例
苏揣,也就是整個(gè)應(yīng)用的所有viewRootImpl的windowSession都是同一個(gè)黄鳍,也就是一個(gè)應(yīng)用只有一個(gè)windowSession。對(duì)于wms而言平匈,他是服務(wù)于多個(gè)應(yīng)用的框沟,如果說每個(gè)viewRootImpl整一個(gè)session,那他的任務(wù)就太重了增炭。WMS的對(duì)象單位是應(yīng)用忍燥,他在內(nèi)部給每個(gè)應(yīng)用session分配了一些數(shù)據(jù)結(jié)構(gòu)如list,用于保存每個(gè)應(yīng)用的window以及對(duì)應(yīng)的viewRootImpl隙姿。當(dāng)需要操作view的時(shí)候梅垄,通過session直接找到viewRootImpl就可以操作了。
后面的邏輯就交給WMS去處理了输玷,WMS就會(huì)創(chuàng)建window队丝,然后結(jié)合參數(shù)計(jì)算window的高度等等,最后使用viewRootImpl進(jìn)行繪制饲嗽。這后面的代碼邏輯就不講了炭玫,這是深入到WMS的內(nèi)容,再講進(jìn)去就太復(fù)雜了(筆者也還沒讀懂WMS)貌虾。讀源碼的目的是了解整個(gè)系統(tǒng)的本質(zhì)與工作流程吞加,對(duì)系統(tǒng)整體的感知,而不用太深入代碼細(xì)節(jié)尽狠,Android系統(tǒng)那么多的代碼衔憨,如果深入進(jìn)去會(huì)出不來的,所以點(diǎn)到為止就好了袄膏。
我們知道windowManager接口是繼承viewManager接口的践图,viewManager還有另外兩個(gè)接口:removeView、updateView沉馆。這里就不講了码党,有興趣的讀者可以自己去閱讀源碼德崭。講添加流程主要是為了理解window系統(tǒng)的運(yùn)作,對(duì)內(nèi)部的流程感知揖盘,以便于更好的理解window眉厨。
最后做個(gè)總結(jié):
window的添加過程是通過PhoneWindow對(duì)應(yīng)的WindowManagerImpl來添加window,內(nèi)部會(huì)調(diào)用WindowManagerGlobal來實(shí)現(xiàn)兽狭。WindowManagerGlobal會(huì)使用viewRootImpl來進(jìn)行跨進(jìn)程通信讓W(xué)MS執(zhí)行創(chuàng)建window的業(yè)務(wù)憾股。
每個(gè)應(yīng)用都有一個(gè)windowSession,用于負(fù)責(zé)和WMS的通信箕慧,如ApplicationThread與AMS的通信服球。
window機(jī)制的關(guān)鍵類
前面的源碼流程中涉及到很多的類,這里把相關(guān)的類統(tǒng)一分析一下颠焦。先看一張圖:
這基本上是我們這篇文章涉及到的所有關(guān)鍵類斩熊。且聽我慢慢講。(圖中綠色的window并不是一個(gè)類蒸健,而是真正意義上的window)
window相關(guān)
window的實(shí)現(xiàn)類只有一個(gè):PhoneWindow座享,他繼承自Window抽象類婉商。后面我會(huì)重點(diǎn)分析他似忧。
WindowManager相關(guān)
顧名思義,windowManager就是window管理類丈秩。這一部分的關(guān)鍵類有windowManager盯捌,viewManager,windowManagerImpl蘑秽,windowManagerGlobal饺著。windowManager是一個(gè)接口,繼承自viewManager肠牲。viewManager中包含了我們非常熟悉的三個(gè)接口:addView
,removeView
,updateView
幼衰。
windowManagerImpl和PhoneWindow是成對(duì)出現(xiàn)的,前者負(fù)責(zé)管理后者缀雳。WindowManagerImpl是windowManager的實(shí)現(xiàn)類渡嚣,但是他本身并沒有真正實(shí)現(xiàn)邏輯,而是交給了WindowManagerGlobal肥印。WindowManagerGlobal是全局單例识椰,windowManagerImpl內(nèi)部使用橋接模式,他是windowManager接口邏輯的真正實(shí)現(xiàn)
view相關(guān)
這里有個(gè)很關(guān)鍵的類:ViewRootImpl深碱。每個(gè)view樹都會(huì)有一個(gè)腹鹉。當(dāng)我使用windowManager的addView方法時(shí),就會(huì)創(chuàng)建一個(gè)ViewRootImpl敷硅。ViewRootImpl的作用很關(guān)鍵:
- 負(fù)責(zé)連接view和window的橋梁事務(wù)
- 負(fù)責(zé)和WindowManagerService的聯(lián)系
- 負(fù)責(zé)管理和繪制view樹
- 事件的中轉(zhuǎn)站
每個(gè)window都會(huì)有一個(gè)ViewRootImpl功咒,viewRootImpl是負(fù)責(zé)繪制這個(gè)view樹和window與view的橋梁愉阎,每個(gè)window都會(huì)有一個(gè)ViewRootImpl。
WindowManagerService
這個(gè)是window的真正管理者力奋,類似于AMS(ActivityManagerService)管理四大組件诫硕。所有的window創(chuàng)建最終都要經(jīng)過windowManagerService。整個(gè)Android的window機(jī)制中刊侯,WMS絕對(duì)是核心章办,他決定了屏幕所有的window該如何顯示如何分發(fā)點(diǎn)擊事件等等。
window與PhoneWindow的關(guān)系
解釋一下標(biāo)題滨彻,window是指window機(jī)制中window這個(gè)概念藕届,而PhoneWindow是指PhoneWindow這個(gè)類。后面我在講的時(shí)候亭饵,如果是指類休偶,我會(huì)在后面加個(gè)‘類’字。如window是指window概念辜羊,window類是指window這個(gè)抽象類踏兜。讀者不要混淆。
還記得我在講window的概念的時(shí)候留了一個(gè)思考嗎八秃?
思考:Android中不是有一個(gè)抽象類叫做window還有一個(gè)PhoneWindow實(shí)現(xiàn)類嗎碱妆,他們不就是window的存在形式,為什么說window是抽象不存在的
這里我再拋出幾個(gè)問題:
- 有一些資料認(rèn)為PhoneWindow就是window昔驱,是view容器疹尾,負(fù)責(zé)管理容器內(nèi)的view,windowManagerImpl可以往里面添加view骤肛,如上面我們講過的addView方法纳本。但是,同時(shí)它又說每個(gè)window對(duì)應(yīng)一個(gè)viewRootImpl腋颠,但卻沒解釋為什么每次addView都會(huì)新建一個(gè)viewRootImpl繁成,前后發(fā)送矛盾。
- 有一些資料也是認(rèn)為PhoneWindow是window淑玫,但是他說addView方法不是添加view而是添加window巾腕,同時(shí)拿這個(gè)方法的名字作為論據(jù)證明view就是window,但是他沒解釋為什么在使用addView方法創(chuàng)建window的過程卻沒有創(chuàng)建PhoneWindow對(duì)象混移。
我們一步步來看祠墅。我們首先來看一下源碼中對(duì)于window抽象類的注釋:
Abstract base class for a top-level window look and behavior policy. An
instance of this class should be used as the top-level view added to the
window manager. It provides standard UI policies such as a background, title
area, default key processing, etc.
頂層窗口外觀和行為策略的抽象基類。此類的實(shí)例應(yīng)用作添加到窗口管理器的頂層視圖歌径。
它提供標(biāo)準(zhǔn)的UI策略毁嗦,如背景、標(biāo)題區(qū)域回铛、默認(rèn)鍵處理等狗准。
大概意思就是:這個(gè)類是頂級(jí)窗口的抽象基類克锣,頂級(jí)窗口必須繼承他,他負(fù)責(zé)窗口的外觀如背景腔长、標(biāo)題袭祟、默認(rèn)按鍵處理等。這個(gè)類的實(shí)例被添加到windowManager中捞附,讓windowManager對(duì)他進(jìn)行管理巾乳。PhoneWindow是一個(gè)top-level window(頂級(jí)窗口),他被添加到頂級(jí)窗口管理器的頂層視圖鸟召,其他的window胆绊,都需要添加到這個(gè)頂層視圖中,所以更準(zhǔn)確的來說欧募,PhoneWindow并不是view容器压状,而是window容器。
那PhoneWindow的存在意義是什么跟继?
第一种冬、提供DecorView模板。如下圖:
我們的Activity是通過setContentView把布局設(shè)置到DecorView中舔糖,那么DecorView本身的布局娱两,就成為了Activity界面的背景。同時(shí)DecorView是分為標(biāo)題欄和內(nèi)容兩部分剩盒,所以也可以可界面設(shè)置標(biāo)題欄谷婆。同時(shí),由于我們的界面是添加在的DecorView中辽聊,屬于DecorView的一部分。那么對(duì)于DecorView的window屬性設(shè)置也會(huì)對(duì)我們的布局界面生效期贫。還記得谷歌的官方給window類注釋的最后一句話嗎:它提供標(biāo)準(zhǔn)的UI策略跟匆,如背景、標(biāo)題區(qū)域通砍、默認(rèn)鍵處理等
玛臂。這些都可以通過DecorView實(shí)現(xiàn),這是PhoneWindow的第一個(gè)作用封孙。
第二迹冤、抽離Activity中關(guān)于window的邏輯。Activity的職責(zé)非常多虎忌,如果所有的事情都自己做泡徙,那么會(huì)造成本身代碼極其臃腫。閱讀過Activity啟動(dòng)的讀者可能知道膜蠢,AMS也通過ActivityStarter這個(gè)類來抽離啟動(dòng)Activity啟動(dòng)的邏輯堪藐。這樣關(guān)于window相關(guān)的事情莉兰,就交給PhoneWindow去處理了。(事實(shí)上礁竞,Activity調(diào)用的是WindowManagerImpl糖荒,但因PhoneWindow和WindowManagerImpl兩者是成對(duì)存在,他們共同處理window相關(guān)的事務(wù)模捂,所以這里就簡單寫成交給PhoneWindow處理捶朵。)當(dāng)Activity需要添加界面時(shí),只需要一句setContentView狂男,調(diào)用了PhoneWindow的setContentView方法泉孩,就把布局設(shè)置到屏幕上了。具體怎么完成并淋,Activity不必管寓搬。
第三、限制組件添加window的權(quán)限县耽。PhoneWindow內(nèi)部有一個(gè)token屬性句喷,用于驗(yàn)證一個(gè)PhoneWindow是否允許添加window。在Activity創(chuàng)建PhoneWindow的時(shí)候兔毙,就會(huì)把從AMS傳過來的token賦值給他唾琼,從而他也就有了添加token的權(quán)限。而其他的PhoneWindow則沒有這個(gè)權(quán)限澎剥,因而也無法添加window锡溯。這部分內(nèi)容我在另一篇文章有詳細(xì)講解,感興趣的讀者可以前往了解一下哑姚。
當(dāng)然祭饭,PhoneWindow的作用肯定遠(yuǎn)不止如此,這里列出很重要的三條叙量,也是筆者目前學(xué)習(xí)到的三個(gè)最重要的作用倡蝙。官方對(duì)于一個(gè)類的設(shè)計(jì)的考慮肯定是非常多,不是筆者簡單的分析所能闡述绞佩,而只是給出一個(gè)新的思考方向寺鸥,帶大家認(rèn)識(shí)真正的window。
總結(jié)一下:
- PhoneWindow本身不是真正意義上的window品山,他更多可以認(rèn)為是輔助Activity操作window的工具類胆建。
- windowManagerImpl并不是管理window的類,而是管理PhoneWindow的類肘交。真正管理window的是WMS笆载。
- PhoneWindow可以配合DecorView可以給其中的window按照一定的邏輯提供標(biāo)準(zhǔn)的UI策略
- PhoneWindow限制了不同的組件添加window的權(quán)限。
常見組件的window創(chuàng)建流程
上面講的是通過windowManagerImpl創(chuàng)建window的過程,我們通過前面的講解了解到宰译,WindowManagerImpl是管理PhoneWindow的檐蚜,他們是同時(shí)出現(xiàn)的。因而有兩種創(chuàng)建window的方式:
- 已經(jīng)存在PhoneWindow沿侈,直接通過WindowManagerImpl創(chuàng)建window
- PhoneWindow尚未存在闯第,先創(chuàng)建PhoneWindow,再利用windowManagerImpl來創(chuàng)建window
當(dāng)我們?cè)贏ctivity中使用getWindowManager方法獲取到的就是應(yīng)用的PhoneWindow對(duì)應(yīng)的WindowManagerImpl缀拭。下面來講一下不同的組件是如何創(chuàng)建window的咳短,
Activity
如果有閱讀過Activity的啟動(dòng)流程的讀者,會(huì)知道Activity的啟動(dòng)最后來到了ActivityThread的handleLaunchActivity這個(gè)方法蛛淋。
關(guān)于Activity的啟動(dòng)流程咙好,我寫過一篇文章,有興趣的讀者可以點(diǎn)擊下方鏈接前往:
至于為什么是這個(gè)方法這里就不講了褐荷,有興趣的讀者可以去看上面的文章勾效。我們直接來看這個(gè)方法的代碼:
public void handleLaunchActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...;
// 這里對(duì)WindowManagerGlobal進(jìn)行初始化
WindowManagerGlobal.initialize();
// 啟動(dòng)Activity并回調(diào)activity的onCreate方法
final Activity a = performLaunchActivity(r, customIntent);
...
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
try {
// 這里創(chuàng)建Application
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
...
if (activity != null) {
...
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
appContext.setOuterContext(activity);
// 這里將window作為參數(shù)傳到activity的attach方法中
// 一般情況下這里window==null
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);
...
// 最后這里回調(diào)Activity的onCreate方法
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
...
}
handleLaunchActivity
的代碼中首先對(duì)WindowManagerGlobal進(jìn)行初始化,然后調(diào)用了performLaunchActivity
方法叛甫。代碼很多层宫,這里只截取了重要部分。首先會(huì)創(chuàng)建Application對(duì)象其监,然后再調(diào)用Activity的attach方法萌腿,把window作為參數(shù)傳進(jìn)去,最后回調(diào)activity的onCreate方法抖苦。所以這里最有可能創(chuàng)建window的方法就是Activity的attach方法了毁菱。我們進(jìn)去看一下:
final void attach(...,Context context,Window window, ...) {
...;
// 這里新建PhoneWindow對(duì)象,并對(duì)window進(jìn)行初始化
mWindow = new PhoneWindow(this, window, activityConfigCallback);
// Activity實(shí)現(xiàn)window的callBack接口锌历,把自己設(shè)置給window
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
// 這里初始化window的WindowManager對(duì)象
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
同樣只截取了重要代碼贮庞,attach方法參數(shù)非常多,我只留下了window相關(guān)的參數(shù)辩涝。在這方法里首先利用傳進(jìn)來的window創(chuàng)建了PhoneWindow贸伐。Activity實(shí)現(xiàn)window的callBack接口,可以把自己設(shè)置給window當(dāng)觀察者怔揩。當(dāng)window發(fā)生變化的時(shí)候可以通知activity。然后再創(chuàng)建WindowManager和PhoneWindow綁定在一起脯丝,這樣我們就可以通過windowManager操作PhoneWindow了商膊。(這里不是setWindowManager嗎,windowManager是什么時(shí)候創(chuàng)建的宠进?)我們進(jìn)去setWindowManager
方法看一下:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
// 這里創(chuàng)建了windowManager
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
這個(gè)方法里首先會(huì)獲取到應(yīng)用服務(wù)的WindowManager(實(shí)現(xiàn)類也是WindowManagerImpl)晕拆,然后通過這個(gè)應(yīng)用服務(wù)的WindowManager創(chuàng)建了新的windowManager。
從這里可以看到是利用系統(tǒng)服務(wù)的windowManager來創(chuàng)建新的windowManagerImpl,因而這個(gè)應(yīng)用所有的WindowManagerImpl都是同個(gè)內(nèi)核windowManager实幕,而創(chuàng)建出來的僅僅是包了個(gè)殼吝镣。
這樣PhoneWindow和WindowManagerImpl就綁定在一起了。Activity可以通過WindowManagerImpl來操作PhoneWindow昆庇。
到這里Activity的PhoneWindow和WindowManagerImpl對(duì)象就創(chuàng)建完成了末贾,接下來是如何把Activity的布局文件設(shè)置給PhoneWindow。在上面我講到調(diào)用Activity的attach方法之后整吆,會(huì)回調(diào)Activity的onCreate方法拱撵,在onCreate方法我們會(huì)調(diào)用setContentView
來設(shè)置布局,如下:
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
這里的getWindow
就是獲取到我們上面創(chuàng)建的PhoneWindow對(duì)象表蝙。我們繼續(xù)看下去:
// 注意他有多個(gè)重載的方法拴测,要選擇參數(shù)對(duì)應(yīng)的方法
public void setContentView(int layoutResID) {
// 創(chuàng)建DecorView
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 這里根據(jù)布局id加載布局
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 回調(diào)activity的方法
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
同樣我們只看重點(diǎn)代碼:
- 首先看decorView創(chuàng)建了沒有,沒有的話創(chuàng)建DecorView
- 把布局加載到DecorView中
- 回調(diào)Activity的callBack方法
這里補(bǔ)充一下什么是DecorView府蛇。DecorView是在PhoneWindow中預(yù)設(shè)好的一個(gè)布局集索,這個(gè)布局長這樣:
DecorView
他是一個(gè)垂直排列的布局,上面是ActionBar汇跨,下面是ContentView务荆,他是一個(gè)FrameLayout。我們的Activity布局就加載到ContentView里進(jìn)行顯示扰法。所以Decorview是Activity布局最頂層的viewGroup蛹含。
然后我們看一下怎么初始化DercorView
的:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 這里創(chuàng)建了DecorView
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 對(duì)DecorView進(jìn)行初始化,得到ContentView
mContentParent = generateLayout(mDecor);
...
}
}
installDecor
方法中主要是新建一個(gè)DecorView對(duì)象塞颁,然后加載預(yù)設(shè)好的布局對(duì)DecorView進(jìn)行初始化浦箱,(預(yù)設(shè)好的布局就是上面講述的布局)并獲取到這個(gè)預(yù)設(shè)布局的ContentView诵棵。好了然后我們?cè)倩氐絯indow的setContentView方法中迫横,初始化了DecorView之后,把Activity布局加載到DecorView的ContentView中如下代碼:
// 注意他有多個(gè)重載的方法耕捞,要選擇參數(shù)對(duì)應(yīng)的方法
public void setContentView(int layoutResID) {
...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 這里根據(jù)布局id加載布局
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 回調(diào)activity的方法
cb.onContentChanged();
}
}
所以可以看到Activitiy的布局確實(shí)是添加到DecorView的ContentView中伴网,這也是為什么onCreate中使用的是setContentView而不是setView蓬推。最后會(huì)回調(diào)Activity的方法告訴Activity,DecorView已經(jīng)創(chuàng)建并初始化完成了澡腾。
到這里DecorView創(chuàng)建完成了沸伏,但還缺少了最重要的一步:把DecorView作為window添加到屏幕上。從前面的介紹我們知道添加window需要用到WindowManagerImpl的addView方法动分。這一步是在ActivityThread的handleResumeActivity
方法中被執(zhí)行:
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
// 調(diào)用Activity的onResume方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
// 讓decorView顯示到屏幕上
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
這一步方法有兩個(gè)重點(diǎn):回調(diào)onResume方法毅糟,把decorView添加到屏幕上。我們看一下makeVisible
方法做了什么:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
是不是非常熟悉澜公?直接調(diào)用WindowManagerImpl的addView
方法來吧decorView添加到屏幕上姆另,至此,我們的Activity界面就會(huì)顯示在屏幕上了。
好了迹辐,這部分很長蝶防,最后來總結(jié)一下:
- 從Activity的啟動(dòng)流程可以得到Activity創(chuàng)建Window的過程
- 創(chuàng)建PhoneWindow -> 創(chuàng)建WindowManager -> 創(chuàng)建decorView -> 利用windowManager把DecorView顯示到屏幕上
- 回調(diào)onResume方法的時(shí)候,DecorView還沒有被添加到屏幕明吩,所以當(dāng)onResume被回調(diào)间学,指的是屏幕即將到顯示,而不是已經(jīng)顯示
PopupWindow
popupWindow日常使用的也比較多贺喝,最常見的需求是彈一個(gè)菜單出來等菱鸥。popupWindow也是利用windowManager來往屏幕上添加window,但躏鱼,popupWindow是依附于activity而存在的氮采,當(dāng)Activity未運(yùn)行時(shí),是無法彈出popupWindow的染苛,通過源碼可以知道鹊漠,當(dāng)調(diào)用onResume方法的時(shí)候,其實(shí)后續(xù)還有很多事情在做茶行,這個(gè)時(shí)候Activity也是尚未完全啟動(dòng)躯概,所以popupWindow不能在onCreate、onStart畔师、onResume方法中彈出娶靡。
彈出popupWindow的過程分為兩個(gè):
- 創(chuàng)建view;
- 通過windowManager添加window看锉。
首先看到PopupWindow的構(gòu)造方法:
public PopupWindow(View contentView, int width, int height, boolean focusable) {
if (contentView != null) {
mContext = contentView.getContext();
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
}
他有多個(gè)重載方法姿锭,但最終都會(huì)調(diào)用到這個(gè)有四個(gè)參數(shù)的方法。主要是前面的得到context和根據(jù)context獲得WindowManager伯铣。
然后我們看到他的顯示方法呻此。顯示方法有兩個(gè):showAtLocation
和showAsDropDown
。主要是處理顯示的位置不同腔寡,其他都是相似的焚鲜。我們看到第一個(gè)方法:
public void showAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
邏輯很簡單,父view的根布局存儲(chǔ)了起來放前,然后調(diào)用另外的重載方法:
public void showAtLocation(IBinder token, int gravity, int x, int y) {
// 如果contentView是空直接返回
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
detachFromAnchor();
mIsShowing = true;
mIsDropdown = false;
mGravity = gravity;
// 得到WindowManager.LayoutParams對(duì)象
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
// 做一些準(zhǔn)備工作
preparePopup(p);
p.x = x;
p.y = y;
// 執(zhí)行popupWindow顯示工作
invokePopup(p);
}
這個(gè)方法的邏輯主要有:
- 判斷contentView是否為空或者是否進(jìn)行顯示
- 做一些準(zhǔn)備工作
- 進(jìn)行popupWindow顯示工作
這里我們看一下他的準(zhǔn)備工作做了什么:
private void preparePopup(WindowManager.LayoutParams p) {
...
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
// 創(chuàng)建了DecorView
// 注意忿磅,這里的DecorView并不是我們之前講的DecorView,而是他的內(nèi)部類:PopupDecorView
mDecorView = createDecorView(mBackgroundView);
mDecorView.setIsRootNamespace(true);
...
}
接下來再看他的顯示工作:
private void invokePopup(WindowManager.LayoutParams p) {
...
// 調(diào)用windowManager添加window
mWindowManager.addView(decorView, p);
...
}
到這里popupWindow就會(huì)被添加到屏幕上了凭语。
最后總結(jié)一下:
- 根據(jù)參數(shù)構(gòu)建popupDecorView
- 把popupDecorView添加到屏幕上
Dialog
dialog的創(chuàng)建過程Activity比較像:創(chuàng)建PhoneWindow贝乎,初始化DecorView,添加DecorView。我這里就簡單講解一下。首先看到他的構(gòu)造方法:
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
// 獲取windowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// 構(gòu)造PhoneWindow
final Window w = new PhoneWindow(mContext);
mWindow = w;
// 初始化PhoneWindow
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
這里和前面的Activity創(chuàng)建過程非常像虫几,但是有個(gè)重點(diǎn)需要注意mWindowManager其實(shí)是Activity的WindowManager锤灿,這里的context一般是activity(實(shí)際上也只能是activity,非activity會(huì)拋出異常辆脸,相關(guān)內(nèi)容讀者有興趣可以閱讀這篇文章window的token驗(yàn)證但校,我們看到activity的getSystemService
方法:
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
// 獲取activity的windowManager
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
可以看到這里的windowManager確實(shí)是Activity的WindowManager
。接下來看到他的show方法:
public void show() {
...
// 回調(diào)onStart方法啡氢,獲取前面初始化好的decorview
onStart();
mDecor = mWindow.getDecorView();
...
WindowManager.LayoutParams l = mWindow.getAttributes();
...
// 利用windowManager來添加window
mWindowManager.addView(mDecor, l);
...
mShowing = true;
sendShowMessage();
}
注意這里的mWindowManager是Activity的WindowManager状囱,所以實(shí)際上,這里是添加到了Activity的PhoneWindow中倘是。接下來的和前面的添加流程一樣亭枷,這里我也不多講解了。
總結(jié)一下:
- dialog和popupWindow不同搀崭,dialog創(chuàng)建了新的PhoneWindow叨粘,使用了PhoneWindow的DecorView模板。
- 而popupWindow沒有dialog的顯示層級(jí)數(shù)更高瘤睹,會(huì)直接顯示在Activity上面升敲,在dialog后添加的popUpWindow也會(huì)顯示在dialog下。
- dialog的創(chuàng)建流程和activity非常像.
從Android架構(gòu)角度看Window
前面我們介紹過關(guān)于PhoneWindow和window之間的關(guān)系轰传,了解到PhoneWindow其實(shí)不是Window驴党,只是一個(gè)window容器。不知讀者有沒想過一個(gè)問題获茬,為什么谷歌要建一個(gè)不是window但卻名字是window的類港庄?是故意要迷惑我們嗎?要了解這個(gè)問題锦茁,我們先來回顧一下整個(gè)android的window機(jī)制結(jié)構(gòu)攘轩。
首先從WindowManagerService開始,我們知道WMS是window的最終管理者码俩,在WMS中為每一個(gè)應(yīng)用持有一個(gè)session度帮,關(guān)于session前面我們講過,每個(gè)應(yīng)用都是全局單例稿存,負(fù)責(zé)和WMS通信的binder對(duì)象笨篷。WMS為每個(gè)window都建立了一個(gè)windowStatus對(duì)象,同一個(gè)應(yīng)用的window使用同個(gè)session進(jìn)行跨進(jìn)程通信瓣履,結(jié)構(gòu)大概如下:
而負(fù)責(zé)與WMS通信的率翅,是viewRootImpl。前面我們講過每個(gè)view樹即為一個(gè)window袖迎,viewRootImpl負(fù)責(zé)和WMS進(jìn)行通信冕臭,同時(shí)也負(fù)責(zé)view的繪制腺晾。如果把上面的圖畫仔細(xì)一點(diǎn)就是:
圖中每一個(gè)windowStatus對(duì)應(yīng)一個(gè)viewRootImpl,WMS通過viewRootImpl來控制view辜贵。這也就是window機(jī)制的管理結(jié)構(gòu)悯蝉。當(dāng)我們需要添加window的時(shí)候,最終的邏輯實(shí)現(xiàn)是WindowManagerGlobal托慨,他的內(nèi)部使用自己的session創(chuàng)建一個(gè)viewRootImpl鼻由,然后向WMS申請(qǐng)?zhí)砑觲indow,結(jié)構(gòu)圖大概如下:
windowManagerGlobal使用自己的IWindowSession創(chuàng)建viewRootImpl厚棵,這個(gè)IWindowSession是全局單例蕉世。viewRootImpl和WMS申請(qǐng)創(chuàng)建window,然后WMS允許之后婆硬,再通知viewRootImpl繪制view狠轻,同時(shí)WMS通過windowStatus存儲(chǔ)了viewRootImpl的相關(guān)信息,這樣如果WMS需要修改view柿祈,直接通過viewRootImpl就可以修改view了哈误。
從上面的描述中可以發(fā)現(xiàn)我全程沒有提及到PhoneWindow
和WindowManagerImpl
。這是因?yàn)?code>他們不屬于window機(jī)制內(nèi)的類躏嚎,而是封裝于window機(jī)制之上的框架蜜自。假設(shè)如果沒有PhoneWindow和WindowManager我們?cè)撊绾翁砑右粋€(gè)window?首先需要調(diào)用WindowGlobal獲取session卢佣,再創(chuàng)建viewRootImpl重荠,再訪問wms,然后再利用viewRootImpl繪制view虚茶,是不是很復(fù)雜戈鲁,而這僅僅只是整體的步驟。而WindowManagerImpl正是這個(gè)功能嘹叫。他內(nèi)部擁有WindowManagerGlobal的單例婆殿,然后幫助我們完成了這一系列的步驟。同時(shí)罩扇,windowManagerImpl也是只有一個(gè)實(shí)例婆芦,其他的windowManagerImpl都是建立在windowManagerImpl單例上。這一點(diǎn)在前面有通過源碼介紹到喂饥。
另外消约,上面我講到PhoneWindow并不是window而是一個(gè)輔助Activity管理的工具類,那為什么他不要命名為windowUtils呢员帮?首先或粮,PhoneWindow這個(gè)類是谷歌給window機(jī)制進(jìn)行更上一層的封裝。PhoneWindow內(nèi)部擁有一個(gè)DecorView捞高,我們的布局view都是添加到decorView中的氯材,因?yàn)槲覀兛梢酝ㄟ^給decorView設(shè)置背景渣锦,寬高度,標(biāo)題欄浓体,按鍵反饋等等泡挺,來間接給我們的布局view設(shè)置。這樣一來命浴,PhoneWindow的存在,向開發(fā)者屏蔽真正的window贱除,暴露給開發(fā)者一個(gè)“存在的”window
生闲。我們可以認(rèn)為PhoneWindow就是一個(gè)window,window是view容器月幌。當(dāng)我們需要在屏幕上添加view的時(shí)候碍讯,只需要獲得應(yīng)用window對(duì)應(yīng)的windowManagerImpl,然后直接調(diào)用addView方法添加view即可扯躺。這里也可以解釋為什么windowManager的接口方法是addView而不是addWindow捉兴,一是window確實(shí)是以view的存在形式?jīng)]錯(cuò),二是為了向開發(fā)者屏蔽真正的window录语,讓我們以為是在往window中添加view倍啥,window是真實(shí)存在的東西。他們的關(guān)系畫個(gè)圖如下:
黃色部分輸于谷歌提供給開發(fā)者的window框架澎埠,而綠色是真正的window機(jī)制結(jié)構(gòu)虽缕。通過PhoneWindow我們可以很方便地進(jìn)行window操作,而不須了解底層究竟是如何工作的蒲稳。PhoneWindow的存在氮趋,更是讓window的“可見性”得到了實(shí)現(xiàn),讓window變成了一個(gè)“view容器”江耀。
好了最后來總結(jié)一下:
- Android內(nèi)部的window機(jī)制與谷歌暴露給我們的api是不一樣的剩胁,谷歌封裝的目的是為了讓我們更好地使用window。
- dialog祥国、popupWindow等框架更是對(duì)具體場景進(jìn)行更進(jìn)一步的封裝昵观。
- 我們?cè)诹私鈝indow機(jī)制的時(shí)候,需要跳過應(yīng)用層系宫,看到window的本質(zhì)索昂,才能更好地幫助我們理解window。
- 在android的其他地方也是一樣扩借,利用封裝向開發(fā)者屏蔽底層邏輯椒惨,讓我們更好地運(yùn)用。但如果我們需要了解他的機(jī)制的時(shí)候潮罪,就需要繞過這層封裝康谆,看到本質(zhì)领斥。
總結(jié)
全文到這里,就基本結(jié)束了沃暗。下面先總結(jié)一下我這篇文章說了什么:
- 詳述了什么是window
- 對(duì)window的各種參數(shù)進(jìn)行講解
- 講解window機(jī)制內(nèi)的關(guān)鍵類
- 從源碼講解window的添加流程以及各大組件的window添加流程
- 詳解了PhoneWindow與window的關(guān)系月洛,談了關(guān)于谷歌的封裝思想
- 文中最重要的一點(diǎn)就是認(rèn)識(shí)window的本質(zhì),區(qū)分好window和view之間的關(guān)系以及window與PhoneWindow的關(guān)系孽锥。
筆者在寫這篇文章的時(shí)候嚼黔,對(duì)于各節(jié)的安排是比較猶豫的:如果先講概念,沒有源碼流程的講解很難懂惜辑;先講源碼流程唬涧,沒有概念的認(rèn)知很難讀懂源碼。最終還是決定了先講window的真正概念盛撑,先讓讀者有個(gè)整體上的感知碎节。
文章很長,筆者對(duì)于window想要講的都在這篇文章中抵卫。
希望文章對(duì)你有幫助狮荔。
全文到此,感謝你的閱讀
原創(chuàng)不易介粘,覺得有幫助可以點(diǎn)贊收藏評(píng)論轉(zhuǎn)發(fā)關(guān)注殖氏。
筆者才疏學(xué)淺,有任何錯(cuò)誤歡迎評(píng)論區(qū)或私信交流碗短。
如需轉(zhuǎn)載請(qǐng)私信交流受葛。
另外歡迎光臨筆者的個(gè)人博客:傳送門
————————————————
版權(quán)聲明:本文為CSDN博主「一只修仙的猿」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議偎谁,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明总滩。
原文鏈接:https://blog.csdn.net/weixin_43766753/article/details/108350589