
一、概论
通过上一篇文章(Window & WindowManager理解)中已经知道了View
不能单独存在,必须依附在 Window
上面,因此有视图的地方就有 Window
。这些视图包括 :Activity
,Dialog
,Toast
,PopupWindow
等等。我们通过这篇文章深入理解这几个东西是如何添加window
的
二、Activity 添加 Window 流程分析
2.1 创建对应Window & 回调
在Activity
中的attach()
方法中,系统会创建 Activity
所属的 Window
,并未其设置回调接口。由于 Activity
实现了 Window
的 Callback
接口,因此当 Window
接受到外接的状态改变时就会回调 Activity
中的方法。
##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) {
//创建 PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
//设置 window 的回调
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
这个Callback
中的方法有很多,但是有些我们是非常熟悉的,例如
dispatchTouchEvent(MotionEvent event)
-
onAttachedToWindow()
等等。
2.2 创建Activity
Activity 的创建过程
比较复杂,最终会通过 ActivityThread
中的 performLaunchActivity()
来完成整个启动过程,在这个方法中会通过类加载器创建 Activity 的实例对象
,并调用其 attach 方法
为其关联所需的环境变量(看以下源码):
##ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//.....
if (activity != null) {
appContext.setOuterContext(activity);
// 为activity对象绑定window所需环境变量
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);
//....
}
return activity;
}
2.3 初始化Activity所属DecorView
由于Activity
的视图是通过 setContentView
方法提供的,我们直接看 setContentView
即可:
##Activity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
看代码知道,调用到了getWindow
的setContentView
方法,而在 Android中Window
的实现是 PhoneWindow
。因此我们看到 PhoneWindow
的 setContentView
方法。
##PhoneWindow
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// 1,创建 DecorView
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//2 添加 activity 的布局文件
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 3.通知Activity onContentChanged 。
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
在上面代码中,如果没有 DecorView
就创建它,一般来说它内部包含了标题栏
和内容栏
,但是这个会随着主题的改变而发生改变。但是不管怎么样,内容栏是一定存在的
,并且内容栏有固定的 id content
,完整的 id 是 android.R.id.content
。
- 注释1:通过
generateDecor
创建了DecorView
,接着会调用generateLayout
来加载具体的布局文件到DecorView
中,这个要加载的布局就和系统版本以及定义的主题有关了。加载完之后就会将内容区域的View
返回出来,也就是mContentParent
如下源码:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// DecorView 为null 就创建
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
//.....
}
protected DecorView generateDecor(int featureId) {
//.....
return new DecorView(context, featureId, this, getAttributes());
}
protected ViewGroup generateLayout(DecorView decor) {
//.......
return contentParent;
}
紧接着上面的PhoneWindow
的setContentView
- 注释2:将
activity
需要显示的布局添加到mContentParent
中。 - 注释3:由于
activity
实现了window
的callback
接口,这里表示activity
的布局文件已经被添加到decorView
的mParentView
中了,于是通知Activity
的onContentChanged
接口。
经过上面三个步骤,DecorView
已经初始完成,Activity
的布局文件以及加载到了DecorView
的 mParentView
中了,但是这个时候DecorView
还没有被 WindowManager
正式添加到Window
中。
2.4 添加DecorView到Window
在 ActivityThread
的 handleResumeActivity
中,会调用 activity
的 onResume
方法,接着就会将 DecorView
添加到 Window
中
##ActivityThread
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//.....
//调用 activity 的 onResume 方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//DecorView 完成了添加和显示的过程
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
//..........
}
看到上面的 wm.addView(decor, l);
就到了上一篇文章的windowManager
的 addView
流程了。
三、Dialog 添加 Window 流程分析
3.1 创建Window
Dialog
中创建Window
是在其构造方法中完成,具体如下:
Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
boolean createContextThemeWrapper) {
//...
//获取 WindowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//创建 Window
final Window w = new PhoneWindow(mContext);
mWindow = w;
//设置 Callback
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
3.2 为DecorView添加视图
初始化DecorView
,将 Dialog
的视图添加到 DecorView 中
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
这个和 activity 的类似,都是通过 Window 去添加指定的布局文件
3.3 添加DecorView到Window 显示
public void show() {
//...
mDecor = mWindow.getDecorView();
mWindowManager.addView(mDecor, l);
//发送回调消息
sendShowMessage();
}
private static final class ListenersHandler extends Handler {
private final WeakReference<DialogInterface> mDialog;
public ListenersHandler(Dialog dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISMISS:
((OnDismissListener) msg.obj).onDismiss(mDialog.get());
break;
case CANCEL:
((OnCancelListener) msg.obj).onCancel(mDialog.get());
break;
case SHOW:
((OnShowListener) msg.obj).onShow(mDialog.get());
break;
}
}
}
从上面三个步骤可以发现,Dialog
的 Window
创建和Activity
的Window
创建很类似,二者几乎没有什么区别。
当 dialog
关闭时,它会通过WindowManager
来移除DecorView
, mWindowManager.removeViewImmediate(mDecor)
。具体看下面源码:
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
return;
}
try {
// 通过 windowManager 进行移除
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
普通的Dialog
有一个特殊的地方,就是必须采用Activity 的 Context
,如果采用Application 的 Context
,就会报错:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
错误信息很明确,是没有Token
导致的,而Token
一般只有 Activity
拥有,所以这里只需要用 Activity
作为Context
即可。
另外,系统 Window
比较特殊,他可以不需要Token
,我们可以将Dialog 的 Window Type
修改为系统类型
就可以了,如下所示:
val dialog = Dialog(application)
dialog.setContentView(textView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
}else{
dialog.window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
}
dialog.show()
需要注意的是,弹出系统级别的弹框需要申请 悬浮窗权限。
四、Toast 添加 Window 流程分析
3.1 概论
Toast
也是基于Window
来实现的,但是他的工作过程有些复杂。在Toast
的内部有两类 IPC
的过程,第一类是 Toast
访问NotificationManagerService
过程。第二类是NotificationManagerServer
回调 Toast
里的TN
接口。下面将NotificationManagerService
简称为NMS
。
Toast
属于系统Window
,内部视图有两种定义方式,一种是系统默认的
,另一种是通过 setView 方法来指定一个 View(setView 方法在 android 11 以后已经废弃了,不会再展示自定义视图)
,他们都对应 Toast 的一个内部成员mNextView
。
3.2 展示show()
Toast
提供了 show
和 cancel
分别用于显示和隐藏 Toast
,它们的内部是一个IPC
的过程,实现如下:
public void show() {
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
// ...
}
}
}
//....
}
public void cancel() {
try {
getService().cancelToast(mContext.getOpPackageName(), mToken);
} catch (RemoteException e) {
// Empty
}
//....
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
return sService;
}
从上面代码中可以看出,显示和影藏都需要通过NMS
来实现,由于NMS
运行在系统进程中,所以只通过能跨进程的调用方式来显示和隐藏Toast
。
首先看 Toast
显示的过程,它调用了NMS
中的 enqueueToast
方法,上面的INotificationManager
只是一个AIDL
接口, 这个接口使用来和 NMS
进行通信的,实际调用到的是NMS
的enqueueToast
方法:
##NotificationManagerService
static final int MAX_PACKAGE_TOASTS = 5;
public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
int duration, int displayId) {
enqueueToast(pkg, token, null, callback, duration, displayId, null);
}
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
final long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, token);
//如果队列中有,就更新它,而不是重新排在末尾
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
int count = 0;
final int N = mToastQueue.size();
for (int i = 0; i < N; i++) {
final ToastRecord r = mToastQueue.get(i);
//对于同一个应用,taost 不能超过 5 个
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_TOASTS) {
Slog.e(TAG, "Package has already queued " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
//创建对应的 ToastRecord
record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,text, callback, duration, windowToken, displayId, textCallback);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveForToastIfNeededLocked(callingPid);
}
// ==0 表示只有一个 toast了,直接显示,否则就是还有toast,真在进行显示
if (index == 0) {
showNextToastLocked(false);
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
private ToastRecord getToastRecord(int uid, int pid, String packageName, boolean isSystemToast,
IBinder token, @Nullable CharSequence text, @Nullable ITransientNotification callback,
int duration, Binder windowToken, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
if (callback == null) {
return new TextToastRecord(this, mStatusBar, uid, pid, packageName,
isSystemToast, token, text, duration, windowToken, displayId, textCallback);
} else {
return new CustomToastRecord(this, uid, pid, packageName,
isSystemToast, token, callback, duration, windowToken, displayId);
}
}
我们来看一下NMS
的 enqueueToast
方法,这个方法中已经属于别的进程了。调用的时候传了 五个参数
,
- 第一个表示当前应用的包名
- 第二个
token
- 第三个
Tn
表示远程回调 - 也是一个
IPC
的过程 - 第四个 时长
- 第五个是显示的
id
1) 上面代码中对给定应用的toast
数量进行判断,如果超过50
条,就直接退出,这是为了防止DOS
,如果某个应用一直循环弹出taost
就会导致其他应用无法弹出,这显然是不合理的。
2) 判断完成之后,就会创建ToastRecord
,它分为两种,一种是TextToastRecord
,还有一种是CustomToastRecord
。由于调用enqueueToast
的时候传入了Tn
,所以getToastRecord
返回的是CustomToastRecord
对象。
3) 最后判断只有一个toast
,就调用showNextToastLocked
显示,否则就是还有好多个taost
真在显示。
void showNextToastLocked(boolean lastToastWasTextRecord) {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
//...
if (tryShowToast(
record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
scheduleDurationReachedLocked(record, lastToastWasTextRecord);
mIsCurrentToastShown = true;
if (rateLimitingEnabled && !isPackageInForeground) {
mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
}
return;
}
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
//是否还有剩余的taost需要显示
record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
}
}
private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
boolean isWithinQuota, boolean isPackageInForeground) {
//.....
return record.show();
}
上面代码中最后调用的是record.show()
这个record
也就是 CustomToastRecord
了。
接着我们来看一下他的 show
方法:
##CustomToastRecord
public final ITransientNotification callback;
@Override
public boolean show() {
if (DBG) {
Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
}
try {
callback.show(windowToken);
return true;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "+ pkg);
mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
return false;
}
}
可以看到,调用的是callback
的 show
方法,这个 callback 就是在CustomToastRecord
创建的时候传入的 Tn
了。这里回就调到了 Tn
的show
方法中。
##Toast #Tn
TN(Context context, String packageName, Binder token, List<Callback> callbacks,
@Nullable Looper looper) {
mPresenter = new ToastPresenter(context, accessibilityManager, getService(),packageName);
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
//.....
}
}
};
}
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
//...
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
mHorizontalMargin, mVerticalMargin,
new CallbackBinder(getCallbacks(), mHandler));
}
}
由于 show
方法是被NMS
跨进程的方式调用的,所以他们运行在Binder
线程池中,为了切换到Toast
请求所在的线程,这里使用了Handler
。通过上面代码,我们可以看出,最终是交给ToastPresenter
去处理。
3.3 ToastPresenter最终处理
public class ToastPresenter {
//....
@VisibleForTesting
public static final int TEXT_TOAST_LAYOUT = R.layout.transient_notification;
/**
* Returns the default text toast view for message {@code text}.
*/
public static View getTextToastView(Context context, CharSequence text) {
View view = LayoutInflater.from(context).inflate(TEXT_TOAST_LAYOUT, null);
TextView textView = view.findViewById(com.android.internal.R.id.message);
textView.setText(text);
return view;
}
//....
public ToastPresenter(Context context, IAccessibilityManager accessibilityManager, INotificationManager notificationManager, String packageName) {
mContext = context;
mResources = context.getResources();
//获取 WindowManager
mWindowManager = context.getSystemService(WindowManager.class);
mNotificationManager = notificationManager;
mPackageName = packageName;
mAccessibilityManager = accessibilityManager;
//创建参数
mParams = createLayoutParams();
}
private WindowManager.LayoutParams createLayoutParams() {
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST; //TYPE_TOAST:2005
params.setFitInsetsIgnoringVisibility(true);
params.setTitle(WINDOW_TITLE);
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
setShowForAllUsersIfApplicable(params, mPackageName);
return params;
}
public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity, int xOffset, int yOffset, float horizontalMargin, float verticalMargin, @Nullable ITransientNotificationCallback callback) {
show(view, token, windowToken, duration, gravity, xOffset, yOffset, horizontalMargin,
verticalMargin, callback, false /* removeWindowAnimations */);
}
public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,int xOffset, int yOffset, float horizontalMargin, float verticalMargin,@Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
//.....
addToastView();
trySendAccessibilityEvent(mView, mPackageName);
if (callback != null) {
try {
//回调
callback.onToastShown();
} catch (RemoteException e) {
Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
}
}
}
private void addToastView() {
if (mView.getParent() != null) {
mWindowManager.removeView(mView);
}
try {
// 将 Toast 视图添加到 Window 中
mWindowManager.addView(mView, mParams);
}
}
}
3.3 Toast 总结:
弹出Toast
也是一个IPC
的过程,最终通过Handler
切换到App
对应线程。
使用IPC的原因是:
- 为了统一管理系统中所有 Toast 的消失与显示.真正显示和消失操作还是在 App 中完成的。
其次,Toast
的窗口类型是 TYPE_TOAST
,属于系统类型
,Toast
有自己的 token
,不受 Activity
控制。
Toast
通过 WindowManager
将 view
直接添加到了Window
中,并没有创建PhoneWindow
和 DecorView
,这点和Activity
与 Dialog
不同。
Toast 的添加流程如图:

两篇文章的总结是:
- 每一个
Window
都对应着一个View
和 一个ViewRootImpl
-
Window
表示一个窗口的概念,也是一个抽象的概念,它并不是实际存在的,它是以View
的方式存在的。 -
WindowManager
是我们访问Window
的入口 -
Window
的具体实现位于WindowManagerService
中 -
WindowManager
和WindowManagerService
交互是一个IPC
的过程,最终的IPC
是在RootViewImpl
中完成的。
网友评论