Android理解Window和WindowManager

導語

Window表示一個窗口的概念,在日常開發(fā)中接觸的機會并不多,如果我們需要做一些類似懸浮窗的功能就需要Window來實現(xiàn)。

主要內(nèi)容

  • Window和WindowManager
  • Window的內(nèi)部機制
  • Window的創(chuàng)建過程

具體內(nèi)容

Window是一個抽象類,具體實現(xiàn)是 PhoneWindow 。不管是 Activity 、 Dialog 、 Toast 它們的視圖都是附加在Window上的,因此Window實際上是View的直接管理者。WindowManager 是外界訪問Window的入口,通過WindowManager可以創(chuàng)建Window,而Window的具體實現(xiàn)位于 WindowManagerService 中,WindowManager和WindowManagerService的交互是一個IPC過程。

Window和WindowManager

下面代碼演示了通過WindowManager添加Window的過程:

mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mFloatingButton = new Button(this);
mFloatingButton.setText("click me");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0,
PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.type = LayoutParams.TYPE_SYSTEM_ERROR;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mFloatingButton.setOnTouchListener(this);
mWindowManager.addView(mFloatingButton, mLayoutParams);

上述代碼將一個button添加到屏幕坐標為(100,300)的位置上。WindowManager的flags和type這兩個屬性比較重要。
Flags代表Window的屬性,控制Window的顯示特性:

  • FLAG_NOT_FOCUSABLE
    在此模式下,Window不需要獲取焦點,也不需要接收各種輸入事件,這個標記同時會啟用FLAG_NOT_TOUCH_MODAL,最終事件會直接傳遞給下層具有焦點的Window。
  • FLAG_NOT_TOUCH_MODAL
    在此模式下,系統(tǒng)將當前Window區(qū)域以外的點擊事件傳遞給底層的Window,當前Window區(qū)域內(nèi)的單擊事件則自己處理。一般需要開啟此標記。
  • FLAG_SHOW_WHEN_LOCKED
    開啟此模式Window將顯示在鎖屏界面上。

type參數(shù)表示W(wǎng)indow的類型:

  • 應用Window:Activity。
  • 子Window: 如Dialog。
  • 系統(tǒng)Window :如Toast和系統(tǒng)狀態(tài)欄。

Window是分層的,每個Window對應一個z-ordered,層級大的會覆蓋在層級小的上面,和HTM的z-index概念一樣。在三類Window中,應用Window的層級范圍是199,子Window的層級范圍是10001999,系統(tǒng)Window的層級范圍是2000~2999,這些值對應WindowManager.LayoutParams的type參數(shù)。一般系統(tǒng)Window選用 TYPE_SYSTEM_OVERLAY 或者 TYPE_SYSTEM_ERROR ( 同時需要權(quán)限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> ) 。

WindowManager提供的功能很簡單,常用的只有三個方法:

  • 添加View。
  • 更新View。
  • 刪除View。

這個三個方法定義在 ViewManager 中,而WindowManager繼承了ViewManager。

public interface ViewManager{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

如何拖動window?
給view設置onTouchListener:mFloatingButton.setOnTouchListener(this)。在onTouch方法中更新view的位置,這個位置根據(jù)手指的位置設定。

Window的內(nèi)部機制

Window是一個抽象的概念,每一個Window都對應著一個View和一個ViewRootImpl,Window和View通過ViewRootImpl來建立聯(lián)系。因此Window并不是實際存在的,它是以View的形式存在的。所以WindowManager的三個方法都是針對View的,說明View才是Window存在的實體。在實際使用中無法直接訪問Window,必須通過WindowManager來訪問Window。

Window的添加過程

Window的添加過程需要通過WindowManager的addView()來實現(xiàn), 而WindowManager是一個接口, 它的真正實現(xiàn)是WindowManagerImpl類。

WindowManagerImpl并沒有直接實現(xiàn)Window的三大操作, 而是全部交給了WindowManagerGlobal來處理. WindowManagerGlobal以工廠的形式向外提供自己的實例. 而WindowManagerImpl這種工作模式就典型的橋接模式, 將所有的操作全部委托給WindowManagerGlobal來實現(xiàn)。

