《深入理解Android 卷III》第四章 深入理解WindowManagerService
《深入理解Android 卷III》即將釋出,作者是張大偉。此書填補了深入理解Android Framework卷中的一個主要空白,即Android Framework中和UI相關的部分。在一個特別講究顏值的時代,本書分析了Android 4.2中WindowManagerService、ViewRoot、Input系統、StatusBar、Wallpaper等重要“顏值繪製/處理”模組
第4章 深入理解WindowManagerService(節選)
本章主要內容:
· 示例最原始最簡單的視窗建立方法
· 研究WMS的視窗管理結構
· 探討WMS佈局系統的工作原理
· 研究WMS動畫系統的工作原理
本章涉及的原始碼檔名及位置:
· SystemServer.java
frameworks/base/services/java/com/android/server/SystemServer.java
· WindowManagerService.java
frameworks/base/services/java/com/android/server/wm/WindowManagerService.java
· ActivityStack.java
frameworks/base/services/java/com/android/server/am/ActivityStack.java
· WindowState.java
frameworks/base/services/java/com/android/server/wm/WindowState.java
· PhoneWindowManager.java
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
· AccelerateDecelerateInterpolator.java
frameworks/base/core/java/android/view/animation/AccelerateDecelerateInterpolator.java
· Animation.java
frameworks/base/core/java/android/view/animation/Animation.java
· AlphaAnimation.java
frameworks/base/core/java/android/view/animation/AlphaAnimation.java
· WindowAnimator.java
frameworks/base/services/java/com/android/server/wm/WindowAnimator.java
· WindowStateAnimator.java
frameworks/base/services/java/com/android/server/wm/WindowStateAnimator.java
4.1 初識WindowManagerService
WindowManagerService(以下簡稱WMS)是繼ActivityManagerService與PackageManagerService之後又一個複雜卻十分重要的系統服務。
在介紹WMS之前,首先要了解視窗(Window)是什麼。
Android系統中的視窗是螢幕上的一塊用於繪製各種UI元素並可以響應應使用者輸入的一個矩形區域。從原理上來講,視窗的概念是獨自佔有一個Surface例項的顯示區域。例如Dialog、Activity的介面、桌布、狀態列以及Toast等都是視窗。
《卷I》第8章曾詳細介紹了一個Activity通過Surface來顯示自己的過程:
· Surface是一塊畫布,應用可以隨心所欲地通過Canvas或者OpenGL在其上作畫。
· 然後通過SurfaceFlinger將多塊Surface的內容按照特定的順序(Z-order)進行混合並輸出到FrameBuffer,從而將Android“漂亮的臉蛋”顯示給使用者。
既然每個視窗都有一塊Surface供自己塗鴉,必然需要一個角色對所有視窗的Surface進行協調管理。於是,WMS便應運而生。WMS為所有視窗分配Surface,掌管Surface的顯示順序(Z-order)以及位置尺寸,控制視窗動畫,並且還是輸入系統的一重要的中轉站。
說明一個視窗擁有顯示和響應使用者輸入這兩層含義,本章將側重於分析視窗的顯示,而響應使用者輸入的過程則在第5章進行詳細的介紹。
本章將深入分析WMS的兩個基礎子系統的工作原理:
· 佈局系統(Layout System),計算與管理視窗的位置、層次。
· 動畫系統(Animation System),根據佈局系統計算的視窗位置與層次渲染視窗動畫。
為了讓讀者對WMS的功能以及工作方式有一個初步地認識,並見識一下WMS的強大,本節將從一個簡單而神奇的例子開始WMS的學習之旅。
4.1.1 一個從命令列啟動的動畫視窗
1.SampleWindow的實現
在這一節裡將編寫一個最簡單的Java程式SampleWindow,僅使用WMS的介面建立並渲染一個動畫視窗。此程式將拋開Activity、Wallpaper等UI架構的複雜性,直接了當地揭示WMS的客戶端如何申請、渲染並登出自己的視窗。同時這也初步地反應了WMS的工作方式。
這個例子很簡單,只有三個檔案:
· SampleWindow.java 主程式原始碼。
· Android.mk 編譯指令碼。
· sw.sh 啟動器。
分別看一下這三個檔案的實現:
[-->SampleWindow.java::SampleWindow]
package understanding.wms.samplewindow;
......
public class SampleWindow {
publicstatic void main(String[] args) {
try {
//SampleWindow.Run()是這個程式的主入口
new SampleWindow().Run();
} catch (Exception e) {
e.printStackTrace();
}
}
//IWindowSession 是客戶端向WMS請求視窗操作的中間代理,並且是程序唯一的
IWindowSession mSession = null;
//InputChannel 是視窗接收使用者輸入事件的管道。在第5章中將對其進行詳細的探討
InputChannel mInputChannel = new InputChannel();
// 下面的三個Rect儲存了視窗的佈局結果。其中mFrame表示了視窗在螢幕上的位置與尺寸
// 在4.4中將詳細介紹它們的作用以及計算原理
RectmInsets = new Rect();
RectmFrame = new Rect();
RectmVisibleInsets = new Rect();
Configuration mConfig = new Configuration();
// 視窗的Surface,在此Surface上進行的繪製都將在此視窗上顯示出來
SurfacemSurface = new Surface();
// 用於在視窗上進行繪圖的畫刷
PaintmPaint = new Paint();
// 新增視窗所需的令牌,在4.2節將會對其進行介紹
IBindermToken = new Binder();
// 一個視窗物件,本例演示瞭如何將此視窗新增到WMS中,並在其上進行繪製操作
MyWindowmWindow = new MyWindow();
//WindowManager.LayoutParams定義了視窗的佈局屬性,包括位置、尺寸以及視窗型別等
LayoutParams mLp = new LayoutParams();
Choreographer mChoreographer = null;
//InputHandler 用於從InputChannel接收按鍵事件做出響應
InputHandler mInputHandler = null;
booleanmContinueAnime = true;
publicvoid Run() throws Exception{
Looper.prepare();
// 獲取WMS服務
IWindowManager wms = IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
// 通過WindowManagerGlobal獲取程序唯一的IWindowSession例項。它將用於向WMS
// 傳送請求。注意這個函式在較早的Android版本(如4.1)位於ViewRootImpl類中
mSession= WindowManagerGlobal.getWindowSession(Looper.myLooper());
// 獲取螢幕解析度
IDisplayManager dm = IDisplayManager.Stub.asInterface(
ServiceManager.getService(Context.DISPLAY_SERVICE));
DisplayInfo di = dm.getDisplayInfo(Display.DEFAULT_DISPLAY);
Point scrnSize = new Point(di.appWidth, di.appHeight);
// 初始化WindowManager.LayoutParams
initLayoutParams(scrnSize);
// 將新視窗新增到WMS
installWindow(wms);
// 初始化Choreographer的例項,此例項為執行緒唯一。這個類的用法與Handler
// 類似,不過它總是在VSYC同步時回撥,所以比Handler更適合做動畫的迴圈器[1]
mChoreographer= Choreographer.getInstance();
// 開始處理第一幀的動畫
scheduleNextFrame();
// 當前執行緒陷入訊息迴圈,直到Looper.quit()
Looper.loop();
// 標記不要繼續繪製動畫幀
mContinueAnime= false;
// 解除安裝當前Window
uninstallWindow(wms);
}
publicvoid initLayoutParams(Point screenSize) {
// 標記即將安裝的視窗型別為SYSTEM_ALERT,這將使得視窗的ZOrder順序比較靠前
mLp.type = LayoutParams.TYPE_SYSTEM_ALERT;
mLp.setTitle("SampleWindow");
// 設定視窗的左上角座標以及高度和寬度
mLp.gravity = Gravity.LEFT | Gravity.TOP;
mLp.x = screenSize.x / 4;
mLp.y = screenSize.y / 4;
mLp.width = screenSize.x / 2;
mLp.height = screenSize.y / 2;
// 和輸入事件相關的Flag,希望當輸入事件發生在此視窗之外時,其他視窗也可以接受輸入事件
mLp.flags = mLp.flags | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
}
publicvoid installWindow(IWindowManager wms) throws Exception {
// 首先向WMS宣告一個Token,任何一個Window都需要隸屬與一個特定型別的Token
wms.addWindowToken(mToken,WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
// 設定視窗所隸屬的Token
mLp.token = mToken;
// 通過IWindowSession將視窗安裝進WMS,注意,此時僅僅是安裝到WMS,本例的Window
// 目前仍然沒有有效的Surface。不過,經過這個呼叫後,mInputChannel已經可以用來接受
// 輸入事件了
mSession.add(mWindow,0, mLp, View.VISIBLE, mInsets, mInputChannel);
/*通過IWindowSession要求WMS對本視窗進行重新佈局,經過這個操作後,WMS將會為視窗
建立一塊用於繪製的Surface並儲存在引數mSurface中。同時,這個Surface被WMS放置在
LayoutParams所指定的位置上 */
mSession.relayout(mWindow,0, mLp, mLp.width, mLp.height, View.VISIBLE,
0, mFrame, mInsets,mVisibleInsets, mConfig, mSurface);
if(!mSurface.isValid()) {
thrownew RuntimeException("Failed creating Surface.");
}
// 基於WMS返回的InputChannel建立一個Handler,用於監聽輸入事件
//mInputHandler一旦被建立,就已經在監聽輸入事件了
mInputHandler= new InputHandler(mInputChannel, Looper.myLooper());
}
publicvoid uninstallWindow(IWindowManager wms) throws Exception {
// 從WMS處解除安裝視窗
mSession.remove(mWindow);
// 從WMS處移除之前新增的Token
wms.removeWindowToken(mToken);
}
publicvoid scheduleNextFrame() {
// 要求在顯示系統重新整理下一幀時回撥mFrameRender,注意,只回調一次
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION
, mFrameRender, null);
}
// 這個Runnable物件用以在視窗上描繪一幀
publicRunnable mFrameRender = new Runnable() {
@Override
publicvoid run() {
try{
// 獲取當期時間戳
long time = mChoreographer.getFrameTime() % 1000;
// 繪圖
if (mSurface.isValid()) {
Canvas canvas = mSurface.lockCanvas(null);
canvas.drawColor(Color.DKGRAY);
canvas.drawRect(2 * mLp.width * time / 1000
- mLp.width, 0, 2 *mLp.width * time
/ 1000, mLp.height,mPaint);
mSurface.unlockCanvasAndPost(canvas);
mSession.finishDrawing(mWindow);
}
if(mContinueAnime)
scheduleNextFrame();
} catch (Exception e) {
e.printStackTrace();
}
}
};
// 定義一個類繼承InputEventReceiver,用以在其onInputEvent()函式中接收視窗的輸入事件
classInputHandler extends InputEventReceiver {
Looper mLooper = null;
publicInputHandler(InputChannel inputChannel, Looper looper) {
super(inputChannel,looper);
mLooper= looper;
}
@Override
publicvoid onInputEvent(InputEvent event) {
if(event instanceof MotionEvent) {
MotionEvent me = (MotionEvent)event;
if (me.getAction() ==MotionEvent.ACTION_UP) {
// 退出程式
mLooper.quit();
}
}
super.onInputEvent(event);
}
}
// 實現一個繼承自IWindow.Stub的類MyWindow。
classMyWindow extends IWindow.Stub {
// 保持預設的實現即可
}
}
由於此程式使用了大量的隱藏API(即SDK中沒有定義這些API),因此需要放在Android原始碼環境中進行編譯它。對應的Android.mk如下:
[-->Android.mk]
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE := samplewindow
include $(BUILD_JAVA_LIBRARY)
將這兩個檔案放在$TOP/frameworks/base/cmds/samplewindow/下,然後用make或mm命令進行編譯。最終生成的結果是samplewindow.jar,檔案位置在out/target/<ProductName>/system/framework/下。將該檔案通過adb push到手機的/system/framework/下。
提示讀者可使用Android4.2模擬器來執行此程式。
然而,samplewindow.jar不是一個可執行程式,。故,需藉助Android的app_process工具來載入並執行它。筆者編寫了一個指令碼做為啟動器:
[-->sw.sh]
base=/system
export CLASSPATH=$base/framework/samplewindow.jar
exec app_process $base/binunderstanding.wms.samplewindow.SampleWindow "[email protected]"
注意app_process其實就是大名鼎鼎的zygote。不過,只有使用--zygote引數啟動時它才會給改名為zygote[2],否則就像java –jar命令一樣,執行指定類的main靜態函式。
在手機中執行該指令碼,其執行結果是一個灰色的方塊不斷地從螢幕左側移動到右側,如圖4-1所示。
圖 4-1 SampleWindow在手機中的執行效果
2.初識視窗的建立、繪製與銷燬
SampleWindow的這段程式碼雖然簡單,但是卻很好地提煉了一個視窗的建立、繪製以及銷燬的過程。注意,本例沒有使用任何 WMS以外的系統服務,也沒有使用Android系統四大元件的框架,也就是說,如果你願意,可以利用WMS實現自己的UI與應用程式框架,這樣就可以衍生出一個新的平臺了。
總結在客戶端建立一個視窗的步驟:
· 獲取IWindowSession和WMS例項。客戶端可以通過IWindowSession向WMS傳送請求。
· 建立並初始化WindowManager.LayoutParams。注意這裡是WindowManager下的LayoutParams,它繼承自ViewGroup.LayoutParams類,並擴充套件了一些視窗相關的屬性。其中最重要的是type屬性。這個屬性描述了視窗的型別,而視窗型別正是WMS對多個視窗進行ZOrder排序的依據。
· 向WMS新增一個視窗令牌(WindowToken)。本章後續將分析視窗令牌的概念,目前讀者只要知道,視窗令牌描述了一個顯示行為,並且WMS要求每一個視窗必須隸屬於某一個顯示令牌。
· 向WMS新增一個視窗。必須在LayoutParams中指明此視窗所隸屬於的視窗令牌,否則在某些情況下新增操作會失敗。在SampleWindow中,不設定令牌也可成功完成新增操作,因為視窗的型別被設為TYPE_SYSTEM_ALERT,它是系統視窗的一種。而對於系統視窗,WMS會自動為其建立顯示令牌,故無需客戶端操心。此話題將會在後文進行更具體的討論。
· 向WMS申請對視窗進行重新佈局(relayout)。所謂的重新佈局,就是根據視窗新的屬性去調整其Surface相關的屬性,或者重新建立一個Surface(例如視窗尺寸變化導致之前的Surface不滿足要求)。向WMS新增一個視窗之後,其僅僅是將它在WMS中進行了註冊而已。只有經過重新佈局之後,窗口才擁有WMS為其分配的畫布。有了畫布,視窗之後就可以隨時進行繪製工作了。
而視窗的繪製過程如下:
· 通過Surface.lock()函式獲取可以在其上作畫的Canvas例項。
· 使用Canvas例項進行作畫。
· 通過Surface.unlockCanvasAndPost()函式提交繪製結果。
提示關於Surface的原理與使用方法,請參考《卷 I》第8章“深入理解Surface系統”。
這是對Surface作畫的標準方法。在客戶端也可以通過OpenGL進行作畫,不過這超出了本書的討論範圍。另外,在SampleWindow例子中使用了Choreographer類進行了動畫幀的安排。Choreographer意為編舞指導,是Jelly Bean新增的一個工具類。其用法與Handler的post()函式非Z且不會再顯示新的視窗,則需要從WMS將之前新增的顯示令牌一併刪除。
3.視窗的概念
在SampleWindow例子中,有一個名為mWindow(型別為IWindow)的變數。讀者可能會理所當然地認為它就是視窗了。其實這種認識並不完全正確。IWindow繼承自Binder,並且其Bn端位於應用程式一側(在例子中IWindow的實現類MyWindow就繼承自IWindow.Stub),於是其在WMS一側只能作為一個回撥,以及起到視窗Id的作用。
那麼,視窗的本質是什麼呢?
是進行繪製所使用的畫布:Surface。
當一塊Surface顯示在螢幕上時,就是使用者所看到的視窗了。客戶端向WMS新增一個視窗的過程,其實就是WMS為其分配一塊Surface的過程,一塊塊Surface在WMS的管理之下有序地排布在螢幕上,Android才得以呈現出多姿多彩的介面來。所以從這個意義上來講,WindowManagerService被稱之為SurfaceManagerService也說得通的。
於是,根據對Surface的操作型別可以將Android的顯示系統分為三個層次,如圖4-2所示。
圖 4-2 Android顯示系統的三個層次
在圖4-2中:
· 第一個層次是UI框架層,其工作為在Surface上繪製UI元素以及響應輸入事件。
· 第二個層次為WMS,其主要工作在於管理Surface的分配、層級順序等。
· 第三層為SurfaceFlinger,負責將多個Surface混合並輸出。
經過這個例子的介紹,相信大家對WMS的功能有了一個初步的瞭解。接下來,我們要進入WMS的內部,通過其啟動過程一窺它的構成。
俗話說,一個好漢三個幫!WMS的強大是由很多重要的成員互相協調工作而實現的。瞭解WMS的構成將會為我們深入探索WMS打下良好的基礎,進而分析它的啟動過程,這是再合適不過了。
1.WMS的誕生
和其他的系統服務一樣,WMS的啟動位於SystemServer.java中ServerThread類的run()函式內。
[-->SystemServer.java::ServerThread.run()]
Public void run() {
......
WindowManagerService wm = null;
......
try {
......
// ①建立WMS例項
/* 通過WindowManagerService的靜態函式main()建立WindowManagerService的例項。
注意main()函式的兩個引數wmHandler和uiHandler。這兩個Handler分別運行於由
ServerThread所建立的兩個名為“WindowManager”和“UI”的兩個HandlerThread中 */
wm =WindowManagerService.main(context, power, display, inputManager,
uiHandler,wmHandler,
factoryTest !=SystemServer.FACTORY_TEST_LOW_LEVEL,
!firstBoot, onlyCore);
// 新增到ServiceManager中去
ServiceManager.addService(Context.WINDOW_SERVICE,wm);
......
catch(RuntimeException e) {
......
}
......
try {
//②初始化顯示資訊
wm.displayReady();
} catch(Throwable e) {......}
......
try {
// ③通知WMS,系統的初始化工作完成
wm.systemReady();
} catch(Throwable e) {......}
......
}
由此可以看出,WMS的建立分為三個階段:
· 建立WMS的例項。
· 初始化顯示資訊。
· 處理systemReady通知。
接下來,將通過以上三個階段分析WMS從無到有的過程。
看一下WMS的main()函式的實現:
[-->WindowManagerService.java::WindowManagerSrevice.main()]
public static WindowManagerService main(finalContext context,
finalPowerManagerService pm, final DisplayManagerService dm,
finalInputManagerService im,
finalHandler uiHandler, final Handler wmHandler,
finalboolean haveInputMethods, final boolean showBootMsgs,
finalboolean onlyCore) {
finalWindowManagerService[] holder = new WindowManagerService[1];
// 通過由SystemServer為WMS建立的Handler新建一個WindowManagerService物件
// 此Handler執行在一個名為WindowManager的HandlerThread中
wmHandler.runWithScissors(newRunnable() {
@Override
publicvoid run() {
holder[0]= new WindowManagerService(context, pm, dm, im,
uiHandler,haveInputMethods, showBootMsgs, onlyCore);
}
}, 0);
returnholder[0];
}
注意Handler類在Android 4.2中新增了一個API:runWithScissors()。這個函式將會在Handler所在的執行緒中執行傳入的Runnable物件,同時阻塞呼叫執行緒的執行,直到Runnable物件的run()函式執行完畢。
WindowManagerService.main()函式在ServerThread專為WMS建立的執行緒“WindowManager”上建立了一個WindowManagerService的新例項。WMS中所有需要的Looper物件,例如Handler、Choreographer等,將會執行在“WindowManager”執行緒中。
接下來看一下其建構函式,看一下WMS定義了哪些重要的元件。
[-->WindowManagerService.java::WindowManagerService.WindowManagerService()]
private WindowManagerService(Context context,PowerManagerService pm,
DisplayManagerService displayManager, InputManagerService inputManager,
Handler uiHandler,
booleanhaveInputMethods, boolean showBootMsgs, boolean onlyCore)
......
mDisplayManager=
(DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
mDisplayManager.registerDisplayListener(this,null);
Display[]displays = mDisplayManager.getDisplays();
/* 初始化DisplayContent列表。DisplayContent是Android4.2為支援多螢幕輸出所引入的一個
概念。一個DisplayContent指代一塊螢幕,螢幕可以是手機自身的螢幕,也可以是基於Wi-FiDisplay
技術的虛擬螢幕[3]*/
for(Display display : displays) {
createDisplayContentLocked(display);
}
.....
/* 儲存InputManagerService。輸入事件最終要分發給具有焦點的視窗,而WMS是視窗管理者,
所以WMS是輸入系統中的重要一環。關於輸入系統的內容將在第5章中深入探討*/
mInputManager= inputManager;
// 這個看起來其貌不揚的mAnimator,事實上具有非常重要的作用。它管理著所有視窗的動畫
mAnimator= new WindowAnimator(this, context, mPolicy);
// 在“UI“執行緒中將對另一個重要成員mPolicy,也就是WindowManagerPolicy進行初始化
initPolicy(uiHandler);
// 將自己加入到Watchdog中
Watchdog.getInstance().addMonitor(this);
......
}
第二步,displayReady()函式的呼叫主要是初始化顯示尺寸的資訊。其內容比較瑣碎,這裡就先不介紹了。不過值得注意的一點是,再displayReady()完成後,WMS會要求ActivityManagerService進行第一次Configuration的更新。
第三步,在systemReady()函式中,WMS本身將不會再做任何操作了,直接呼叫mPolicy的systemReady()函式。
2.WMS的重要成員
總結一下在WMS的啟動過程中所建立的重要成員,參考圖4-3。
圖 4-3 WMS的重要成員
以下是對圖4-3中重要成員的簡單介紹:
· mInputManager,InputManagerService(輸入系統服務)的例項。用於管理每個視窗的輸入事件通道(InputChannel)以及向通道上派發事件。關於輸入系統的詳細內容將在本書第5章詳細探討。
· mChoreographer,Choreographer的例項,在SampleWindow的例子中已經見過了。Choreographer的意思是編舞指導。它擁有從顯示子系統獲取VSYNC同步事件的能力,從而可以在合適的時機通知渲染動作,避免在渲染的過程中因為發生螢幕重繪而導致的畫面撕裂。從這個意義上來講,Choreographer的確是指導Android翩翩起舞的大師。WMS使用Choreographer負責驅動所有的視窗動畫、螢幕旋轉動畫、牆紙動畫的渲染。
· mAnimator,WindowAnimator的例項。它是所有視窗動畫的總管(視窗動畫是一個WindowStateAnimator的物件)。在Choreographer的驅動下,逐個渲染所有的動畫。
· mPolicy,WindowPolicyManager的一個實現。目前它只有PhoneWindowManager一個實現類。mPolicy定義了很多視窗相關的策略,可以說是WMS的首席顧問!每當WMS要做什麼事情的時候,都需要向這個顧問請教應當如何做。例如,告訴WMS某一個型別的Window的ZOrder的值是多少,幫助WMS矯正不合理的視窗屬性,會為WMS監聽螢幕旋轉的狀態,還會預處理一些系統按鍵事件(例如HOME、BACK鍵等的預設行為就是在這裡實現的),等等。所以,mPolicy可謂是WMS中最重要的一個成員了。
· mDisplayContents,一個DisplayContent型別的列表。Android4.2支援基於Wi-fi Display的多螢幕輸出,而一個DisplayContent描述了一塊可以繪製視窗的螢幕。每個DisplayContent都用一個整型變數作為其ID,其中手機預設螢幕的ID由Display.DEFAULT_DISPLAY常量指定。DisplayContent的管理是由DisplayManagerService完成的,在本章不會去探討DisplayContent的實現細節,而是關注DisplayContent對視窗管理與佈局的影響。
下面的幾個成員的初始化並沒有出現在建構函式中,不過它們的重要性一點也不亞於上面幾個。
· mTokenMap,一個HashMap,儲存了所有的顯示令牌(型別為WindowToken),用於視窗管理。在SampleWindow例子中曾經提到過,一個視窗必須隸屬於某一個顯示令牌。在那個例子中所新增的令牌就被放進了這個HashMap中。從這個成員中還衍生出幾個輔助的顯示令牌的子集,例如mAppTokens儲存了所有屬於Activity的顯示令牌(WindowToken的子類AppWindowToken),mExitingTokens則儲存了正在退出過程中的顯示令牌等。其中mAppTokens列表是有序的,它與AMS中的mHistory列表的順序保持一致,反映了系統中Activity的順序。
· mWindowMap,也是一個HashMap,儲存了所有視窗的狀態資訊(型別為WindowState),用於視窗管理。在SampleWindow例子中,使用IWindowSession.add()所新增的視窗的狀態將會被儲存在mWindowMap中。與mTokenMap一樣,mWindowMap一樣有衍生出的子集。例如mPendingRemove儲存了那些退出動畫播放完成並即將被移除的視窗,mLosingFocus則儲存了那些失去了輸入焦點的視窗。在DisplayContent中,也有一個windows列表,這個列表儲存了顯示在此DisplayContent中的視窗,並且它是有序的。視窗在這個列表中的位置決定了其最終顯示時的Z序。
· mSessions,一個List,元素型別為Session。Session其實是SampleWindow例子中的IWindowSession的Bn端。也就是說,mSessions這個列表儲存了當前所有想向WMS尋求視窗管理服務的客戶端。注意Session是程序唯一的。
· mRotation,只是一個int型變數。它儲存了當前手機的旋轉狀態。
WMS定義的成員一定不止這些,但是它們是WMS每一種功能最核心的變數。讀者在這裡可以線對它們有一個感性的認識。在本章後續的內容裡將會詳細分析它們在WMS的各種工作中所發揮的核心作用。
4.1.3 初識WMS的小結
這一節通過SampleWindow的例子向讀者介紹了WMS的客戶端如何使用視窗,然後通過WMS的誕生過程簡單剖析了一下WMS的重要成員組成,以期通過本節的學習能夠為後續的學習打下基礎。
從下一節開始,我們將會深入探討WMS的工作原理。
經過上一節的介紹,讀者應該對WMS的視窗管理有了一個感性的認識。從這一節開將深入WMS的內部去剖析其工作流程。
根據前述內容可知,SampleWindow新增視窗的函式是IWindowSession.add()。IWindowSession是WMS與客戶端互動的一個代理,add則直接呼叫到了WMS的addWindow()函式。我們將從這個函式開始WMS之旅。本小節只討論它的前半部分。
注意由於篇幅所限,本章不準備討論removeWindow的實現。
[-->WindowManagerService.java::WindowManagerService.addWindow()Part1]
public int addWindow(Session session, IWindowclient, int seq,
WindowManager.LayoutParams attrs, int viewVisibility,int displayId
Rect outContentInsets, InputChannel outInputChannel) {
// 首先檢查許可權,沒有許可權的客戶端不能新增視窗
intres = mPolicy.checkAddPermission(attrs);
......
// 當為某個視窗新增子視窗時,attachedWindow將用來儲存父視窗的例項
WindowState attachedWindow = null;
//win就是即將被新增的視窗了
WindowState win = null;
......
finalint type = attrs.type;
synchronized(mWindowMap){
......
//①獲取視窗要新增到的DisplayContent
/* 在新增視窗時,必須通過displayId引數指定新增到哪一個DisplayContent。
SampleWindow例子沒有指定displayId引數,Session會替SampleWindow選擇
DEFAULT_DISPLAY,也就是手機螢幕 */
finalDisplayContent displayContent = getDisplayContentLocked(displayId);
if(displayContent == null) {
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
......
// 如果要新增的視窗是另一個的子視窗,就要求父視窗必須已經存在
// 注意, attrs.type表示了視窗的型別,attrs.token則表示了視窗所隸屬的物件
// 對於子視窗來說,attrs.token表示了父視窗
if(type >= FIRST_SUB_WINDOW &&.type <= LAST_SUB_WINDOW) {
attachedWindow = windowForClientLocked(null, attrs.token, false);
if (attachedWindow == null) {
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
//在這裡還可以看出WMS要求視窗的層級關係最多為兩層
if (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW
&&attachedWindow.mAttrs.type <= LAST_SUB_WINDOW) {
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
booleanaddToken = false;
// ②WindowToken出場!根據客戶端的attrs.token取出已註冊的WindowToken
WindowToken token = mTokenMap.get(attrs.token);
// 下面的if語句塊初步揭示了WindowToken和視窗之間的關係
if(token == null) {
// 對於以下幾種型別的視窗,必須通過LayoutParams.token成員為其指定一個已經
// 新增至WMS的WindowToken
if (type >= FIRST_APPLICATION_WINDOW
&& type<= LAST_APPLICATION_WINDOW) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (type == TYPE_INPUT_METHOD) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (type == TYPE_WALLPAPER) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (type == TYPE_DREAM) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
// 其他型別的視窗則不需要事先向WMS新增WindowToken因為WMS會在這裡隱式地創
// 建一個。注意最後一個引數false,這表示此WindowToken由WMS隱式建立。
token = new WindowToken(this, attrs.token, -1, false);
addToken = true;
} else if (type >= FIRST_APPLICATION_WINDOW
&&type <= LAST_APPLICATION_WINDOW) {
// 對於APPLICATION型別的視窗,要求對應的WindowToken的型別也為APPLICATION
// 並且是WindowToken的子類:AppWindowToken
AppWindowToken atoken = token.appWindowToken;
if (atoken == null) {
return WindowManagerImpl.ADD_NOT_APP_TOKEN;
} else if (atoken.removed) {
returnWindowManagerImpl.ADD_APP_EXITING;
}
if (type==TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn){
return WindowManagerImpl.ADD_STARTING_NOT_NEEDED;
}
} else if (type == TYPE_INPUT_METHOD) {
// 對於其他幾種型別的視窗也有類似的要求:視窗型別必須與WindowToken的型別一致
if (token.windowType != TYPE_INPUT_METHOD) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_WALLPAPER) {
if (token.windowType != TYPE_WALLPAPER) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_DREAM) {
if (token.windowType != TYPE_DREAM) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
// ③WMS為要新增的視窗建立了一個WindowState物件
// 這個物件維護了一個視窗的所有狀態資訊
win= new WindowState(this, session, client, token,
attachedWindow,seq, attrs, viewVisibility, displayContent);
......
// WindowManagerPolicy出場了。這個函式的呼叫會調整LayoutParams的一些成員的取值
mPolicy.adjustWindowParamsLw(win.mAttrs);
res= mPolicy.prepareAddWindowLw(win, attrs);
if(res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
// 接下來將剛剛隱式建立的WindowToken新增到mTokenMap中去。通過這行程式碼應該
//讀者應該能想到,所有的WindowToken都被放入這個HashTable中
......
if(addToken) {
mTokenMap.put(attrs.token, token);
}
win.attach();
// 然後將WindowState物件加入到mWindowMap中
mWindowMap.put(client.asBinder(),win);
// 剩下的程式碼稍後再做分析
......
}
}
addWindow()函式的前段程式碼展示了三個重要的概念,分別是WindowToken、WindowState以及DisplayContent。並且在函式開始處對視窗型別的檢查判斷也初步揭示了它們之間的關係:除子視窗外,新增任何一個視窗都必須指明其所屬的WindowToken;視窗在WMS中通過一個WindowState例項進行管理和保管。同時必須在視窗中指明其所屬的DisplayContent,以便確定視窗將被顯示到哪一個螢幕上。
4.2.1 理解WindowToken
1.WindowToken的意義
為了搞清楚WindowToken的作用是什麼,看一下其位於WindowToken.java中的定義。雖然它沒有定義任何函式,但其成員變數的意義卻很重要。
· WindowToken將屬於同一個應用元件的視窗組織在了一起。所謂的應用元件可以是Activity、InputMethod、Wallpaper以及Dream。在WMS對視窗的管理過程中,用WindowToken指代一個應用元件。例如在進行視窗ZOrder排序時,屬於同一個WindowToken的視窗會被安排在一起,而且在其中定義的一些屬性將會影響所有屬於此WindowToken的視窗。這些都表明了屬於同一個WindowToken的視窗之間的緊密聯絡。
· WindowToken具有令牌的作用,是對應用元件的行為進行規範管理的一個手段。WindowToken由應用元件或其管理者負責向WMS宣告並持有。應用元件在需要新的視窗時,必須提供WindowToken以表明自己的身份,並且視窗的型別必須與所持有的WindowToken的型別一致。從上面的程式碼可以看到,在建立系統型別的視窗時不需要提供一個有效的Token,WMS會隱式地為其宣告一個WindowToken,看起來誰都可以添加個系統級的視窗。難道Android為了內部使用方便而置安全於不顧嗎?非也,addWindow()函式一開始的mPolicy.checkAddPermission()的目的就是如此。它要求客戶端必須擁有SYSTEM_ALERT_WINDOW或INTERNAL_SYSTEM_WINDOW許可權才能建立系統型別的視窗。
2.向WMS宣告WindowToken
既然應用元件在建立一個視窗時必須指定一個有效的WindowToken才行,那麼WindowToken究竟該如何宣告呢?
在SampleWindow應用中,使用wms.addWindowToken()函式宣告mToken作為它的令牌,所以在新增視窗時,通過設定lp.token為mToken向WMS進行出示,從而獲得WMS新增視窗的許可。這說明,只要是一個Binder物件(隨便一個),都可以作為Token向WMS進行宣告。對於WMS的客戶端來說,Token僅僅是一個Binder物件而已。
為了驗證這一點,來看一下addWindowToken的程式碼,如下所示:
[-->WindowManagerService.java::WindowManagerService.addWindowToken()]
@Override
publicvoid addWindowToken(IBinder token, int type) {
// 需要宣告Token的呼叫者擁有MANAGE_APP_TOKENS的許可權
if(!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS,
"addWindowToken()")) {
thrownew SecurityException("Requires MANAGE_APP_TOKENS permission");
}
synchronized(mWindowMap){
......
// 注意其建構函式的引數與addWindow()中不同,最後一個引數為true,表明這個Token
// 是顯式申明的
wtoken= new WindowToken(this, token, type, true);
mTokenMap.put(token,wtoken);
......
}
}
使用addWindowToken()函式宣告Token,將會在WMS中建立一個WindowToken例項,並新增到mTokenMap中,鍵值為客戶端用於宣告Token的Binder例項。與addWindow()函式中隱式地建立WindowToken不同,這裡的WindowToken被宣告為顯式的。隱式與顯式的區別在於,當隱式建立的WindowToken的最後一個視窗被移除後,此WindowToken會被一併從mTokenMap中移除。顯式建立的WindowToken只能通過removeWindowToken()顯式地移除。
addWindowToken()這個函式告訴我們,WindowToken其實有兩層含義:
· 對於顯示元件(客戶端)而言的Token,是任意一個Binder的例項,對顯示元件(客戶端)來說僅僅是一個建立視窗的令牌,沒有其他的含義。
· 對於WMS而言的WindowToken這是一個WindowToken類的例項,儲存了對應於客戶端一側的Token(Binder例項),並以這個Token為鍵,儲存於mTokenMap中。客戶端一側的Token是否已被宣告,取決於其對應的WindowToken是否位於mTokenMap中。
注意在一般情況下,稱顯示元件(客戶端)一側Binder的例項為Token,而稱WMS一側的WindowToken物件為WindowToken。但是為了敘述方便,在沒有歧義的前提下不會過分仔細地區分這兩個概念。
接下來,看一下各種顯示元件是如何宣告WindowToken的。
(1) Wallpaper和InputMethod的Token
Wallpaper的Token宣告在WallpaperManagerService中。參考以下程式碼:
[-->WallpaperManagerService.java::WallpaperManagerService.bindWallpaperComponentLocked()]
BooleanbindWallpaperComponentLocked(......) {
......
WallpaperConnection newConn = new WallpaperConnection(wi, wallpaper);
......
mIWindowManager.addWindowToken(newConn.mToken,
WindowManager.LayoutParams.TYPE_WALLPAPER);
......
}
WallpaperManagerService是Wallpaper管理器,它負責維護系統已安裝的所有的Wallpaper並在它們之間進行切換,而這個函式的目的是準備顯示一個Wallpaper。newConn.mToken與SampleWindow例子一樣,是一個簡單的Binder物件。這個Token將在即將顯示出來的Wallpaper被連線時傳遞給它,之後Wallpaper即可通過這個Token向WMS申請建立繪製桌布所需的視窗了。
注意 WallpaperManagerService向WMS宣告的Token型別為TYPE_WALLPAPER,所以,Wallpaper僅能本分地建立TYPE_WALLPAPER型別的視窗。
相應的,WallpaperManagerService會在detachWallpaperLocked()函式中取消對Token的宣告:
[-->WallpaperManagerService.java::WallpaperManagerService.detachWallpaperLocked()]
booleandetachWallpaperLocked(WallpaperData wallpaper){
......
mIWindowManager.removeWindowToken(wallpaper.connection.mToken);
......
}
再此之後,如果這個被detach的Wallpaper想再要建立視窗便不再可能了。
WallpaperManagerService使用WindowToken對一個特定的Wallpaper做出瞭如下限制:
· Wallpaper只能建立TYPE_WALLPAPER型別的視窗。
· Wallpaper顯示的生命週期由WallpaperManagerService牢牢地控制著。僅有當前的Wallpaper才能建立視窗並顯示內容。其他的Wallpaper由於沒有有效的Token,而無法建立視窗。
InputMethod的Token的來源與Wallpaper類似,其宣告位於InputMethodManagerService的startInputInnerLocked()函式中,取消宣告的位置在InputmethodManagerService的unbindCurrentMethodLocked()函式。InputMethodManagerService通過Token限制著每一個InputMethod的視窗型別以及顯示生命週期。
(2) Activity的Token
Activity的Token的使用方式與Wallpaper和InputMethod類似,但是其包含更多的內容。畢竟,對於Activity,無論是其組成還是操作都比Wallpaper以及InputMethod複雜得多。對此,WMS專為Activity實現了一個WindowToken的子類:AppWindowToken。
既然AppWindowToken是為Activity服務的,那麼其宣告自然在ActivityManagerService中。具體位置為ActivityStack.startActivityLocked(),也就是啟動Activity的時候。相關程式碼如下:
[-->ActivityStack.java::ActivityStack.startActivityLocked()]
private final void startActivityLocked(......) {
......
mService.mWindowManager.addAppToken(addPos,r.appToken, r.task.taskId,
r.info.screenOrientation, r.fullscreen);
......
}
startActivityLocked()向WMS宣告r.appToken作為此Activity的Token,這個Token是在ActivityRecord的建構函式中建立的。隨然後在realStartActivityLocked()中將此Token交付給即將啟動的Activity。
[-->ActivityStack.java::ActivityStack.realStartActivityLocked()]
final boolean realStartActivityLocked(......) {
......
app.thread.scheduleLaunchActivity(newIntent(r.intent), r.appToken,
System.identityHashCode(r), r.info,
newConfiguration(mService.mConfiguration),
r.compat, r.icicle, results, newIntents,!andResume,
mService.isNextTransitionForward(),profileFile, profileFd,
profileAutoStop);
......
}
啟動後的Activity即可使用此Token建立型別為TYPE_APPLICATION的視窗了。
取消Token的宣告則位於ActivityStack.removeActivityFromHistoryLocked()函式中。
Activity的Token在客戶端是否和Wallpaper一樣,僅僅是一個基本的Binder例項呢?其實不然。看一下r.appToken的定義可以發現,這個Token的型別是IApplicationToken.Stub。其中定義了一系列和視窗相關的一些通知回撥,它們是:
· windowsDrawn(),當視窗完成初次繪製後通知AMS。
· windowsVisible(),當視窗可見時通知AMS。
· windowsGone(),當視窗不可見時通知AMS。
· keyDispatchingTimeout(),視窗沒能按時完成輸入事件的處理。這個回撥將會導致ANR。
· getKeyDispatchingTimeout(),從AMS處獲取界定ANR的時間。
AMS通過ActivityRecord表示一個Activity。而ActivityRecord的appToken在其建構函式中被建立,所以每個ActivityRecord擁有其各自的appToken。而WMS接受AMS對Token的宣告,併為appToken建立了唯一的一個AppWindowToken。因此,這個型別為IApplicationToken的Binder物件appToken粘結了AMS的ActivityRecord與WMS的AppWindowToken,只要給定一個ActivityRecord,都可以通過appToken在WMS中找到一個對應的AppWindowToken,從而使得AMS擁有了操縱Activity的視窗繪製的能力。例如,當AMS認為一個Activity需要被隱藏時,以Activity對應的ActivityRecord所擁有的appToken作為引數呼叫WMS的setAppVisibility()函式。此函式通過appToken找到其對應的AppWindowToken,然後將屬於這個Token的所有視窗隱藏。
注意每當AMS因為某些原因(如啟動/結束一個Activity,或將Task移到前臺或後臺)而調整ActivityRecord在mHistory中的順序時,都會呼叫WMS相關的介面移動AppWindowToken在mAppTokens中的順序,以保證兩者的順序一致。在後面講解視窗排序規則時會介紹到,AppWindowToken的順序對視窗的順序影響非常大。
4.2.2 理解WindowState
從WindowManagerService.addWindow()函式的實現中可以看出,當向WMS新增一個視窗時,WMS會為其建立一個WindowState。WindowState表示一個視窗的所有屬性,所以它是WMS中事實上的視窗。這些屬性將在後面遇到時再做介紹。
類似於WindowToken,WindowState在顯示元件一側也有個對應的型別:IWindow.Stub。IWindow.Stub提供了很多與視窗管理相關通知的回撥,例如尺寸變化、焦點變化等。
另外,從WindowManagerService.addWindow()函式中看到新的WindowState被儲存到mWindowMap中,鍵值為IWindow的Bp端。mWindowMap是整個系統所有視窗的一個全集。
說明對比一下mTokenMap和mWindowMap。這兩個HashMap維護了WMS中最重要的兩類資料:WindowToken及WindowState。它們的鍵都是IBinder,區別是: mTokenMap的鍵值可能是IAppWindowToken的Bp端(使用addAppToken()進行宣告),或者是其他任意一個Binder的Bp端(使用addWindowToken()進行宣告);而mWindowToken的鍵值一定是IWindow的Bp端。
關於WindowState的更多細節將在後面的講述中進行介紹。不過經過上面的分析,不難得到WindowToken和WindowState之間的關係,參考圖4-4。
圖 4-4 WindowToken與WindowState的關係
更具體一些,以一個正在回放視訊並彈出兩個對話方塊的Activity為例,WindowToken與WindowState的意義如圖4-5所示。
圖 4-5WindowState與WindowToken的從屬關係
4.2.3理解DisplayContent
如果說WindowToken按照視窗之間的邏輯關係將其分組,那麼DisplayContent則根據視窗的顯示位置將其分組。隸屬於同一個DisplayContent的視窗將會被顯示在同一個螢幕中。每一個DisplayContent都對應這一個唯一的ID,在新增視窗時可以通過指定這個ID決定其將被顯示在那個螢幕中。
DisplayContent是一個非常具有隔離性的一個概念。處於不同DisplayContent的兩個視窗在佈局、顯示順序以及動畫處理上不會產生任何耦合。因此,就這幾個方面來說,DisplayContent就像一個孤島,所有這些操作都可以在其內部獨立執行。因此,這些本來屬於整個WMS全域性性的操作,變成了DisplayContent內部的操作了。
4.3 理解視窗的顯示次序
在addWindow()函式的前半部分中,WMS為視窗建立了用於描述視窗狀態的WindowState,接下來便會為新建的視窗確定顯示次序。手機螢幕是以左上角為原點,向右為X軸方向,向下為Y軸方向的一個二維空間。為了方便管理視窗的顯示次序,手機的螢幕被擴充套件為了一個三維的空間,即多定義了一個Z軸,其方向為垂直於螢幕表面指向螢幕外。多個視窗依照其前後順序排布在這個虛擬的Z軸上,因此視窗的顯示次序又被稱為Z序(Z order)。在這一節中將深入探討WMS確定視窗顯示次序的過程以及其影響因素。
4.3.1 主序、子序和視窗型別
看一下WindowState的建構函式:
[-->WindowState.java::WindowState.WindowState()]
WindowState(WindowManagerService service, Sessions, IWindow c, WindowToken token,
WindowState attachedWindow, int seq, WindowManager.LayoutParams a,
intviewVisibility, final DisplayContent displayContent) {
......
// 為子視窗分配ZOrder
if((mAttrs.type >= FIRST_SUB_WINDOW &&
mAttrs.type <= LAST_SUB_WINDOW)) {
// 這裡的mPolicy即是WindowManagerPolicy
mBaseLayer= mPolicy.windowTypeToLayerLw(
attachedWindow.mAttrs.type)
* WindowManagerService.TYPE_LAYER_MULTIPLIER
+ WindowManagerService.TYPE_LAYER_OFFSET;
mSubLayer= mPolicy.subWindowTypeToLayerLw(a.type);
......
} else {// 為普通視窗分配ZOrder
mBaseLayer= mPolicy.windowTypeToLayerLw(a.type)
* WindowManagerService.TYPE_LAYER_MULTIPLIER
+ WindowManagerService.TYPE_LAYER_OFFSET;
mSubLayer= 0;
......
}
......
}
視窗的顯示次序由兩個成員欄位描述:主序mBaseLayer和子序mSubLayer。主序用於描述視窗及其子視窗在所有視窗中的顯示位置。而子序則描述了一個子視窗在其兄弟視窗中的顯示位置。
· 主序越大,則視窗及其子視窗的顯示位置相對於其他視窗的位置越靠前。
· 子序越大,則子視窗相對於其兄弟視窗的位置越靠前。對於父視窗而言,其主序取決於其型別,其子序則保持為0。而子視窗的主序與其父視窗一樣,子序則取決於其型別。從上述程式碼可以看到,主序與子序的分配工作是由WindowManagerPolicy的兩個成員函式windowTypeToLayerLw()和subWindowTypeToLayerLw()完成的。
表4-1與表4-2列出了所有可能的視窗型別以及其主序與子序的值。
表 4-1 視窗的主序
視窗型別 | 主序 | 視窗型別 | 主序 |
TYPE_UNIVERSE_BACKGROUND | 11000 | TYPE_WALLPAPER | 21000 |
TYPE_PHONE | 31000 | TYPE_SEARCH_BAR | 41000 |
TYPE_RECENTS_OVERLAY | 51000 | TYPE_SYSTEM_DIALOG | 51000 |
TYPE_TOAST | 61000 | TYPE_PRIORITY_PHONE | 71000 |
TYPE_DREAM | 81000 | TYPE_SYSTEM_ALERT | 91000 |
TYPE_INPUT_METHOD | 101000 | TYPE_INPUT_METHOD_DIALOG | 111000 |
TYPE_KEYGUARD | 121000 | TYPE_KEYGUARD_DIALOG | 131000 |
TYPE_STATUS_BAR_SUB_PANEL | 141000 | 應用視窗與未知型別的視窗 | 21000 |
表 4-2 視窗的子序
子視窗型別 | 子序 |
TYPE_APPLICATION_PANEL | 1 |
TYPE_APPLICATION_ATTACHED_DIALOG | 1 |
TYPE_APPLICATION_MEDIA | -2 |
TYPE_APPLICATION_MEDIA_OVERLAY | -1 |
TYPE_APPLICATION_SUB_PANEL | 2 |
注意表4-2中的MEDIA和MEDIA_OVERLAY的子序為負值,這表明它們的顯示次序位於其父視窗的後面。這兩個型別的子視窗是SurfaceView控制元件建立的。SurfaceView被例項化後,會向WMS新增一個型別為MEDIA的子視窗,它的父視窗就是承載SurfaceView控制元件的視窗。這個子視窗的Surface將被用於視訊回放、相機預覽或遊戲繪製。為了不讓這個子視窗覆蓋住所有的父視窗中承載的其他控制元件(如拍照按鈕,播放器控制按鈕等),它必須位於父視窗之後。
從表4-1所描述的主序與視窗型別的對應關係中可以看出,WALLPAPER型別的視窗的主序竟和APPLICATION型別的視窗主序相同,這看似有點不合常理,WALLPAPER不是應該顯示在所有Acitivity之下嗎?其實WALLPAPER型別的視窗是一個很不安分的角色,需要在所有的APPLICATION視窗之間跳來跳去。這是因為,有的Activity指定了android:windowShowWallpaper為true,則表示視窗要求將使用者當前桌布作為其背景。對於WMS來說,最簡單的辦法就是將WALLPAPER視窗放置到緊鄰擁有這個式樣的視窗的下方。在這種需求下,為了保證主序決定視窗順序的原則,WALLPAPER使用了與APPLICATION相同的主序。另外,輸入法視窗也是一個很特殊的情況,輸入法視窗會選擇輸入目標視窗,並將自己放置於其上。在本章中不討論這兩個特殊的例子,WALLPAPER的排序規則將在第7章中進行介紹,而輸入法的排序則留給讀者自行研究。
雖然知道了視窗的主序與子序是如何分配的,不過我們仍然存有疑問:如果有兩個相同型別的視窗,那麼它們的主序與子序豈不是完全相同?如何確定它們的顯示順序呢?事實上,表4-1和表4-2中所描述的主序和子序僅僅是排序的依據之一,WMS需要根據當前所有同類型視窗的數量為每個視窗計算最終的現實次序。
4.3.2 通過主序與子序確定視窗的次序
回到WMS的addWindow()函式中,繼續往下看:
[-->WindowManagerService.java::WindowManagerService.addWindow()]
public int addWindow(Session session, IWindowclient, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
RectoutContentInsets, InputChannel outInputChannel) {
......
synchronized(mWindowMap){
//在前面的程式碼中,WMS驗證了新增視窗的令牌的有效性,併為新視窗建立了新的WindowState物件
// 新的WindowState物件在其建構函式中根據視窗型別初始化了其主序mBaseLayer和mSubLayer
......
// 接下來,將新的WindowState按照顯示次序插入到當前DisplayContent的mWindows列表中
// 為了程式碼結構的清晰,不考慮輸入法視窗和桌布視窗的處理