Android Window学习记录(二)Window的创建
有关window和windowmaanger的理解可以参考这篇博客https://blog.csdn.net/qq_53749266/article/details/124332280?spm=1001.2014.3001.5501
一、什么是DecorView?
DecorView是在PhoneWindow中预设好的一个布局,是一个FrameLayout,这个布局长这样: 
他是一个垂直排列的布局,上面是ActionBar,下面是ContentView,他是一个FrameLayout。Activity的布局就加载到ContentView里进行显示。所以Decorview是Activity布局最顶层的viewGroup。内容栏是一定要存在的,并且具体固定的完整id是android.R.id.content。 然后看一下怎么初始化DercorView的:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
protected ViewGroup generateLayout(DecorView decor) {
...
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
}
installDecor方法主要是新建一个DecorView对象,然后加载预设好的布局(Activity布局)对DecorView进行初始化,并获取到这个预设布局的ContentView。
二、Window的创建
WindowManagerImpl是管理PhoneWindow的,有两种创建window的方式:如果已经存在PhoneWindow,直接通过WindowManagerImpl创建window。如果PhoneWindow尚未存在,先创建PhoneWindow,再利用windowManagerImpl来创建window。
我们在Activity中使用getWindowManager方法获取到的就是应用的PhoneWindow对应的WindowManagerImpl。 无论是哪种window,它的添加过程在WMS 处理部分中基本是类似的,只不过会在权限和窗口显示次序等方面会有些不同,但是在 WindowManager处理部分会有所不同。
2.1 Activity的Window创建过程
要分析Activity中的Window的创建过程就必须了解Activity的启动过程,Activity的启动过程最后来到了ActivityThread的handleLaunchActivity
public void handleLaunchActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...;
WindowManagerGlobal.initialize();
final Activity a = performLaunchActivity(r, customIntent);
...
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
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) {
...
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
appContext.setOuterContext(activity);
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);
...
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
...
}
handleLaunchActivity的代码中首先对WindowManagerGlobal进行初始化,然后调用了performLaunchActivity方法。 performLaunchActivity内部通过类加载器创建Activity的实例对象,创建Application对象,然后再调用Activity的attach方法关联运行过程中所依赖的一系列上下文环境变量,把window作为参数传进去,最后回调activity的onCreate方法。所以window会是在Activity的attach方法中创建:
final void attach(...,Context context,Window window, ...) {
...;
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
首先利用传进来的window创建PhoneWindow。Activity实现了window的callBack接口,可以把Activity自己设置为window的观察者。然后再创建WindowManager和PhoneWindow绑定在一起,绑定后我们就可以通过windowManager操作PhoneWindow了。(这里不是setWindowManager吗,windowManager是什么时候创建的?)我们看一下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);
}
首先会获取到应用服务的WindowManager(实现类也是WindowManagerImpl),然后通过这应用服务的WindowManager创建了新的windowManager。所以一个应用所有的WindowManagerImpl都是同个内核windowManager。
这样PhoneWindow和WindowManagerImpl就绑定在一起了。Activity就可以通过WindowManagerImpl来操作PhoneWindow。
创建完成Activity的PhoneWindow和WindowManagerImpl后,接下来看看是怎么把Activity的布局文件设置给PhoneWindow。上面提到调用Activity的attach方法之后,会回调Activity的onCreate方法,在其中会调用setContentView来设置布局,如下:
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
Activity将setContentView具体实现交给了Window处理,这里的getWindow返回我们上面创建的PhoneWindow对象。我们继续看下去:
public void setContentView(int layoutResID) {
if (mContentParent == null) {
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 {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
只看setContentView的重点代码:
- 首先看
decorView创建了没有,没有的话创建DecorView - 把布局加载到
DecorView中 - 回调
Activity的callBack方法
可以看到Activitiy的布局添加到的是DecorView的ContentView中,这也是onCreate中使用的是setContentView而不是setView的原因。
最后会回调Activity的方法告诉Activity,DecorView已经创建并初始化完成了。由于Activity实现了Window的Callback接口,Activity的onContentChanged方法是个空实现,我们可以在子Activity中处理这个回调。
Activity的布局文件已经添加到DecorView里面了,DecorView已经创建完成了,但还缺少了最重要的一步:把DecorView作为window添加到屏幕上。
我们已经知道添加window需要用到WindowManagerImpl的addView方法。虽然说早在Activity的attach方法中Window就已经被创建了,但是这个时候由于DecorView并没有被WindowManager识别,所以这个时候的Window无法提供具体功能,因为它还无法接收外界的输入信息。
这一步是在ActivityThread的handleResumeActivity方法中被执行:
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
这一步方法有两个重点:回调onResume方法,把decorView添加到屏幕上。我们看一下makeVisible方法做了什么:
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
是不是非常熟悉?直接调用WindowManagerImpl的addView方法来吧decorView添加到屏幕上,ecorView真正地完成了添加和显示这两个过程,至此,我们的Activity界面就会显示在屏幕上了。 这部分很长,最后来总结一下:
- 从
Activity的启动流程可以得到Activity创建Window的过程 - 创建
PhoneWindow -> 创建WindowManager -> 创建DecorView -> 利用WindowManager把DecorView显示到屏幕上 - 回调
onResume方法的时候,DecorView还没有被添加到屏幕,所以当onResume被回调,指的是屏幕即将显示,而不是已经显示
2.2 Dialog的Window创建过程
Dialog dialog = new Dialog(context);
TextView textView = new TextView(this);
textView.setText("this is toast! ");
dialog.setContentView(textView);
dialog.show();
Dialog的Window的创建过程和Activity类似,有如下几个步骤。创建PhoneWindow,初始化DecorView,添加DecorView
-
1.**创建Window** Dialog中创建的window就是PhoneWindow,这个过程和Activity的Window的创建过程是一致 Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
...
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);
}
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);
}
普通的Dialog有一个特殊之处,那就是必须采用Activity的Context,如果采用Application的Context会报错,是没有应用token所导致的。应用token一般只有Activity拥有,所以这里只需要用Activity作为Context来显示dialog。 -
2.初始化DecorView并将Dialog的视图添加到DecorView中,这个过程也和Activity的类似,都是通过Window去添加指定的布局文件。 public void setContentView(int layoutResID) {
mWindow.setContentView(layoutResID);
}
-
3.将DecorView添加到Window中并显示在Dialog的show方法中,会通过WindowManager将DecorView添加到Window中,如下所示。 public void show() {
...
onStart();
mDecor = mWindow.getDecorView();
...
WindowManager.LayoutParams l = mWindow.getAttributes();
...
mWindowManager.addView(mDecor, l);
...
mShowing = true;
sendShowMessage();
}
系统Window比较特殊,它可以不需要token,如果context改为this.getApplicationContext(),只需要指定对话框的Window为系统类型就可以正常弹出对话框。 之前讲到,WindowManager.LayoutParams中的type表示Window的类型,而系统Window的层级范围是2000~2999,这些层级范围就对应着type参数,系统Window的层级有很多值,对于本例来说,可以选用TYPE_SYSTEM_OVERLAY来指定对话框的Window类型为系统Window,如下所示。dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)
然后别忘了在AndroidManifest文件中声明权限从而可以使用系统Window,如下所示。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
2.3 Toast的Window创建过程
Toast和Dialog不同。Toast也是基于Window来实现的,但是由于Toast具有定时取消这一功能,所以采用了Handler。 在Toast的内部有两类IPC过程,第一类是Toast访问NotificationManagerService,第二类是Notification-ManagerService回调Toast里的TN接口。
**Toast属于系统Window,它内部的视图由两种方式指定,一种是系统默认的样式**,另一种是通过setView方法来指定一个自定义View,视图都对应Toast的一个View类型的内部成员mNextView。Toast提供了show和cancel分别用于显示和隐藏Toast,show和cancel的内部是IPC过程,