  1. 檢查所有參數(shù)是否合法, 如果是子Window那么還需要調(diào)整一些布局參數(shù)。
  2. 創(chuàng)建ViewRootImpl并將View添加到列表中。
  3. 通過ViewRootImpl來更新界面并完成Window的添加過程。

這個過程是通過ViewRootImpl的setView()來完成的. View的繪制過程是由ViewRootImpl來完成的, 在內(nèi)部會調(diào)用requestLayout()來完成異步刷新請求. 而scheduleTraversals()實際上是View繪制的入口. 接著會通過WindowSession完成Window的添加過程(Window的添加過程是一次IPC調(diào)用). 最終會通過WindowManagerService來實現(xiàn)Window的添加。

WindowManagerService內(nèi)部會為每一個應用保留一個單獨的Session。

Window的刪除過程

Window 的刪除過程和添加過程一樣, 都是先通過WindowManagerImpl后, 在進一步通過WindowManagerGlobal的removeView()來實現(xiàn)的。

方法內(nèi)首先通過findViewLocked來查找待刪除的View的索引, 這個過程就是建立數(shù)組遍歷, 然后調(diào)用removeViewLocked來做進一步的刪除。

這里通過ViewRootImpl的die()完成來完成刪除操作. die()方法只是發(fā)送了請求刪除的消息后就立刻返回了, 這個時候View并沒有完成刪除操作, 所以最后會將其添加到mDyingViews中, mDyingViews表示待刪除的View的列表。

die方法中只是做了簡單的判斷, 如果是異步刪除那么就發(fā)送一個MSG_DIE的消息, ViewRootImpl中的Handler會處理此消息并調(diào)用doDie(); 如果是同步刪除, 那么就不發(fā)送消息直接調(diào)用doDie()方法。

在doDie()方法中會調(diào)用dispatchDetachedFromWindow()方法, 真正刪除View的邏輯在這個方法內(nèi)部實現(xiàn). 其中主要做了四件事:

  1. 垃圾回收的相關(guān)工作,比如清除數(shù)據(jù)和消息,移除回調(diào)。
  2. 通過Session的remove方法刪除Window:mWindowSession.remove(mWindow),這同樣是一個IPC過程,最終會調(diào)用WMS的removeWindow()方法。
  3. 調(diào)用View的dispatchDetachedFromWindow()方法, 內(nèi)部會調(diào)用View的onDetachedFromWindow()以及onDetachedFromWindowInternal()。而對于onDetachedFromWindow()就是在View從Window中移除時,這個方法就會被調(diào)用,可以在這個方法內(nèi)部做一些資源回收的工作。比如停止動畫,停止線程。
  4. 調(diào)用WindowManagerGlobal#doRemoveView方法刷新數(shù)據(jù),包括mRoots,mParams,mDyingViews,需要將當前Window所關(guān)聯(lián)的這三類對象從列表中刪除。
Window的更新過程

WindowManagerGlobal#updateViewLayout()方法做的比較簡單,它需要更新View的LayoutParams并替換掉老的LayoutParams,接著在更新ViewRootImpl中的LayoutParams。這一步主要是通過setLayoutParams()方法實現(xiàn)。

在ViewRootImpl中會通過scheduleTraversals()來對View重新布局,包括測量、布局、重繪。除了View本身的重繪以外,ViewRootImpl還會通過WindowSession來更新Window的視圖,這個過程最后由WMS的relayoutWindow()實現(xiàn)同樣是一個IPC過程。

Window的創(chuàng)建過程

由之前的分析可以知道,View是Android中視圖的呈現(xiàn)方式,但是View不能單獨存在,必須附著在Window這個抽象的概念上面,因此有視圖的地方就有Window。這些視圖包括:Activity、Dialog、Toast、PopUpWindow、菜單等等。

Activity的Window創(chuàng)建過程

Activity的大體啟動流程: 最終會由ActivityThread中的PerformLaunchActivity()來完成整個啟動過程, 這個方法內(nèi)部會通過類加載器創(chuàng)建Activity的實例對象, 并調(diào)用其attach()方法為其關(guān)聯(lián)運行過程中所依賴的一系列上下文環(huán)境變量。

在attach()方法里, 系統(tǒng)會創(chuàng)建Activity所屬的Window對象并為其設置回調(diào)接口, Window對象的創(chuàng)建是通過PolicyManager#makeNewWindow()方法實現(xiàn). 由于Activity實現(xiàn)了Window的CallBack接口, 因此當Window接收到外界的狀態(tài)改變的時候就會回調(diào)Activity方法. 比如說我們熟悉的onAttachedToWindow(), onDetachedFromWindow(), dispatchTouchEvent()等等。

Activity將具體實現(xiàn)交給了Window處理, 而Window的具體實現(xiàn)就是PhoneWindow, 所以只需要看PhoneWindow的相關(guān)邏輯。分為以下幾步:

  1. 如果沒有DecorView, 那么就創(chuàng)建它. 由installDecor()—>generateDecor()觸發(fā)。
  2. 將View添加到DecorView的mContentParent中。
  3. 回調(diào)Activity的onContentChanged()通知activity視圖已經(jīng)發(fā)生改變。

這個時候DecorView已經(jīng)被創(chuàng)建并初始化完畢,Activity的布局文件也已經(jīng)添加成功到DecorView的mContentParent中。但是這個時候DecorView還沒有被WindowManager正式添加到Window中。雖然早在Activity的attach方法中window就已經(jīng)被創(chuàng)建了,但是這個時候由于DecorView并沒有被WindowManager識別,所以這個時候的Window無法提供具體功能,因為他還無法接收外界的輸入信息。

在ActivityThread#handleResumeActivity()方法中,首先會調(diào)用Activity#onResume(),接著會調(diào)用Activity#makeVisible(),正是在makeVisible方法中,DecorView真正的完成了添加和顯示這兩個過程。

Dialog的Window創(chuàng)建過程

Dialog的Window的創(chuàng)建過程和Activity類似, 有如下幾步:

  1. 創(chuàng)建Window。
    Dialog的創(chuàng)建后的實際就是PhoneWindow, 這個過程和Activity的Window創(chuàng)建過程一致。
  2. 初始化DecorView并將Dialog的視圖添加到DecorView中。
    這個過程也類似, 都是通過Window去添加指定的布局文件。
  3. 將DecorView添加到Window中并顯示。
    在Dialog的show方法中,會通過WindowManager將DecorView添加Window中。

普通的Dialog有一個特殊之處,那就是必須采用Activity的Content,如果采用Application的Content,那么就會報錯。報的錯是沒有應用token所導致的,而應用token一般只有Activity才擁有。

還有一種方法,系統(tǒng)Window比較特殊,他可以不需要token,因此只需要指定對話框的Window為系統(tǒng)類型就可以正常彈出對話框。

//JAVA 給Dialog的Window改變?yōu)橄到y(tǒng)級的Window
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
//XML 聲明權(quán)限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Toast的Window創(chuàng)建過程

Toast和Dialog不同, 它的工作過程就稍顯復雜. 首先Toast也是基于Window來實現(xiàn)的. 但是由于Toast具有定時取消的功能, 所以系統(tǒng)采用了Handler. 在Toast的內(nèi)部有兩類IPC過程, 第一類是Toast訪問NotificationManagerService()后面簡稱NMS. 第二類是NotificationManagerService回調(diào)Toast里的TN接口.

Toast屬于系統(tǒng)Window, 它內(nèi)部的視圖有兩種方式指定, 一種是系統(tǒng)默認的樣式, 另一種是通過setView方法來指定一個自定義View. 不管如何, 他們都對應Toast的一個View類型的內(nèi)部成員mNextView. Toast內(nèi)部提供了cancel和show兩個方法. 分別用于顯示和隱藏Toast. 他們內(nèi)部是一個IPC過程.

顯示和隱藏Toast都是需要通過NMS來實現(xiàn)的. 由于NMS運行在系統(tǒng)的進程中, 所以只能通過遠程調(diào)用的方式來顯示和隱藏Toast. 而TN這個類是一個Binder類. 在Toast和NMS進行IPC的過程中, 當NMS處理Toast的顯示或隱藏請求時會跨進程回調(diào)TN的方法. 這個時候由于TN運行在Binder線程池中, 所以需要通過Handler將其切換到當前主線程. 所以由其可知, Toast無法在沒有Looper的線程中彈出, 因為Handler需要使用Looper才能完成切換線程的功能.

對于非系統(tǒng)應用來說, 最多能同時存在對Toast封裝的ToastRecord上限為50個. 這樣做是為了防止DOS(Denial of Service). 如果不這樣, 當通過大量循環(huán)去連續(xù)的彈出Toast, 這將會導致其他應用沒有機會彈出Toast, 那么對于其他應用的Toast請求, 系統(tǒng)的行為就是拒絕服務, 這就是拒絕服務攻擊的含義.

在ToastRecord被添加到mToastQueue()中后, NMS就會通過showNextToastLocked()方法來顯示當前的Toast.

Toast的顯示是由ToastRecord的callback來完成的. 這個callback實際上就是Toast中的TN對象的遠程Binder. 通過callback來訪問TN中的方法是需要跨進程的. 最終被調(diào)用的TN中的方法會運行在發(fā)起Toast請求的應用的Binder線程池.

Toast的隱藏也會通過ToastRecord的callback完成的.同樣是一次IPC過程. 方式和Toast顯示類似.

以上基本說明Toast的顯示和影響過程實際上是通過Toast中的TN這個類來實現(xiàn)的. 他有兩個方法show(), hide(). 分別對應著Toast的顯示和隱藏. 由于這兩個方法是被NMS以跨進程的方式調(diào)用的, 因此他們運行在Binder線程池中. 為了將執(zhí)行環(huán)境切換到Toast請求所在線程中, 在他們內(nèi)部使用了handler。

TN的handleShow中會將Toast的視圖添加到Window中.
TN的handleHide中會將Toast的視圖從Window中移除.

總結(jié)

