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) {
// 这里创建了DecorView
mDecor = generateDecor(-1);
...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 对DecorView进行初始化,得到ContentView
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进行初始化
WindowManagerGlobal.initialize();
// 启动Activity并回调activity的onCreate方法
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
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);
// 这里将window作为参数传到activity的attach方法中
// 一般情况下这里window==null
//**关联**运行过程中所依赖的一系列上下文环境变量
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);
...
// 最后这里回调Activity的onCreate方法
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, ...) {
...;
// 这里新建PhoneWindow对象,并对window进行初始化
//Activity的Window是通过**PolicyManager的一个工厂方法**来创建
mWindow = new PhoneWindow(this, window, activityConfigCallback);
// Activity实现window的callBack接口,把自己设置给window
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
// 这里初始化window的WindowManager对象
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) {
//获取到应用服务的WindowManager
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
// 这里创建了windowManager
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) {
// 创建DecorView
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 {
// **这里根据布局id加载布局,把Activity的布局加载到DecorView的**mContentParent**中**
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 回调activity的方法,**通知Activity视图已经发生改变**
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) {
// 调用Activity的onResume方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
// 让decorView显示到屏幕上
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);//context要activity
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) { ... // 获取windowManager mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); //mWindowManager其实是Activity的WindowManager,这里的context一般是activity // 构造PhoneWindow final Window w = new PhoneWindow(mContext); mWindow = w; // 初始化PhoneWindow 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()"); } // 获取的是activity的windowManager 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方法,获取前面初始化好的decorview onStart(); mDecor = mWindow.getDecorView(); ... WindowManager.LayoutParams l = mWindow.getAttributes(); ... // 利用windowManager来添加window mWindowManager.addView(mDecor, l); //这里的mWindowManager是Activity的WindowManager ... 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) {
// Empty
}
}
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
可以看到,显示和隐藏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) {
// Empty
}
NMS
的enqueueToast
方法的第一个参数表示当前应用的包名,第二个参数tn
表示远程回调,第三个参数表示Toast
的时长。
enqueueToast
将Toast
请求封装为ToastRecord
,并将其添加到一个名为mToastQueue
的队列。mToastQueue
其实是一个ArrayList
,对于非系统应用来说,mToastQueue
中最多能同时存在50个ToastRecord
,以防止DOS(Denial of Service)
。如果不这么做,在有通过大量的循环去连续弹出Toast
的情况,会导致其他应用没有机会弹出Toast。
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
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);
// remove it from the list and let the process die
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);
// don't worry about this, we're about to remove it from
// the list anyway
}
通过上面的分析,大家知道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) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
**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) {
// 如果contentView是空直接返回
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
detachFromAnchor();
mIsShowing = true;
mIsDropdown = false;
mGravity = gravity;
// 得到WindowManager.LayoutParams对象
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
// 做一些准备工作
preparePopup(p);
p.x = x;
p.y = y;
// 执行popupWindow显示工作
invokePopup(p);
}
这个方法的逻辑主要有:
- 判断
contentView
是否为空或者是否进行显示 - 做一些准备工作
- 进行
popupWindow
显示工作
这里我们看一下他的准备工作和显示工作做了什么:
private void preparePopup(WindowManager.LayoutParams p) {
...
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
// 创建了DecorView
// 注意,这里的DecorView并不是我们之前讲的DecorView,而是他的内部类:PopupDecorView
mDecorView = createDecorView(mBackgroundView);
mDecorView.setIsRootNamespace(true);
...
}
private void invokePopup(WindowManager.LayoutParams p) {
...
// 调用windowManager添加window
mWindowManager.addView(decorView, p);
...
}
到这里popupWindow
就会被添加到屏幕上了。
最后总结一下:
根据参数构建
popupDecorView
把popupDecorView
添加到屏幕上
参考资料
Android开发艺术探索
https://blog.csdn.net/weixin_43766753/article/details/108350589