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
|