public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
}
}
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
}
}
可以看到,显示和隐藏Toast都需要通过NMS来实现,NMS运行在系统的进程中,所以只能通过远程调用的方式来显示和隐藏Toast。
**TN类是一个Binder类**,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示或隐藏请求时会跨进程回调TN中的方法,由于**TN运行在Binder线程池中,所以需要通过**Handler将其切换到发送Toast请求所在的线程中。
注意,由于这里使用了Handler,所以这意味着Toast无法在没有Looper的线程中弹出,这是因为**Handler需要使用Looper才能完成切换线程的功能**。 首先看Toast的显示过程,它调用了NMS中的enqueueToast方法,如下所示。
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
}
NMS的enqueueToast方法的第一个参数表示当前应用的包名,第二个参数tn表示远程回调,第三个参数表示Toast的时长。
enqueueToast将Toast请求封装为ToastRecord,并将其添加到一个名为mToastQueue的队列。mToastQueue其实是一个ArrayList,对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,以防止DOS(Denial of Service)。如果不这么做,在有通过大量的循环去连续弹出Toast的情况,会导致其他应用没有机会弹出Toast。
if (! isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
}
正常情况下,当ToastRecord被添加到mToastQueue中后,NMS就会通过showNextToastLocked方法来显示当前的Toast。 下面的代码就是showNextToastLocked方法,需要注意的是,Toast的显示是由ToastRecord的callback来完成的,这个callback实际上就是Toast中的**TN对象的远程Binder,通过callback来访问TN中的方法是需要跨进程来完成的,最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池**中。
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record ! = null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.
callback);
try {
record.callback.show();
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.
callback
+ " in package " + record.pkg);
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
Toast显示以后,NMS还会通过scheduleTimeoutLocked方法来发送一个延时消息,具体的延时取决于Toast的时长,如下所示:
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
在上面的代码中,LONG_DELAY是3.5s,而SHORT_DELAY是2s。延迟相应的时间后,NMS会通过cancelToastLocked方法来隐藏Toast并将其从mToastQueue中移除,这个时候如果mToastQueue中还有其他Toast,那么NMS就继续显示其他Toast。 Toast的隐藏也是通过ToastRecord的callback来完成的,这同样也是一次IPC过程,它的工作方式和Toast的显示过程是类似的,如下所示。
try {
record.callback.hide();
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ " in package " + record.pkg);
}
通过上面的分析,大家知道Toast的显示和隐藏过程实际上是通过Toast中的TN这个类来实现的,对应两个方法show和hide。这两个方法都是被NMS以跨进程的方式调用的,因此这两个方法运行在Binder线程池中。为了将执行环境切换到Toast请求所在的线程,在它们的内部使用了Handler,如下所示。
/**
* schedule handleShow into the right thread
*/
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
上述代码中,mShow和mHide是两个Runnable,它们内部分别调用了handleShow和handleHide方法。由此可见,handleShow和handleHide才是真正完成显示和隐藏Toast的地方。TN的handleShow中会将Toast的视图添加到Window中,如下所示。
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams)
而NT的handleHide中会将Toast的视图从Window中移除,如下所示。
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
**if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}**
mView = null;
}
}
到这里Toast的Window的创建过程已经分析完了,到这里对Toast的工作过程就有了一个更加全面的理解了。除了上面已经提到的Activity、Dialog和Toast以外,PopupWindow、菜单以及状态栏等都是通过Window来实现的,这里就不一一介绍了。本章的意义在于让读者对Window有一个更加清晰的认识,同时能够深刻理解Window和View的依赖关系,这有助于理解其他更深层次的概念,比如SurfaceFlinger。任何View都是附属在一个Window上面的,那么这里问一个问题:一个应用中到底有多少个Window呢?
2.4 PopupWinodw的window创建过程
popupWindow也是利用windowManager来往屏幕上添加window。popupWindow是依附于activity而存在的,当Activity未运行时,是**无法弹出popupWindow**的。
弹出popupWindow的过程分为两步:创建view;通过windowManager添加window。 首先看到PopupWindow的构造方法:
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);
}
他有多个重载方法,但最终都会调用到这个有四个参数的方法。主要是前面的得到context和根据context获得WindowManager。
然后我们看到他的显示方法。显示方法有两个:showAtLocation和showAsDropDown。主要是处理显示的位置不同,其他都是相似的。
public void showAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
showAtLocation逻辑很简单,父view的根布局存储了起来,然后调用另外的重载方法:
public void showAtLocation(IBinder token, int gravity, int x, int y) {
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
detachFromAnchor();
mIsShowing = true;
mIsDropdown = false;
mGravity = gravity;
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
preparePopup(p);
p.x = x;
p.y = y;
invokePopup(p);
}
这个方法的逻辑主要有:
- 判断
contentView是否为空或者是否进行显示 - 做一些准备工作
- 进行
popupWindow显示工作
这里我们看一下他的准备工作和显示工作做了什么:
private void preparePopup(WindowManager.LayoutParams p) {
...
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
mDecorView = createDecorView(mBackgroundView);
mDecorView.setIsRootNamespace(true);
...
}
private void invokePopup(WindowManager.LayoutParams p) {
...
mWindowManager.addView(decorView, p);
...
}
到这里popupWindow就会被添加到屏幕上了。
最后总结一下:
根据参数构建popupDecorView把popupDecorView添加到屏幕上
参考资料
Android开发艺术探索
https://blog.csdn.net/weixin_43766753/article/details/108350589
|