Window的创建
上一篇说到了Window和WindowManager的关系并且讲述了WindowManager如何添加Window与Window内部的三个方法的实现
这篇主要讲几个常见的Window的创建比如Activity,Dialog和Toast
其中Activity属于应用Window
Dialog属于子Window
Toast属于系统Window
z-ordered越来越大,它的优先级就越来越大
Activity的Window的创建
Activity的启动最终是由ActivityThread的performLaunchActivity,这个方法在内部会通过内加载器创建一个Activity的实例变量。
然后调用attach,将一系列上下文环境变量关联起来,确保 Activity 可以正常运行并与应用程序的其他组件进行通信。
我们可以看看attach关联了哪些
我们看看它的源码
通过ctrl+n找
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.activityConfigCallback,
r.assistToken, r.shareableActivityToken);
并且在attach的过程中还会创建Activity所属的Window
记得我们在View的工作原理的时候说到过:
Activity创建完成后,会把DecorView添加到Window中,并创建相应的ViewRootImpl,再把ViewRootImpl与DecorView关联起来
而Activity的Window的创建就是在performLaunchActivity中创建Activity后调用attach来创建的
1.Window的创建
问题:2个Window的不同
在attach中
《艺术开发探索》中说在attach中通过PolicyManager的makeNewWindow获得window,但我找了半天没找到那段源码
只在attach里面找到了
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) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
mMainThread = aThread;
mInstrumentation = instr;
mToken = token;
mAssistToken = assistToken;
mShareableActivityToken = shareableActivityToken;
mIdent = ident;
mApplication = application;
mIntent = intent;
mReferrer = referrer;
mComponent = intent.getComponent();
mActivityInfo = info;
mTitle = title;
mParent = parent;
mEmbeddedID = id;
mLastNonConfigurationInstances = lastNonConfigurationInstances;
if (voiceInteractor != null) {
if (lastNonConfigurationInstances != null) {
mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
} else {
mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
Looper.myLooper());
}
}
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
mWindow.setPreferMinimalPostProcessing(
(info.flags & ActivityInfo.FLAG_PREFER_MINIMAL_POST_PROCESSING) != 0);
getAutofillClientController().onActivityAttached(application);
setContentCaptureOptions(application.getContentCaptureOptions());
}
即它没有用**PolicyManager.makeNewWindow(this)**来创建Window的
而是
mWindow = new PhoneWindow(this, window, activityConfigCallback);
其中这里面的window对象来自
抽象类
public abstract class Window
chatGPT的回答是:
实际的 Window
创建是在 PhoneWindow
中进行的,而不是在 Activity
的 attach()
方法中直接调用 PolicyManager.makeNewWindow(this)
。
在
final void attach(...) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
private void performLaunchActivity(...) {
...
// 创建 Activity 的实例
Activity activity = instantiateActivity(cl, component);
...
// 创建 PhoneWindow 对象
Window window = policyManager.makeNewWindow(activity);
...
// 调用 attach() 方法
activity.attach(..., window, ...);
...
}
总结起来,mWindow = new PhoneWindow(this, window, activityConfigCallback);
创建了一个具体的 PhoneWindow
对象,用于管理 Activity 的窗口。而 Window window = policyManager.makeNewWindow(activity);
创建了一个基本的 Window
对象,作为系统策略管理器与 Activity 的窗口之间的接口。
第一个Window是具体的窗口实现,用于管理 Activity 的窗口。
第二个Window是系统级别的Window对象,并不是直接与 Activity 关联的窗口对象。
并且给出的最终回复,在 Activity 的创建过程中,主要是创建了一个具体的 PhoneWindow
对象,用于表示和管理 Activity 的窗口。系统级别的 Window
对象由策略管理器创建,用于处理窗口的管理和操作。
我们创建完成PhoneWindow后,Activity
实现了window
的callBack
接口,可以把Activity
自己设置为window
的观察者。然后我们就开始初始化WindowManager,记得之前我们说过的Window没办法直接访问,我们只能通过访问WindowManager来访问Window
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
我们继续看看setWindowManager的源码
2.WindowManager的初始化过程
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);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
mAppToken,mAppName, mHardwareAccelerated这三个就是给它们赋了个值m主要还是下面的
先判断传进去的WindowManager为不为空,为空的话进行初始化
,这个初始化与上一篇博客我进行完WindowManager的布局后的初始化一样都是
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
然后将**((WindowManagerImpl)wm).createLocalWindowManager(this)赋给mWindowManager**
话说mWindowManager是什么,我们点击它,会发现它是一个全局变量
private WindowManager mWindowManager;
而((WindowManagerImpl)wm).createLocalWindowManager(this)是在创建一个本地的窗口管理器对象。
本地窗口管理器是一个与设备本地窗口系统交互的对象,它提供了与底层窗口系统通信的功能。通过创建本地窗口管理器,可以在应用程序中实现对窗口的创建、显示、布局、交互等操作。
PhoneWindow类型的mWindow已经通过
mWindow = new PhoneWindow();
创建好了
刚才在setWindowManager中的内部也成功创建好了WindowManager
便可以成功绑定Window和WindowManager
又因为我们之前说过WindowManager主要是由WindowManagerImpl实现的,
所以刚才那句话也就相当于Window和WindowManagerImpl进行成功的绑定
我们就可以通过调用WindowManagerImpl的那三个方法来实现Window的add,remove,update了
3.流程图
4.把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;
}
首先我们先看第一个if
虽然不知道它在干什么但是我们看到了
installDecor();
就可以明白它创建了一个DecorView
首先判断 mContentParent
是否为 null。mContentParent
是 Window 的内容视图的父级容器,如果为 null,表示 DecorView 还未创建,因此需要调用 installDecor()
来创建并安装 DecorView。
如果 mContentParent
不为 null,则进一步判断是否存在 FEATURE_CONTENT_TRANSITIONS 特性。如果不存在该特性,表示不需要进行内容转场动画,那么可以通过 mContentParent.removeAllViews()
清空已有的内容视图。
之后第二个if就是判断是否存在 FEATURE_CONTENT_TRANSITIONS 特性,如果开启了该特性,则会使用场景切换的方式来加载布局
没开启的话会直接使用 mLayoutInflater.inflate(layoutResID, mContentParent)
方法将指定布局文件加载到 DecorView 的 mContentParent
容器中,完成 Activity 的布局加载。
之后就是 回调Activity
的callBack方法
4.1简单流程
总之把Activity的布局文件加载到PhoneWindow就以下几个流程
1.判断DecorView是否创建,没有创建就创建
2.将Activity的布局加载到DecorView的mContentParent中
3.进行Activity的回调
所以我们就可以知道了Activity的加载布局文件为什么是setContentView了,因为Activity的加载布局文件最后加载到了Window的DecorWindow的**ContentView
**中
5.总结前三步
现在回顾以下
我们刚才做的
1是创建了Window,(Window是在ActivityThread中调用performLaunchActivity中创建的Activity调用attach中创建的)
2.如何创建WindowManager,并且进行了绑定(在Window调用setWindowManager中创建的WindowManager)
3.如何将Activity的布局文件设置给PhoneWindow(1.判断DecorView是否创建
2.将布局文件传给DecorView的ContentParent
3.进行Activity的回调,给Activity说DecorView已经创建好了)
现在就差最后一步了,那就是把DecorView
作为window
添加到屏幕上。
肯定想的是那很简单啊,WindowManager已经和Window绑定了,那么我们直接WindowManager.add不就好了
而且在Activity的创建的时候我们说过:
Activity创建完成后,会把DecorView添加到Window中,并创建相应的ViewRootImpl,再把ViewRootImpl与DecorView关联起来
但是
但是这个时候由于DecorView
并没有被WindowManager
识别,所以这个时候的Window
无法提供具体功能,因为它还无法接收外界的输入信息
6.将DecorView添加到Window中
还是在ActivityThread中
我们调用
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();
}
首先第一步是performResumeActivity进行了onResume方法的回调
第二步是**makeVisible()**让decorView显示到屏幕上
我们点击makeVisible()看里面的源码
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
会发现它在这个地方调用了addView,并且将mDecor设置为可见
7.Activity创建的流程图
Dialog的Window创建
只要你把Activity的Window的创建好了,Dialog的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);
}
1.创建Window
这步和Activity的Window创建一样,就没必要讲了
2.初始化DecorView并将Dialog视图添加到DecorView中
public void setContentView(int layoutResID) {
mWindow.setContentView(layoutResID);
}
3.将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();
}
Dialog的Window创建和Activity的Window创建过程有很多类似的地方,二者几乎没有区别
Dialog在被关闭的时候,会通过WindowManager来移除DecorView:
mWindowManager.removeViewImmediate(mDecor);
removeViewImmediate是同步方法
removeView是异步方法
普通的Dialog有一个特殊之处,那就是必须采用Activity的Context,如果采用Application的Context,那么就会报错
是没有应用token导致的,而应用token一般只有Activity拥有,所以只需要用Activity作为Context来显示对话框。
系统Window比较特殊,它可以不要token
我们只需要这么改就可以运行了
dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM.ERROR);
还有
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
Toast的Window创建
Toast比Dialog稍微难一点,虽然Toast也是基于Window来实现的,但是Toast有定时取消这一功能。
所以系统采用了Handler,
Toast内部有2类IPC过程:
1.Toast访问NotificationManagerService
2.NotificationManagerService回调Toast里的TN接口
Toast属于系统Window,它内部的视图由两种方式指定,一种是系统默认的样式,另一种是通过setView方法来指定一个自定义View,视图都对应Toast的一个View类型的内部成员mNextView。Toast提供了show和cancel分别用于显示和隐藏Toast,show和cancel的内部是IPC过程,
Toast的show()
首先看Toast
的显示过程,它调用了NMS
中的enqueueToast
方法,如下所示。
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
}
}
NMS
的enqueueToast
方法的第一个参数表示当前应用的包名,第二个参数tn
表示远程回调,第三个参数表示Toast
的时长。
enqueueToast会先将Toast请求封装成为ToastRecord对象并将它添加到一个名为mToastQueue的ArrayList集合中
对于非系统应用来说,mToastQueue最多同时存在50个ToastRecord,这样做是为了防止DOS,如果不这样做,使用大量循环弹出Toast会导致其他应用没机会弹出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;
}
}
}
}
}
正常情况下,一个app的ToastRecord不会到达上限,当ToastRecord被添加到mToastQueue后,NMS会通过showNextToastLocked方法显示当前的Toast
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的显示是由ToastRecord的callback完成的,这个callback是Toast中的TN对象的远程Binder
通过callback访问TN中的方法需要跨进程完成,最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池
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为2.5s
我最开始以为这个就决定着
Toast.makeText(MainActivity.this,"nihao",Toast.LENGTH_LONG).show()
的这个的持续时间,后来搜了一下发现:
实际上,delay
的值是用于处理 Toast 消息的显示时间间隔,而不是 Toast 消息的总显示时间。在 Android 中,LONG_DELAY
和 SHORT_DELAY
是表示延迟时间的常量,具体的值可能会根据系统的设置而有所不同。
Toast的show()总结
我对show方法的理解就是Toast把它封装到ToastRecord并把它放入ToastQueue集合中
然后NMS会进行回调ToastRecord中的callback即Toast中的TN方法
Toast的hide()
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的显示和隐藏过程实际上是通过Toast的TN这个类实现的,它有一个**show()方法和一个hide()**方法
分别对应Toast的显示和隐藏
由于这两个方法是被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,所以它们两个才是真正完成显示和隐藏的
TN的handleShow会将Toast视图加载到Window中
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams)
而TN的handleHide会将Toast视图从Window中移除