點擊上方藍字關注我,天天一見,給你力量java
前言
關於Window,你瞭解多少呢?看看下面這些問題你都能答上來嗎。android
若是你遇到這些問題
-
Window是什麼?和View的關係? -
WindowManager是什麼?和WMS的關係? -
怎麼添加一個Window? -
Window怎樣能夠顯示到鎖屏界面 -
Window三種類型都存在的狀況下,顯示層級是怎樣。 -
Window就是指PhoneWindow嗎? -
PhoneWindow何時被建立的? -
要實現能夠拖動的View該怎麼作? -
Window的添加、刪除和更新過程。 -
Activity、PhoneWindow、DecorView、ViewRootImpl 的關係? -
Window中的token是什麼,有什麼用? -
Application中能夠直接彈出Dialog嗎? -
關於事件分發,事件究竟是先到DecorView仍是先到Window的?
Window是什麼
窗口。你能夠理解爲手機上的整個畫面,全部的視圖都是經過Window呈現的,好比Activity、dialog
都是附加在Window上的。Window類的惟一實現是PhoneWindow
,這個名字就更加好記了吧,手機窗口唄。web
那Window
到底在哪裏呢?咱們看到的View是Window嗎?是也不是。面試
-
若是說的只是
Window概念
的話,那能夠說是的,View就是Window的存在形式,Window管理着View。微信 -
若是說是
Window類
的話,那確實不是View,惟一實現類PhoneWindow管理着當前界面上的View,包括根佈局——DecorView,和其餘子view的添加刪除等等。架構
不知道你暈沒有,我總結下,Window
是個概念性的東西,你看不到他,若是你能感知它的存在,那麼就是經過View,因此View是Window的存在形式,有了View,你才感知到View外層有一個皇帝的新衣
——window。app
WindowManager是什麼?和WMS的關係?
WindowManager
就是用來管理Window的,實現類爲WindowManagerImpl
,實際工做會委託給WindowManagerGlobal
類中完成。異步
而具體的Window操做,WM會經過Binder
告訴WMS,WMS作最後的真正操做Window的工做,會爲這個Window分配Surface,並繪製到屏幕上。編輯器
怎麼添加一個Window?
var windowParams: WindowManager.LayoutParams = WindowManager.LayoutParams()
windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
windowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG
var btn = Button(this)
windowManager.addView(btn, windowParams)
簡單貼了下代碼,加了一個Button。ide
有的朋友可能會疑惑了,這明明是個Button,是個View啊,咋成了Window?
剛纔說過了,View是Window的表現形式,在實際實現中,添加window其實就是添加了一個你看不到的window,而且裏面有View才能讓你感受獲得這個是一個Window。
因此經過windowManager
添加的View其實就是添加Window的過程。
這其中還有兩個比較重要的屬性:flags和type
,下面會依次說到。
Window怎樣能夠顯示到鎖屏界面
Window的flag能夠控制Window
的顯示特性,也就是該怎麼顯示、touch事件處理、與設備的關係、等等。因此這裏問的鎖屏界面顯示也是其中的一種Flag。
// Window不須要獲取焦點,也不接受各類輸入事件。
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
// @deprecated Use {@link android.R.attr#showWhenLocked} or
// {@link android.app.Activity#setShowWhenLocked(boolean)} instead to prevent an
// unintentional double life-cycle event.
// 窗口能夠在鎖屏的 Window 之上顯示
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
Window三種類型都存在的狀況下,顯示層級是怎樣。
Type表示Window的類型,一共三種:
-
應用Window
。對應着一個Activity,Window層級爲1~99,在視圖最下層。 -
子Window
。不能單獨存在,須要附屬在特定的父Window之中(如Dialog就是子Window),Window層級爲1000~1999。 -
系統Window
。須要聲明權限才能建立的Window,好比Toast和系統狀態欄,Window層級爲2000-2999,處在視圖最上層。
能夠看到,區別就是有個Window層級(z-ordered),層級高的能覆蓋住層級低的,離用戶更近。
Window就是指PhoneWindow嗎?
若是有人問我這個問題,我確定內心要大大的疑惑了🤔。
可不就是PhoneWindow
嗎?都惟一實現類了,淨問些奇怪問題。
可是面試的時候遇到這種問題總要答啊?這時候就要扯出Window的概念了。
-
若是指的Window類,那麼PhoneWindow做爲惟一實現類,通常指的就是PhoneWindow。
-
若是指的Window這個概念,那確定不是指
PhoneWindow
,而是存在於界面上真實的View。固然也不是全部的View都是Window,而是經過WindowManager添加到屏幕的view纔是Window,因此PopupWindow
是Window,上述問題中添加的單個View也是Window。
PhoneWindow何時被建立的?
熟悉Activity啓動流程的朋友應該知道,啓動過程會執行到ActivityThread的handleLaunchActivity
方法,這裏初始化了WindowManagerGlobal
,也就是WindowManager實際操做Window的類,待會會看到:
public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
//...
WindowManagerGlobal.initialize();
//...
final Activity a = performLaunchActivity(r, customIntent);
//...
return a;
}
而後會執行到performLaunchActivity中建立Activity,並調用attach方法進行一些數據的初始化(僞代碼):
final void attach() {
//初始化PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
//和WindowManager關聯
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
}
能夠看到,在Activity的attach方法中,建立了PhoneWindow,而且設置了callback,windowManager
。
這裏的callback待會會說到,跟事件分發有關係,能夠說是當前Activity和PhoneWindow創建聯繫。
要實現能夠拖動的View該怎麼作?
仍是接着剛纔的btn例子,若是要修改btn的位置,使用updateViewLayout便可,而後在ontouch方法中傳入移動的座標便可。
btn.setOnTouchListener { v, event ->
val index = event.findPointerIndex(0)
when (event.action) {
ACTION_MOVE -> {
windowParams.x = event.getRawX(index).toInt()
windowParams.y = event.getRawY(index).toInt()
windowManager.updateViewLayout(btn, windowParams)
}
else -> {
}
}
false
}
Window的添加、刪除和更新過程。
Window的操做都是經過WindowManager
來完成的,而WindowManager是一個接口,他的實現類是WindowManagerImpl
,而且所有交給WindowManagerGlobal
來處理。下面具體說下addView,updateViewLayout,和removeView。
1)addView
//WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
ViewRootImpl root;
View panelParentView = null;
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
}
}
}
-
這裏能夠看到,建立了一個ViewRootImpl實例,這樣就說明了每一個Window都對應着一個ViewRootImpl。 -
而後經過add方法修改了 WindowManagerGlobal
中的一些參數,好比mViews—存儲了全部Window所對應的View,mRoots——全部Window所對應的ViewRootImpl,mParams—全部Window對應的佈局參數。 -
最後調用了ViewRootImpl的setView方法,繼續看看。
final IWindowSession mWindowSession;
mWindowSession = WindowManagerGlobal.getWindowSession();
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//
requestLayout();
res = mWindowSession.addToDisplay(mWindow,);
}
setView
方法主要完成了兩件事,一是經過requestLayout方法完成異步刷新界面的請求,進行完整的view繪製流程。其次,會經過IWindowSession進行一次IPC調用,交給到WMS來實現Window的添加。
其中mWindowSession是一個Binder對象,至關於在客戶端的代理類,對應的服務端的實現爲Session,而Session就是運行在SystemServer進程中,具體就是處於WMS服務中,最終就會調用到這個Session的addToDisplay方法,從方法名就能夠猜到這個方法就是具體添加Window到屏幕的邏輯,具體就不分析了,下次說到屏幕繪製的時候再細談。
2)updateViewLayout
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
//...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
這裏更新了WindowManager.LayoutParams
和ViewRootImpl.LayoutParams
,而後在ViewRootImpl內部一樣會從新對View進行繪製,最後經過IPC通訊,調用到WMS的relayoutWindow完成更新。
3)removeView
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (view != null) {
InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class);
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
該方法中,經過view找到mRoots
中的對應索引,而後一樣走到ViewRootImpl
中進行View刪除工做,經過die
方法,最終走到dispatchDetachedFromWindow()
方法中,主要作了如下幾件事:
-
回調onDetachedFromeWindow。 -
垃圾回收相關操做; -
經過Session的remove()在WMS中刪除Window; -
經過Choreographer移除監聽器
Activity、PhoneWindow、DecorView、ViewRootImpl 的關係?
看完上面的流程,咱們再來理理這四個小夥伴之間的關係:
-
PhoneWindow
實際上是 Window 的惟一子類,是Activity
和 View 交互系統的中間層,用來管理View的,而且在Window建立(添加)的時候就新建了ViewRootImpl實例。 -
DecorView
是整個 View 層級的最頂層,ViewRootImpl
是DecorView 的parent,可是他並非一個真正的 View,只是繼承了ViewParent接口,用來掌管View的各類事件,包括requestLayout、invalidate、dispatchInputEvent 等等。
Window中的token是什麼,有什麼用?
token?又是個啥呢?剛纔window操做過程當中也沒出現啊。
token
其實你們應該工做中會發現一點蹤影,好比application的上下文去建立dialog的時候,就會報錯:
unable to add window --token null
因此這個token跟window操做是有關係的,翻到剛纔的addview方法中,還有個細節咱們沒說到,就是adjustLayoutParamsForSubWindow方法。
//Window.java
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
//子Window
if (wp.token == null) {
View decor = peekDecorView();
if (decor != null) {
wp.token = decor.getWindowToken();
}
}
} else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
//系統Window
} else {
//應用Window
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
}
}
上述代碼分別表明了三個Window的類型:
-
子Window
。須要從decorview中拿到token。 -
系統Window
。不須要token。 -
應用Window
。直接拿mAppToken,mAppToken是在setWindowManager方法中傳進來的,也就是新建Window的時候就帶進來了token。
而後在WMS中的addWindow方法會驗證這個token,下次說到WMS的時候再看看。
因此這個token
就是用來驗證是否可以添加Window,能夠理解爲權限驗證,其實也就是爲了防止開發者亂用context建立window。
擁有token
的context(好比Activity)就能夠操做Window。沒有token
的上下文(好比Application)就不容許直接添加Window到屏幕(除了系統Window)。
Application中能夠直接彈出Dialog嗎?
這個問題其實跟上述問題相關:
-
若是直接使用 Application
的上下文是不能建立Window的,而Dialog的Window等級屬於子Window,必須依附與其餘的父Window,因此必須傳入Activity這種有window的上下文。 -
那有沒有其餘辦法能夠在 Application
中彈出dialog呢?有,改爲系統級Window:
//檢查權限
if (!Settings.canDrawOverlays(this)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent, 0)
}
dialog.window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
-
另外還有一種辦法,在Application類中,能夠經過 registerActivityLifecycleCallbacks
監聽Activity生命週期,不過這種辦法也是傳入了Activity的context,只不過在Application類中完成這個工做。
關於事件分發,事件究竟是先到DecorView仍是先到Window的?
通過上述一系列問題,是否是對Window印象又深了點呢?最後再看一個問題,這個是wanandroid論壇上看到的,
這裏的window能夠理解爲PhoneWindow
,其實這道題就是問事件分發在Activity、DecorView、PhoneWindow
中的順序。
當屏幕被觸摸,首先會經過硬件產生觸摸事件傳入內核,而後走到FrameWork層(具體流程感興趣的能夠看看參考連接),最後通過一系列事件處理到達ViewRootImpl的processPointerEvent
方法,接下來就是咱們要分析的內容了:
//ViewRootImpl.java
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
...
//mView分發Touch事件,mView就是DecorView
boolean handled = mView.dispatchPointerEvent(event);
...
}
//DecorView.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//分發Touch事件
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//cb其實就是對應的Activity
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
事件的分發流程就比較清楚了:
ViewRootImpl——>DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup
(這其中就用到了getCallback參數,也就是以前addView中傳入的callback,也就是Activity自己)
可是這個流程確實有些奇怪,爲何繞來繞去的呢,光DecorView就走了兩遍。
參考連接中的說法我仍是比較認同的,主要緣由就是解耦。
-
ViewRootImpl
並不知道有Activity這種東西存在,它只是持有了DecorView。因此先傳給了DecorView,而DecorView知道有AC,因此傳給了AC。 -
Activity
也不知道有DecorView,它只是持有PhoneWindow,因此這麼一段調用鏈就造成了。
參考
《Android開發藝術探索》 《Android進階解密》 https://mp.weixin.qq.com/s/wy9V4wXUoEFZ6ekzuLJySQ https://blog.csdn.net/weixin_43766753/article/details/108350589 https://wanandroid.com/wenda/show/12119
感謝你們的閱讀,有一塊兒學習的小夥伴能夠關注下公衆號—
碼上積木
❤️每日一個知識點,創建完總體系架構。
點在看你最好看
本文分享自微信公衆號 - 碼上積木(Lzjimu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。