Android 窗口机制 SDK31源码分析 总目录
前面几个章节已经搞清楚Activity的窗口加载过程,那么其他窗口呢?今天我们分析一下Dialog,搞清楚以下几个问题:
- token是什么?
- Dialog为什么一定需要Activity作为Context
- Dialog弹出后对于Activity生命周期有何影响
- 如何正确的设置到Dialog的宽高
前言
首先大家需要了解一下LayoutParams ,当然属性很多,简单了解即可:
...
public int type;
public static final int FIRST_APPLICATION_WINDOW = 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 LAST_APPLICATION_WINDOW = 99;
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 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;
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;
public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+16;
public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
public static final int TYPE_KEYGUARD_SCRIM = FIRST_SYSTEM_WINDOW+29;
public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
public static final int LAST_SYSTEM_WINDOW = 2999;
......
}
这里需要我们知道的是WindowManager.LayoutParams 有三种窗口type,分别对应为:
- 应用窗口程序:type值在
FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW ,必须将token 设置为Activity的token 。比如Dialog。 - 子窗口: type值在
FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW SubWindows ,必须将token 设置为Activity的token 。比如PopupWindow。 - 系统窗口: type值在
FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW ,使用需要权限,属于特定的系统功能。比如Toast。
这里就说到了token 的问题,应用窗口程序和子窗口均需要获取到Activity的token。那么token是什么呢?
token
AMS(ActivityManagerService) 和ActivityThread 之间的通信采用了token来对Activity进行标识,且WMS(WindowManagerService) 会用token进行鉴别,只有符合条件的window 才会被添加。
那么token是怎么保存的呢?
Activity
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,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
...
mWindow = new PhoneWindow(this);
mToken = token;
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
...
}
从前面我知道ActivityThread通过调用performLaunchActivity 来启动Activity,随后调用Activity的attach的方法,会传入token。Activity自己会保存一份,随后传入mWindow.setWindowManager 中。
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
在setWindowManager 方法中,token被赋值到Window的mAppToken 属性上,同时在当前Window上创建了WindowManager。
接下来再看addView ,在前面章节的分析中,我们知道addView 最终在WindowManagerGlobal 中进行了实现,
WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
...
}
由前面的parentWindow 赋值情况我们知道,对于Activity启动流程来说,走到这里,parentWindow 一定是不为null的。
其实:只有系统窗口,parentWindow 才会为null。
window
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
if (wp.token == null) {
View decor = peekDecorView();
if (decor != null) {
wp.token = decor.getWindowToken();
}
}
...
} else {
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
...
}
...
}
这里会判断窗口类型,设置token。获取到Token后就保存在了LayoutParams 里面,之后被传递到ViewRootImpl.setView 中去。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
...
mWindowAttributes.copyFrom(attrs);
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
}
这里将包含token的LayoutParams 通过Session最终调用到了WMS 的addWindow 方法(这些流程前面的章节都提到过,所以这里就简单带过)。
WindowManagerService
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsState requestedVisibility,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
...
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);
if (displayContent == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window to a display that does "
+ "not exist: %d. Aborting.", displayId);
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
if (!displayContent.hasAccess(session.mUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add window to a display for which the application "
+ "does not have access: %d. Aborting.",
displayContent.getDisplayId());
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
if (mWindowMap.containsKey(client.asBinder())) {
ProtoLog.w(WM_ERROR, "Window %s is already added", client);
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
...
activity = token.asActivityRecord();
if (activity == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window with non-application token "
+ ".%s Aborting.", token);
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
} else if (activity.getParent() == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window with exiting application token "
+ ".%s Aborting.", token);
return WindowManagerGlobal.ADD_APP_EXITING;
} else if (type == TYPE_APPLICATION_STARTING) {
if (activity.mStartingWindow != null) {
ProtoLog.w(WM_ERROR, "Attempted to add starting window to "
+ "token with already existing starting window");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
if (activity.mStartingData == null) {
ProtoLog.w(WM_ERROR, "Attempted to add starting window to "
+ "token but already cleaned");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
}
...
}
以上,WMS 会对token进行校验,只有合理的token才允许添加Window ,否则返回相关的错误指标(判断的代码过于长,只是列出了一部分)。
那么这些返回值如何进行处理呢?别慌,看下面的代码。
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
case WindowManagerGlobal.ADD_INVALID_USER:
throw new WindowManager.BadTokenException("Unable to add Window "
+ mWindow + " -- requested userId is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
而ViewRootImpl中也对返回的值进行了判断,如果token不合理,则直接抛出了相关的异常!!!
所以,简单总结一下,每个Activity都有一个自己的token,用于各种校验,而对于WMS 来说,如果想添加非系统级别的窗口,都需要一个合理的token。
好了,有了上面的了解,我们分析一下其他的窗口,比如Dialog。
Dialog
先简单使用一下:
val dialog = object : AppCompatDialog(this) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(TextView(this@LearnTestActivity).apply {
text = "dialog simple test"
})
}
}
dialog.show()
val phoneWindow = dialog.window
if (phoneWindow != null) {
val wlp = phoneWindow.attributes
wlp.gravity = Gravity.CENTER
wlp.height = (obtainPhoneCurrentHeight(this@LearnTestActivity) * 0.5).toInt()
wlp.width = (obtainPhoneCurrentWidth(this@LearnTestActivity) * 0.5).toInt()
phoneWindow.attributes = wlp
}
Dialog使用起来很简单的,只需要创建dialog对象重写onCreate 方法,设置自己的布局,然后使用的时候,调用show方法即可。但是构建Dialog对象的时候,传递给构造器的context必须为Activity。为什么呢?就和我们上面聊到的token有关系了,原因下面分析:
好,那么我们就从Dialog的构造函数看起来。PS:上面我使用的AppCompatDialog 最终也是调用到了Dialog 的构造器,所以直接看Dialog 即可。
Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == Resources.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
Activity
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
if (WINDOW_SERVICE.equals(name)) {
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
整体流程和Activity的创建很相似,首先创建相关的主题资源,其次获取到Activity的WindowManager,之后创建自己的PhoneWindow,设置相关的监听回调等等。最后调用PhoneWindow的setWindowManager 方法。但是注意,这是我们调用setWindowManager 时,传入的token为null,也就是说,此时PhoneWindow内部的mAppToken 属性为null。
我们接下来看Dialog的show 方法。
public void show() {
...
if (!mCreated) {
dispatchOnCreate(null);
} else {
final Configuration config = mContext.getResources().getConfiguration();
mWindow.getDecorView().dispatchConfigurationChanged(config);
}
onStart();
mDecor = mWindow.getDecorView();
...
WindowManager.LayoutParams l = mWindow.getAttributes();
...
mWindowManager.addView(mDecor, l);
...
mShowing = true;
sendShowMessage();
}
这里会调用到onCreate方法,构建布局。之后使用Activity的WindowManager调用了addView 方法。之后的流程和Activity的添加是一致的**。因为这里拿到的是Activity的WindowManager,且WindowManager的构建类型为应用程序窗口,所以在WindowManagerGlobel 的addView 中,会获取到parentWindow 的token,即Activity的token。所以Dialog才可以被添加。如果此时传递的是Appliocation 或者是Service ,则在ViewRootImpl.setView 中会抛出token相关的错误异常,找不到对应的token**,所以这就是为什么使用Dialog我们必须传入Activity的原因。
Dialog流程,上面已经分析完毕了。
接下来回答最初的几个问题:
-
token是什么? 简单理解,Activity的标识符。 -
Dialog为什么一定需要Activity作为Context 因为需要依赖Activity的token进行构建。 -
Dialog弹出后对于Activity生命周期有何影响 分两种情况:
- 如果弹出的是当前界面的Dialog,则当前Activity不会有任何的变化。
- 如果是别的Activity的Dialog,则当前Activity会走
onPause 。 -
如何正确的设置到Dialog的宽高 这个像上面写的demo一样,需要等到调用dialog.show() 方法之后才可以去设置WindowManager.LayoutParams 的宽高,因为只有调用show() 方法之后,decorview 才不会为null,只有它不为null,才会进行真正的相关属性设置,具体实现代码如下所示: Dialog
public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
if (mDecor != null) {
mWindowManager.updateViewLayout(mDecor, params);
}
}
创作不易,如有帮助一键三连咯🙆?♀?。欢迎技术探讨噢!
|