  1. 在創(chuàng)建視圖并顯示出來時,首先是通過創(chuàng)建一個Window對象,然后通過WindowManager對象的 addView(View view, ViewGroup.LayoutParams params); 方法將 contentView 添加到Window中,完成添加和顯示視圖這兩個過程。
  2. 在關(guān)閉視圖時,通過WindowManager來移除DecorView, mWindowManager.removeViewImmediate( view); 。
  3. Toast比較特殊,具有定時取消功能,所以系統(tǒng)采用了Handler,內(nèi)部有兩類IPC過程:
    1. Toast訪問 NotificationManagerService。
    2. NotificationManagerService 回調(diào)Toast里的 TN 接口。

顯示和隱藏Toast都通過NotificationManagerService( NMS) 來實現(xiàn),而NMS運行在系統(tǒng)進程中,所以只能通過IPC來進行顯示/隱藏Toast。而TN是一個Binder類,在Toast和NMS進行IPC的過程中,當NMS處理Toast的顯示/隱藏請求時會跨進程回調(diào)TN中的方法,這時由于TN運行在Binder線程池中,所以需要通過Handler將其切換到當前線程( 即發(fā)起Toast請求所在的線程) ,然后通過WindowManager的 addView/removewView 方法真正完成顯示和隱藏Toast。

更多內(nèi)容戳這里(整理好的各種文集)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內(nèi)容