作者:不洗碗工作室 - catango
版權歸作者所有,轉載請註明出處
通常情況下,我們使用的Dialog,Activity等需要顯示到螢幕上面的內容都需要WindowManager來操作的,WindowManager是一個非常重要的子系統.其中有關的類為WindowManager,WindowManagerService,Surface和SurfaceFlinger
什麼是Window
Android系統中的Window是螢幕上的一塊用於繪製各種UI元素並能夠響應應使用者輸入的一個矩形區域和獨自佔有一個Surface例項的顯示區域。 比如Dialog、Activity的介面、桌布、狀態列以及Toast等都是Window.一般來說它是和具體的View繫結在一起的.
Window是一個抽象類,具體實現是PhoneWindow.建立Window只需要WindowManager即可,WindowManager是外界訪問Window的入口,通過WindowManager能夠操縱Window,其具體實現都在WMS中.
層級關係
Window有三種型別,應用類的Window(如Activity),子Window(如Dialog),系統Window(如Toast).Window是分層的,每個Window都是z-ordered,層級大的覆蓋在層級小的上面,這些層級對應在WindowManager.LayoutParams的type引數.而系統級別的Window需要許可權宣告.
- 應用程式視窗
public static final int FIRST_APPLICATION_WINDOW = 1;//1
public static final int TYPE_BASE_APPLICATION = 1;//視窗的基礎值,其他的視窗值要大於這個值
public static final int TYPE_APPLICATION = 2;//普通的應用程式視窗型別
public static final int TYPE_APPLICATION_STARTING = 3;//應用程式啟動視窗型別,用於系統在應用程式視窗啟動前顯示的視窗。
public static final int TYPE_DRAWN_APPLICATION = 4;
public static final int LAST_APPLICATION_WINDOW = 99;//2
複製程式碼
- 子視窗(必須有依賴的父視窗才可以)
public static final int FIRST_SUB_WINDOW = 1000;//子視窗型別初始值
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2;
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3;
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4;
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
public static final int LAST_SUB_WINDOW = 1999;//子視窗型別結束值
複製程式碼
- 系統視窗
public static final int FIRST_SYSTEM_WINDOW = 2000;//系統視窗型別初始值
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;//系統狀態列視窗
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;//搜尋條視窗
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;//通話視窗
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;//系統ALERT視窗
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;//鎖屏視窗
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//TOAST視窗
...
public static final int LAST_SYSTEM_WINDOW = 2999;//系統視窗型別結束值
複製程式碼
Window的屬性
Flags參數列示Window的屬性,通過 WindowManager.LayoutParams的flags來設定
- FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 只要視窗可見,就允許在開啟狀態的螢幕上鎖屏
- FLAG_NOT_FOCUSABLE 視窗不能獲得輸入焦點,設定該標誌的同時,FLAG_NOT_TOUCH_MODAL也會被設定
- FLAG_NOT_\TOUCHABLE 視窗不接收任何觸控事件
- FLAG_NOT_TOUCH_MODAL 在該視窗區域外的觸控事件傳遞給其他的Window,而自己只會處理視窗區域內的觸控事件
- FLAG_KEEP_SCREEN_ON 只要視窗可見,螢幕就會一直亮著
- FLAG_LAYOUT_NO_LIMITS 允許視窗超過螢幕之外
- FLAG_FULLSCREEN 隱藏所有的螢幕裝飾視窗,比如在遊戲、播放器中的全屏顯示
- FLAG_SHOW_WHEN_LOCKED 視窗可以在鎖屏的視窗之上顯示
- FLAG_IGNORE_CHEEK_PRESSES 當使用者的臉貼近螢幕時(比如打電話),不會去響應此事件
- FLAG_TURN_SCREEN_ON 視窗顯示時將螢幕點亮
Token
在原始碼中token一般代表的是Binder物件,作用於IPC程式間資料通訊。並且它也包含著此次通訊所需要的資訊,在ViewRootImpl裡,token用來表示mWindow(W類,即IWindow),並且在WMS中只有符合要求的token才能讓Window正常顯示。 在Window中,token(LayoutParams中的token)分為以下2種情況
- 應用視窗 : token表示的是activity的mToken(ActivityRecord)
- 子視窗 : token表示的是父視窗的W物件,也就是mWindow(IWindow)
無論是應用視窗,還是子視窗,token 不能為空。否則會丟擲異常,並且應用視窗的token 必須是某個 Activity 的 mToken,子視窗的token 必須是父視窗的 IWindow 物件。而且子視窗還需等父視窗新增成功之後才可新增到Window上。
什麼是Surface
基本上我們看到的檢視都是通過Surface來渲染的,Surface是一塊畫布,通過Canvas或者OpenGL在上面作畫.然後通過SufaceFlinger將多塊Surface按照Z-order排序後輸出到FrameBuffer中展示到螢幕上.其中surface使用了雙緩衝技術.Surface是通過WMS分配到Window上面的.
通過原始碼我們可以知道,Surface裡面定義了一個Canvas,可以這麼理解,Canvas就是其緩衝區內容的具現.最終都會儲存到Surface裡的buffer裡,最後由SurfaceFlinger合成並顯示。
什麼是WindowManagerService
在WindowManager呼叫之前,首先是獲取到WindowManagerService.各種系統級服務都會註冊到ContextImpl的一個map中維護,然後通過相應的字串來獲取,比如LayoutInflate是通過Context呼叫getSystemService,傳入LAYOUT _ INFLATER _ SERVICE字元來獲取到的.WindowManager也一樣,通過WINDOW _ SERVICE來獲取到的.
WMS就是對所有Window的Surface進行協調管理的類,WMS為全部Window分配Surface,控制Surface尺寸,大小,動畫等等.我們通過WindowManager來控制View的新增,刪除等操作,其實都是通過WMS處理的.WindowManager通過Binder來進行跨程式通訊,類似ActivityManager和AMS的關係.
什麼是WindowManager
WindowManager繼承自ViewManager,看名字就知道這個類是管理View的,而View又在Window中展示,也可以說WindowManager是管理Window(廢話)的.我們想對Window進行新增刪除等操作都可以使用WindowManager
其中ViewManager介面有三大方法
- addView
- updateViewLayout
- removeView
一般來說,我們常用的方法也就這三個,新增更新和刪除,它的基本流程都是建立一個Window,並且向裡面新增View,如果想刪除一個Window,刪除它對應的View就好.所以說WindowManager操縱Window的過程就是操作對應在Window裡面View的過程.
具體的使用
BB了它的作用之後,其實最重要的還是怎麼去用了.下面簡單介紹下WindowManager的使用
TextView view = new TextView(this);
view.setText("Balabala");
WindowManager.LayoutParams params =
new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, PixelFormat.TRANSPARENT);
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
params.x = 200;
params.y = 300;
getWindowManager().addView(view, params);
複製程式碼
這段程式碼就是將一個TextView新增到Window,螢幕座標是200,300的位置.首先建立一個TextView,然後建立一個LayoutParams設定Window相應的屬性,其中比較重要的是type和flags屬性,參考上面的值按需設定即可.
如果當Window需要變化時,比如使用者拖動懸浮框這種操作,我們只需要監聽觸控事件,然後不停的呼叫updateViewLayout方法.
內部流程
可以看到它內部的幾種方法都是針對View的,說明了View才是Window的主體,實際上我們是無法直接訪問Window的.上面也說過了,Window的操作全都是通過WindowManager來操作的,WindowManager其實也是一個介面,它的具體實現是WindowMangerImpl類.
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
複製程式碼
其中實現了這三個方法的具體實現也不是通過WindowManger的,而是通過mGlobal來實現的,這裡其實用到了傳說中的橋接模式, 那麼Window和View的具體關係我們就可以從WindowManagerGlobal的新增刪除更新的方法來分析了.WindowManagerGlobal本身是一個單例,因此它其實是管理所有Window的Manager.這點也可以從以下幾個比較重要的引數可以看到
private final ArrayList<View> mViews = new ArrayList<View>();//儲存所有Window對應的View
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //所有Window對應的ViewRootImpl
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>(); //儲存所有Window所對應的佈局引數
private final ArraySet<View> mDyingViews = new ArraySet<View>(); //儲存正在被刪除但是操作還未完成的View物件
複製程式碼
下面用新增的例子來分析下Window和View的關係.
View的新增
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//1.檢查引數合法性,如果是子Window還要去調整下佈局引數設定token等操作
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;
//2. parentWindow一般來說都是Activity的Window
if (parentWindow != null) {
//3.調整佈局,adjustLayoutParamsForSubWindow主要是設定token,如果是應用視窗,token可能為null,就會給他賦值mAppToken,這個token是Activity的attach()方法中傳入的token
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
//4.從Views中尋找當前的View的index,如果>0,在將移除的View中停止移除
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
}
//5.層級的判斷,如果這是一個子Window,那麼找到它的父window,將來引用。這裡的param是其父Window呼叫adjustLayoutParamsForSubWindow方法獲取到的,本質作用是為了使用父視窗的token
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
//6,建立ViewRootImpl與view繫結,並且將View和ViewRootImpl新增到列表中()
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
//7,通過setView方法來更新介面並且完成Window新增的邏輯,內部呼叫requestLayout方法完成非同步重新整理請求
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
複製程式碼
關鍵步驟的說明在註釋中,在最後非同步我們可以看到呼叫了ViewRootImpl的setView方法,重點傳入了view和相應的param引數,setView方法的原始碼重要部分如下:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
int res;
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();//View的繪製流程
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
//建立InputChannel
mInputChannel = new InputChannel();
}
try {
//通過WindowSession進行IPC呼叫,將View新增到Window上,其中View資訊包含在mAttachINfo中
//mWindow即W類,用來接收WmS資訊
//同時通過InputChannel接收觸控事件回撥
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
......
//處理觸控事件回撥
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
......
}
複製程式碼
執行requestLayout()方法完成View的繪製流程(下篇文章專門講解),並且通過WindowSession將View和InputChannel新增到WMS中,從而將View新增到Window上並且接收觸控事件。
通過mWindowSession來完成Window的新增過程 ,mWindowSession的型別是IWindowSession,是一個Bindler物件,真正的實現類是Session,也就是Window的新增是一次IPC呼叫. mWindowSession在ViewRootImpl的建構函式中通過WindowManagerGlobal.getWindowSession()方法建立
同時將mWindow(即 W extends IWindow.Stub)傳送給WMS,用來接收WMS資訊。 如此一來,Window的新增請求就交給WMS去處理了,在WMS內部會為每一個應用保留一個單獨的Session。WMS會建立一個WindowState物件用來表示當前新增的視窗。 WMS負責管理這裡些 WindowState 物件。至此,Window的新增過程就結束了。
Token
下來簡單說明下token這個東西,這個不知道不影響理解window的新增...
token的作用上面說過了,在這個流程中token是怎麼體現的呢?在WindowManagerImpl中,有個applyDefaultToken的方法,將params的token設定為預設defaultToken(activity的attach方法中會搞一個這樣的token,本質其實是一個ActivityRecord).在ViewRootImpl的構造方法中會為attachInfo設定token,並且在performTraversals會將自己的mAttachInfo關聯到所有的子view,保證所有的view都能調到token.
如果子視窗沒有父視窗或者父視窗也是子視窗,那麼程式將會崩潰,token一定不能為空,如果我們在onCreate方法中呼叫popupWindow或者dialog的context不是activity的token,那麼就會崩潰的.具體原因可以看看popupWindow的Window建立過程和dialog的window的建立過程