在Android系統中,窗口是有分組概念的,例如,Activity中彈出的所有PopupWindow會隨著Activity的隱藏而隱藏,可以說這些都附屬于Actvity的子窗口分組,對于Dialog也同樣如此,只不過Dialog與Activity屬于同一個分組。之間已經簡單介紹了窗口類型劃分:應用窗口、子窗口、系統窗口,Activity與Dialog都屬于應用窗口,而PopupWindow屬于子窗口,Toast、輸入法等屬于系統窗口。只有應用窗口與系統窗口可以作為父窗口,子窗口不能作為子窗口的父窗口,也就說Activity與Dialog或者系統窗口中可以彈出PopupWindow,但是PopupWindow不能在自己內部彈出PopupWindow子窗口。日常開發中,一些常見的問題都同窗口的分組有關系,比如為什么新建Dialog的時候必須要用Activity的Context,而不能用Application的;為什么不能以PopupWindow的View為錨點彈出子PopupWindow?其實這里面就牽扯都Android的窗口組織管理形式,本文主要包含以下幾點內容:
- 窗口的分組管理 :應用窗口組、子窗口組、系統窗口組
- Activity、Dialg應用窗口及PopWindow子窗口的添加原理跟注意事項
- 窗口的Z次序管理:窗口的分配序號、次序調整等
- WMS中窗口次序分配如何影響SurfaceFlinger服務
在WMS窗口添加一文中分析過,窗口的添加是通過WindowManagerGlobal.addView()來完成 函數原型如下
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow)
前三個參數是必不可少的,view、params、display,其中display表示要輸出的顯示設備,先不考慮。view 就是APP要添加到WindowManagerGlobal管理的View,而 params是WindowManager.LayoutParams,主要用來描述窗口屬性,WindowManager.LayoutParams有兩個很重要的參數type與token,
public static class LayoutParams extends ViewGroup.LayoutParams
implements Parcelable {
...
public int type;
...
public IBinder token = null;
}
type用來描述窗口的類型,而token其實是標志窗口的分組,token相同的窗口屬于同一分組,后面會知道這個token其實是WMS在APP端對應的一個WindowToken的鍵值。這里先看一下type參數,之前曾添加過Toast窗口,它的type值是TYPE_TOAST,標識是一個系統提示窗口,下面先簡單看下三種窗口類型的Type對應的值,首先看一下應用窗口
窗口TYPE值 | 窗口類型 |
---|---|
FIRST_APPLICATION_WINDOW = 1 | 開始應用程序窗口 |
TYPE_BASE_APPLICATION=1 | 所有程序窗口的base窗口,其他應用程序窗口都顯示在它上面 |
TYPE_APPLICATION =2 | 普通應用程序窗口,token必須設置為Activity的token |
TYPE_APPLICATION_STARTING =3 | 應用程序啟動時所顯示的窗口 |
LAST_APPLICATION_WINDOW = 99 | 結束應用程序窗口 |
一般Activity都是TYPE_BASE_APPLICATION類型的,而TYPE_APPLICATION主要是用于Dialog,再看下子窗口類型
窗口TYPE值 | 窗口類型 |
---|---|
FIRST_SUB_WINDOW = 1000 | SubWindows子窗口,子窗口的Z序和坐標空間都依賴于他們的宿主窗口 |
TYPE_APPLICATION_PANEL =1000 | 面板窗口,顯示于宿主窗口的上層 |
TYPE_APPLICATION_MEDIA =1001 | 媒體窗口(例如視頻),顯示于宿主窗口下層 |
TYPE_APPLICATION_SUB_PANEL =1002 | 應用程序窗口的子面板,顯示于所有面板窗口的上層 |
TYPE_APPLICATION_ATTACHED_DIALOG = 1003 | 對話框,類似于面板窗口,繪制類似于頂層窗口,而不是宿主的子窗口 |
TYPE_APPLICATION_MEDIA_OVERLAY =1004 | 媒體信息,顯示在媒體層和程序窗口之間,需要實現半透明效果 |
LAST_SUB_WINDOW=1999 | 結束子窗口 |
最后看幾個系統窗口類型,
窗口TYPE值 | 窗口類型 |
---|---|
FIRST_SYSTEM_WINDOW = 2000 | 系統窗口 |
TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW | 狀態欄 |
TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3 | 系統提示,出現在應用程序窗口之上 |
TYPE_TOAST = FIRST_SYSTEM_WINDOW+5 | 顯示Toast |
了解窗口類型后,我們需要面對的首要問題是:窗口如何根據類型進行分組歸類的?Dialog是如何確定附屬Activity,PopupWindow如何確定附屬父窗口?。
窗口的分組原理
如果用一句話概括窗口分組的話:Android窗口是以token來進行分組的,同一組窗口握著相同的token,什么是token呢?在 Android WMS管理框架中,token一個IBinder對象,IBinder在實體端與代理端會相互轉換,這里只看實體端,它的取值只有兩種:ViewRootImpl中ViewRootImpl.W,或者是ActivityRecord中的IApplicationToken.Stub對象,其中ViewRootImpl.W的實體對象在ViewRootImpl中實例化,而IApplicationToken.Stub在ActivityManagerService端實例化,之后被AMS添加到WMS服務中去,作為Activity應用窗口的鍵值標識。之前說過Activity跟Dialog屬于同一分組,現在就來看一下Activity跟Dialog的token是如何復用的,這里的復用分為APP端及WMS服務端,關于窗口的添加流程之前已經分析過,這里只跟隨窗口token來分析窗口的分組,我們知道在WMS端,WindowState與窗口的一一對應,而WindowToken與窗口分組,這可以從兩者的定義看出如下:
class WindowToken {
final WindowManagerService service;
final IBinder token;
final int windowType;
final boolean explicit;
<!--當前窗口對應appWindowToken,是不是同Activity存在依附關系-->
AppWindowToken appWindowToken;
<!--關鍵點1 當前WindowToken對應的窗口列表-->
final WindowList windows = new WindowList();
...
}
final class WindowState implements WindowManagerPolicy.WindowState {
static final String TAG = "WindowState";
final WindowManagerService mService;
final WindowManagerPolicy mPolicy;
final Context mContext;
final Session mSession;
<!--當前WindowState對應IWindow窗口代理-->
final IWindow mClient;
<!--當前WindowState對應的父窗口-->
final WindowState mAttachedWindow;
...
<!--當前WindowState隸屬的token-->
WindowToken mToken;
WindowToken mRootToken;
AppWindowToken mAppToken;
AppWindowToken mTargetAppToken;
...
}
可以看到WindowToken包含一個 WindowList windows = new WindowList(),其實就是WindowState列表;而WindowState有一個WindowToken mToken,也就是WindowToken包含一個WindowState列表,而每個WindowState附屬一個WindowToken窗口組,示意圖如下:
Activity對應token及WindowToken(AppWindowToken)的添加
AMS在為Activity創建ActivityRecord的時候,會新建IApplicationToken.Stub appToken對象,在startActivity之前會首先向WMS服務登記當前Activity的Token,隨后,通過Binder通信將IApplicationToken傳遞給APP端,在通知ActivityThread新建Activity對象之后,利用Activity的attach方法添加到Activity中,先看第一步AMS將Activity的token加入到WMS中,并且為Activity創建APPWindowToken。
<!--AMS ActivityStack.java中代碼 -->
final void startActivityLocked(ActivityRecord r, boolean newTask,
boolean doResume, boolean keepCurTransition, Bundle options) {
...<!--關鍵點1 添加Activity token到WMS-->
mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken,XXX);
}
@Override
public void addAppToken(int addPos, IApplicationToken token, int taskId, int stackId,
int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int userId,
int configChanges, boolean voiceInteraction, boolean launchTaskBehind) {
synchronized(mWindowMap) {
<!--新建AppWindowToken-->
AppWindowToken atoken = findAppWindowToken(token.asBinder());
atoken = new AppWindowToken(this, token, voiceInteraction);
...
<!--將AppWindowToken以IApplicationToken.Stub為鍵值放如WMS的mTokenMap中-->
mTokenMap.put(token.asBinder(), atoken);
<!--開始肯定是隱藏狀態,因為還沒有resume-->
atoken.hidden = true;
atoken.hiddenRequested = true;
}
}
也就是說Activity分組的Token其實是早在Activity顯示之前就被AMS添加到WMS中去的,之后AMS才會通知App端去新建Activity,并將Activity的Window添加到WMS中去,接著看下APP端的流程:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
<!--關鍵點1 新建Activity-->
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
<!--關鍵點2 新建appContext-->
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
<!--關鍵點3 attach到WMS-->
activity.attach(appContext, this, getInstrumentation(), r.token,XXX);
...
}
關鍵點1,新建一個Activity,之后會為Activiyt創建一個appContext,這個Context主要是為了activity.attach使用的,其實就是單純new一個ContextImpl,之后Activity會利用attach函數將ContextImpl綁定到自己身上。
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
return new ContextImpl(null, mainThread, packageInfo, null, null, false,
null, overrideConfiguration, displayId);
}
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
<!--關鍵點1 為Activity綁定ContextImpl 因為Activity只是一個ContextWraper-->
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
<!--關鍵點2 new一個PhoneWindow 并設置回調-->
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
<!--關鍵點3 Token的傳遞-->
mToken = token;
mIdent = ident;
mApplication = application;
...
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
<!--將Window的WindowManager賦值給Activity-->
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}
mWindow.setWindowManager并不是直接為Window設置WindowManagerImpl,而是利用當前的WindowManagerImpl重新為Window創建了一個WindowManagerImpl,并將自己設置此WindowManagerImpl的parentWindow:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
之后將Window的WindowManagerImpl傳遞給Activity,作為Activity的WindowManager將來Activity通過getSystemService獲取WindowManager服務的時候,其實是直接返回了Window的WindowManagerImpl,
@Override
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
之后看一下關鍵點3,這里傳遞的token其實就是AMS端傳遞過來的IApplicationToken代理,一個IBinder對象。之后利用ContextImpl的getSystemService()函數得到一個一個WindowManagerImpl對象,再通過setWindowManager為Activity創建自己的WindowManagerImpl。到這一步,Activity已經準備完畢,剩下的就是在resume中通過addView將窗口添加到到WMS,具體實現在ActivityThread的handleResumeActivity函數中:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
<!--關鍵點1-->
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
<!--關鍵點2 獲取WindowManager-->
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
<!--關鍵點3 添加到WMS管理-->
wm.addView(decor, l);
}
...
}
關鍵點1是為了獲取Activit的Window及DecorView對象,如果用戶沒有通過setContentView方式新建DecorView,這里會利用PhoneWindow的getDecorView()新建DecorView,
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
之后通過Activity的getWindowManager()獲取WindowManagerImpl對象,這里獲取的WindowManagerImpl其實是Activity自己的WindowManagerImpl,
private WindowManagerImpl(Display display, Window parentWindow) {
mDisplay = display;
mParentWindow = parentWindow;
}
它的mParentWindow 是非空的,獲取WindowManagerImpl之后,便利用 addView(decor, l)將DecorView對應的窗口添加到WMS中去,最后調用的是
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
可以看到這里會傳遞mParentWindow給WindowManagerGlobal對象,作為調整WindowMangaer.LayoutParams 中token的依據:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
<!--調整wparams的token參數-->
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
ViewRootImpl root;
View panelParentView = null;
..
<!--新建ViewRootImpl ,并利用wparams參數添加窗口-->
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
..
<!--新建ViewRootImpl -->
root.setView(view, wparams, panelParentView);
}
parentWindow.adjustLayoutParamsForSubWindow是一個很關鍵的函數,從名字就能看出,這是為了他調整子窗口的參數:
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
<!--如果是子窗口如何處理-->
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
<!--后面會看到,其實PopupWindow類的子窗口的wp.token是在上層顯示賦值的-->
if (wp.token == null) {
View decor = peekDecorView();
if (decor != null) {
// 這里其實是父窗口的IWindow對象 Window只有Dialog跟Activity才有
wp.token = decor.getWindowToken();
}
}
} else {
<!--這里其實只對應用窗口有用 Activity與Dialog都一樣-->
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
}
}
對于Activity來說,wp.token = mContainer == null ? mAppToken : mContainer.mAppToken,其實就是AMS端傳過來的IApplicationToken,之后在ViewRootImpl中setView的時候,會利用IWindowSession代理與WMS端的Session通信,將窗口以及token信息傳遞到WMS端,其中IApplicationToken就是該Activity所處于的分組,在WMS端,會根據IApplicationToken IBinder鍵值,從全局的mTokenMap中找到對應的AppWindowToken。既然說分組,就應該有其他的子元素,下面看一下Activity上彈出Dialog的流程,進一步了解為什么Activity與它彈出的Dialog是統一分組(復用同一套token)。
Dialg分組及顯示原理:為什么Activity與Dialog算同一組?
在添加到WMS的時候,Dialog的窗口屬性是WindowManager.LayoutParams.TYPE_APPLICATION,同樣屬于應用窗口,因此,必須使用Activity的AppToken才行,換句話說,必須使用Activity內部的WindowManagerImpl進行addView才可以。Dialog和Activity共享同一個WindowManager(也就是WindowManagerImpl),而WindowManagerImpl里面有個Window類型的mParentWindow變量,這個變量在Activity的attach中創建WindowManagerImpl時傳入的為當前Activity的Window,而Activity的Window里面的mAppToken值又為當前Activity的token,所以Activity與Dialog共享了同一個mAppToken值,只是Dialog和Activity的Window對象不同,下面用代碼確認一下:
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
<!--關鍵點 1 根據theme封裝context-->
if (createContextThemeWrapper) {
...
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
<!--獲取mWindowManager-->
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
<!--創建PhoneWindow-->
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
以上代碼先根據Theme調整context,之后利用context.getSystemService(Context.WINDOW_SERVICE),這里Dialog是從Activity彈出來的,所以context是Activity,如果你設置Application,會有如下error,至于為什么,后面分析會看到。
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.view.ViewRootImpl.setView(ViewRootImpl.java:563)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:269)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
接著看Activity的getSystemService,上文分析過這種方法獲取的其實是Activity中PhoneWindow的WindowManagerImpl,所以后面利用WindowManagerImpl addView的時候,走的流程與Activity一樣。看一下show的代碼:
public void show() {
...
onStart();
mDecor = mWindow.getDecorView();
...
<!--關鍵點 WindowManager.LayoutParams的獲取-->
WindowManager.LayoutParams l = mWindow.getAttributes();
...
try {
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
} finally {
}
}
Window在創建的時候,默認新建WindowManager.LayoutParams mWindowAttributes
private final WindowManager.LayoutParams mWindowAttributes =
new WindowManager.LayoutParams();
采用的是無參構造方法,
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
因此這里的type = TYPE_APPLICATION,也就是說Dialog的窗口類型其實是應用窗口。因此在addView走到上文的adjustLayoutParamsForSubWindow的時候,仍然按照Activity的WindowManagerImpl addView的方式處理,并利用Activity的PhoneWindow的 adjustLayoutParamsForSubWindow調整參數,賦值給WindowManager.LayoutParams token的值仍然是Activity的IApplicationToken,同樣在WMS端,對應就是APPWindowToken,也就是Activity與Dialog屬于同一分組。
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
<!--這里其實只對應用窗口有用 Activity與Dialog都一樣-->
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
}
回到之前遺留的一個問題,為什么Dialog用Application作為context不行呢?Dialog的窗口類型屬于應用窗口,如果采用Application作為context,那么通過context.getSystemService(Context.WINDOW_SERVICE)獲取的WindowManagerImpl就不是Activity的WindowManagerImpl,而是Application,它同Activity的WindowManagerImpl的區別是沒有parentWindow,所以adjustLayoutParamsForSubWindow函數不會被調用,WindowManager.LayoutParams的token就不會被賦值,因此ViewRootImpl在通過setView向WMS在添加窗口的時候會失敗:
public int addWindow(Session session, IWindow client, XXX )
...
<!--對于應用窗口 token不可以為null-->
WindowToken token = mTokenMap.get(attrs.token);
if (token == null) {
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
WMS會返回WindowManagerGlobal.ADD_BAD_APP_TOKEN的錯誤給APP端,APP端ViewRootImpl端收到后會拋出如下異常
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
....
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
以上就為什么不能用Application作為Dialog的context的理由(不能為Dialog提供正確的token),接下來看一下PopupWindow是如何處理分組的。
PopupWindow類子窗口的添加流程及WindowToken分組
PopupWindow是最典型的子窗口,必須依附父窗口才能存在,先看下PopupWindow一般用法:
View root = LayoutInflater.from(AppProfile.getAppContext()).inflate(R.layout.pop_window, null);
PopupWindow popupWindow = new PopupWindow(root, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
popupWindow.setBackgroundDrawable(new BitmapDrawable());
popupWindow.showAsDropDown(archorView);
PopupWindow的構造函數很普通,主要是一些默認入場、出廠動畫的設置,如果在新建PopupWindow的時候已經將根View傳遞到構造函數中去,PopupWindow的構造函數會調用setContentView,如果在show之前,沒有調用setContentView,則拋出異常。
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);
}
下面主要看PopupWindow的showAsDropDown函數
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
<!--關鍵點1 利用通過View錨點所在窗口顯性構建PopupWindow的token-->
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
<!--關鍵點2-->
preparePopup(p);
...
<!--關鍵點3-->
invokePopup(p);
}
showAsDropDown有3個關鍵點,關鍵點1是生成WindowManager.LayoutParams參數,WindowManager.LayoutParams參數里面的type、token是非常重要參數,PopupWindow的type是TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW,是一個子窗口。關鍵點2是PopupDecorView的生成,這個View是PopupWindow的根ViewGroup,類似于Activity的DecorView,關鍵3利用WindowManagerService的代理,將View添加到WMS窗口管理中去顯示,先看關鍵點1:
private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
p.gravity = computeGravity();
p.flags = computeFlags(p.flags);
p.type = mWindowLayoutType;
<!--顯性賦值token-->
p.token = token;
p.softInputMode = mSoftInputMode;
p.windowAnimations = computeAnimationResource();
if (mBackground != null) {
p.format = mBackground.getOpacity();
} else {
p.format = PixelFormat.TRANSLUCENT;
}
..
p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
| PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
return p;
}
上面的Token其實用的是anchor.getWindowToken(),如果是Activity中的View,其實用的Token就是Activity的ViewRootImpl中的IWindow對象,如果這個View是一個系統窗口中的View,比如是Toast窗口中彈出來的,用的就是Toast ViewRootImpl的IWindow對象,歸根到底,PopupWindow自窗口中的Token是ViewRootImpl的IWindow對象,同Activity跟Dialog的token(IApplicationToken)不同,該Token標識著PopupWindow在WMS所處的分組,最后來看一下PopupWindow的顯示:
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
final PopupDecorView decorView = mDecorView;
decorView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
<!--關鍵點1-->
mWindowManager.addView(decorView, p);
if (mEnterTransition != null) {
decorView.requestEnterTransition(mEnterTransition);
}
}
主要是調用了WindowManager的addView添加視圖并顯示,這里首先需要關心一下mWindowManager,
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
這的context 可以是Activity,也可以是Application,因此WindowManagerImpl也可能不同,不過這里并沒有多大關系,因為PopupWindow的token是顯性賦值的,就是是就算用Application,也不會有什么問題,對于PopupWindow子窗口,關鍵點是View錨點決定其token,而不是WindowManagerImpl對象:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
之后利用ViewRootImpl的setView函數的時候,WindowManager.LayoutParams里的token其實就是view錨點獲取的IWindow對象,WindowManagerService在處理該請求的時候,
public int addWindow(Session session, IWindow client, XXX ) {
<!--關鍵點1,必須找到子窗口的父窗口,否則添加失敗-->
WindowState attachedWindow = null;
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
attachedWindow = windowForClientLocked(null, attrs.token, false);
if (attachedWindow == null) {
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
<!--關鍵點2 如果Activity第一次添加子窗口 ,子窗口分組對應的WindowToken一定是null-->
boolean addToken = false;
WindowToken token = mTokenMap.get(attrs.token);
AppWindowToken atoken = null;
if (token == null) {
...
token = new WindowToken(this, attrs.token, -1, false);
addToken = true;
}
<!--關鍵點2 新建窗口WindowState對象 注意這里的attachedWindow非空-->
WindowState win = new WindowState(this, session, client, token,
attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
...
<!--關鍵點4 添加更新全部map,-->
if (addToken) {
mTokenMap.put(attrs.token, token);
}
mWindowMap.put(client.asBinder(), win);
}
從上面的分析可以看出,WMS會為PopupWindow窗口創建一個子窗口分組WindowToken,每個子窗口都會有一個指向父窗口的引用,因為是利用父窗口的IWindow作為鍵值,父窗口可以很方便的利用自己的IWindow獲取WindowToken,進而得到全部的子窗口,
關于系統窗口,前文層分析過Toast系統窗口,Toast類系統窗口在WMS端只有一個WindowToken,鍵值是null,這個比較奇葩,不過還沒驗證過。
窗口的Z次序管理:窗口的分配序號、次序調整等
雖然我們看到的手機屏幕只是一個二維平面X*Y,但其實Android系統是有隱形的Z坐標軸的,其方向與手機屏幕垂直,與我們的實現平行,所以并不能感知到。
前面分析了窗口分組的時候涉及了兩個對象WindowState與Windtoken,但僅限分組,分組無法決定窗口的顯示的Z-order,那么再WMS是怎么管理所有窗口的Z-order的? 在WMS中窗口被抽象成WindowState,因此WindowState內部一定有屬性來標志這個窗口的Z-order,實現也確實如此,WindowState采用三個個int值mBaseLayer+ mSubLayer + mLayer 來標志窗口所處的位置,前兩個主要是根據窗口類型確定窗口位置,mLayer才是真正的值,定義如下:
final class WindowState implements WindowManagerPolicy.WindowState {
final WindowList mChildWindows = new WindowList();
final int mBaseLayer;
final int mSubLayer;
<!--最終Z次序的賦值-->
int mLayer;
}
從名字很容知道mBaseLayer是標志窗口的主次序,面向的是一個窗口組,而mSubLayer主要面向單獨窗口,要來標志一個窗口在一組窗口中的位置,對兩者來說值越大,窗口越靠前,從此final屬性知道,兩者的值是不能修改的,而mLayer可以修改,對于系統窗口,一般不會同時顯示兩個,因此,可以用主序決定,比較特殊的就是Activity與子窗口,首先子窗口的主序肯定是父窗口決定的,子窗口只關心次序就行。而父窗口的主序卻相對麻煩,比如對于應用窗口來說,他們的主序都是一樣的,因此還要有一個其他的維度來作為參考,比如對于Activity,主序都是一樣的,怎么定他們真正的Z-order呢?其實Activity的順序是由AMS保證的,這個順序定了,WMS端Activity窗口的順序也是定了,這樣下來次序也方便定了。
WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a,
int viewVisibility, final DisplayContent displayContent) {
...
<!--關鍵點1 子窗口類型的Z order-->
if ((mAttrs.type >= FIRST_SUB_WINDOW &&
mAttrs.type <= LAST_SUB_WINDOW)) {
mBaseLayer = mPolicy.windowTypeToLayerLw(
attachedWindow.mAttrs.type) * WindowManagerService.TYPE_LAYER_MULTIPLIER
+ WindowManagerService.TYPE_LAYER_OFFSET;
mSubLayer = mPolicy.subWindowTypeToLayerLw(a.type);
mAttachedWindow = attachedWindow; final WindowList childWindows = mAttachedWindow.mChildWindows;
final int numChildWindows = childWindows.size();
if (numChildWindows == 0) {
childWindows.add(this);
} else {
...
} else {
<!--關鍵點2 普通窗口類型的Z order-->
mBaseLayer = mPolicy.windowTypeToLayerLw(a.type)
* WindowManagerService.TYPE_LAYER_MULTIPLIER
+ WindowManagerService.TYPE_LAYER_OFFSET;
mSubLayer = 0;
mAttachedWindow = null;
mLayoutAttached = false;
}
...
}
由于窗口所能選擇的類型是確定的,因此mBaseLayer與mSubLayer所能選擇的值只有固定幾個,很明顯這兩個參數不能精確的確定Z-order,還會有其他微調的手段,也僅限微調,在系統層面,決定了不同類型窗口所處的位置,比如系統Toast類型的窗口一定處于所有應用窗口之上,不過我們最關心的是Activity類的窗口如何確定Z-order的,在new WindowState之后,只是粗略的確定了Activity窗口的次序,看一下添加窗口的示意代碼:
addWindow(){
<!--1-->
new WindowState
<!--2-->
addWindowToListInOrderLocked(win, true);
<!--3-->
assignLayersLocked(displayContent.getWindowList());
}
新建state對象之后,Z-order還要通過addWindowToListInOrderLocked及assignLayersLocked才能確定,addWindowToListInOrderLocked主要是根據窗口的Token找到歸屬,插入到對應Token的WindowState列表,如果是子窗口還要插入到父窗口的對應位置中:
插入到特定位置后其實Z-order就確定了,接下來就是通過assignLayersLocked為WindowState分配真正的Z-order mLayer,
private final void assignLayersLocked(WindowList windows) {
int N = windows.size();
int curBaseLayer = 0;
int curLayer = 0;
int i;
boolean anyLayerChanged = false;
for (i=0; i<N; i++) {
final WindowState w = windows.get(i);
final WindowStateAnimator winAnimator = w.mWinAnimator;
boolean layerChanged = false;
int oldLayer = w.mLayer;
if (w.mBaseLayer == curBaseLayer || w.mIsImWindow
|| (i > 0 && w.mIsWallpaper)) {
<!--通過偏移量-->
curLayer += WINDOW_LAYER_MULTIPLIER;
w.mLayer = curLayer;
} else {
curBaseLayer = curLayer = w.mBaseLayer;
w.mLayer = curLayer;
}
if (w.mLayer != oldLayer) {
layerChanged = true;
anyLayerChanged = true;
}
...
}
mLayer最終確定后,窗口的次序也就確定了,這個順序要最終通過后續的relayout更新到SurfaceFlinger服務,之后,SurfaceFlinger在圖層混排的時候才知道如何處理。
WMS中窗口次序分配如何影響SurfaceFlinger服務
SurfaceFlinger在圖層混排的時候應該不會混排所有的窗口,只會混排可見的窗口,比如有多個全屏Activity的時候,SurfaceFlinger只會處理最上面的,那么SurfaceFlinger如何知道哪些窗口可見哪些不可見呢?前文分析了WMS分配Z-order之后,要通過setLayer更新到SurfaceFlinger,接下來看具體流程,創建SurfaceControl之后,會創建一次事務,確定Surface的次序:
SurfaceControl.openTransaction();
try {
mSurfaceX = left;
mSurfaceY = top;
try {
mSurfaceControl.setPosition(left, top);
mSurfaceLayer = mAnimLayer;
final DisplayContent displayContent = w.getDisplayContent();
if (displayContent != null) {
mSurfaceControl.setLayerStack(displayContent.getDisplay().getLayerStack());
}
<!--設置次序-->
mSurfaceControl.setLayer(mAnimLayer);
mSurfaceControl.setAlpha(0);
mSurfaceShown = false;
} catch (RuntimeException e) {
mService.reclaimSomeSurfaceMemoryLocked(this, "create-init", true);
}
mLastHidden = true;
} finally {
SurfaceControl.closeTransaction();
}
}
這里通過openTransaction與closeTransaction保證一次事務的完整性,中間就Surface次序的調整,closeTransaction會與SurfaceFlinger通信,通知SurfaceFlinger更新Surface信息,這其中就包括Z-order。
總結
本文簡要分析了Android窗口的分組,以及WMS窗口次序的確定,最后簡單提及了一下窗口次序如何更新到SurfaceFlinger服務的,也方便將來理解圖層合成。
作者:看書的小蝸牛
原文鏈接:Android窗口管理分析(3):窗口分組及Z-order的確定
僅供參考,歡迎指正