有时候我们需要在桌面上显示一个类似的悬浮窗的东西,这种效果就需要用Window来实现,Window是一个抽象类,表示一个窗口,它的具体实现类是PhoneWindow,实现位于WindowManagerService中。
一,Window
window 是一个抽象类,具体实现是 PhoneWindow
window 也是一个抽象的概念,每个 window 内对应这一个 DecorView 和一个 ViewRootImpl, window 和 DecorView 通过 ViewRootImpl联系
Window有三种类型,分别是 应用Window,子Window,和系统Window。
应用Window: 应用Window对应一个Activity子Window: 子Window不能单独存在,需要依附在特定的父Window中,比如常见的一些Dialog就是一个子Window。系统Window: 系统Window是需要声明权限才能创建的Window,比如Toast和系统状态栏都是系统Window
Window是分层的,每个Window都有对应的z-ordered,层级大的会覆盖在层级小的Window上面,这和Html中的z-index概念是完全一致的。在三种Window中,应用Window层级是1~99,子Window层级范围是1000~1999,系统Window的层级范围是2000~2999,如下:
| Window | 层级范围 |
|---|---|
| 应用Window | 1~99 |
| 子Window | 1000~1999 |
| 系统Window | 2000~2999 |
这些层级的范围对应着WindowManager.LayoutParams的type参数,如果想要Window位于所有Window的最顶层,那么采用较大的层级即可,很显然系统Window的层级是最大的,当采用系统层级时,需要声明权限。
二,WindowManager
2.1 简介
WindowManager是外界访问Window的入口。
我们对Window的操作是通过WindowManger来完成的,WindowManager是一个接口,它继承自只有三个方法的ViewManager接口,
WindowManager的实现类是WindowManagerImpl
public interface ViewManager{
// 添加View
public void addView(View view, ViewGroup.LayoutParams params);
// 更新 View
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
// 删除 View
public void removeView(View view);
}
这三个方法其实就是 WindowManager 对外提供的主要功能,即添加 View、更新 View 和删除 View。
WindowManager添加Window的例子:
public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Button floatingBtn = new Button(this);
floatingBtn.setText("button");
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
WdinowManager.LayoutParams.WRAP_CONTENT,
WdinowManager.LayoutParams.WRAP_CONTENT,
0,0,
PixelFormat.TRANSPARENT
);
// flag 设置 Window 属性
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
// type 设置 Window 类别(层级)
layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
layoutParams.gravity = Gravity.CENTER;
WindowManager windowManger = getWindowManager();
// 添加视图
windowManger.addView(floatingBtn,layoutParams);
}
}
代码中并没有调用Activity的setContentView 的方法,而是直接通过WindowManager添加Window,其中设置为系统Window,所以应添加权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
结果如下图:
第二个界面是锁屏界面,由于按钮是处于较大层级的系统 Window 中的,所以可以看到 button。
2.2 WindowManager内部机制
WindowManager的继承关系:
在实际使用无法直接访问Window,对Window的访问必须通过WindowManager。WindowManager提供三个接口方法addView ,updateViewLayout,removeView,都是针对View的,
这说明View才是Window存在的实体,上面的例子实现了Window的添加,WindowManager是一个接口,它的真正实现是WindowManagerImpl类:
@Override
public void addView(View view, ViewGroup.LayoutParams params){
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params){
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view){
mGlobal.removeView(view, false);
}
可以看到,WindowManagerImpl 并没有直接实现 Window 的三大操作,而是交给了WindowManagerGlobal 来处理,下面以 addView 为例,分析一下 WindowManagerGlobal中的实现过程:
- 检查参数合法性,如果是子 Window 做适当调整
if(view == null){
throw new IllegalArgumentException("view must not be null");
}
if(display == null){
throw new IllegalArgumentException("display must not be null");
}
if(!(params instanceof WindowManager.LayoutParams)){
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
if(parentWindow != null){
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
- 创建 ViewRootImpl 并将 View 添加到集合中
在 WindowManagerGlobal 内部有如下几个集合比较重要:
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
其中 :
mViews 存储的是所有 Window 所对应的 View,
mRoots 存储的是所有 Window 所对应的 ViewRootImpl,
mParams 存储的是所有 Window 所对应的布局参数,
mDyingViews 存储了那些正在被删除的 View 对象,或者说是那些已经调用了
removeView 方法但是操作删除还未完成的 Window 对象,可以通过表格直观的表示:
| 集合 | 存储内容 |
|---|---|
| mViews | Window 所对应的 View |
| mRoots | Window 所对应的 ViewRootImpl |
| mParams | Window 所对应的布局参数 |
| mDyingViews | 正在被删除的 View 对象 |
addView 操作时会将相关对象添加到对应集合中:
root = new ViewRootImpl(view.getContext(),display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
- 通过 ViewRootImpl 来更新界面并完成 Window 的添加过程
在学习 View 的工作原理时,我们知道 View 的绘制过程是由 ViewRootImpl 来完成的,这里当然也不例外,具体是通过 ViewRootImpl 的 setView 方法来实现的。在 setView 内部会通过 requestLayout 来完成异步刷新请求,如下:
public void requestLayout(){
if(!mHandingLayoutInLayoutRequest){
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
可以看到 scheduleTraversals 方法是 View 绘制的入口,继续查看它的实现:
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(),
mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mInputChannel);
mWindowSession 的类型是 IWindowSession,它是一个 Binder 对象,真正的实现类是 Session,这也就是之前提到的 IPC 调用的位置。在 Session 内部会通过 WindowManagerService 来实现 Window 的添加,代码如下
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams, attrs, int viewVisibility,
int displayId, Rect outContentInsets, InputChannel outInputChannel){
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outInputChannel);
}
终于,Window的添加请求移交给了WindowManagerService手上了,在WindowManagerService内部会为每一个应用保留一个单独的Session,具体Window在WindowMangerService内部是怎么添加的,就不进一步分析了,因为到此为止我们对 Window 的添加这一从应用层到 Framework 的流程已经清楚了,下面通过图示总结一下:
理解了 Window 的添加过程,Window 的删除过程和更新过程都是类似的,也就容易理解了,它们最终都会通过一个 IPC 过程将操作移交给 WindowManagerService 这个位于 Framework 层的窗口管理服务来处理。
三. WindowManagerService
WindowManagerService就是位于FrameWork层的窗口管理服务,它的职责就是管理所有窗口。窗口的本质是什么呢?其实就是一块显示区域,在Android中就是绘制的画布:Surface。当一块Surface显示屏幕上时,就是用户所看到的窗口了。WindowManagerServce添加一个窗口的过程,其实就是WindowManagerService为其分配一块Surface的过程,一块块的Surface在WindowManagerService管理下有序的排列在屏幕上,Android才呈现出多姿多彩的界面。于是根据对Surface的操作类型可以将Android的显示系统分为三个层次:
一般开发中,我们操作的是UI框架层,对Window的操作通过WindowManager即可完成,而WindowManagerService作为系统及服务运行在一个单独的进程,所有WindwowManager和WindowManagerService的交互是一个IPC的过程
四,Window的创建过程
View是Android的视图呈现方式,但是View不能单独存在,它必须附着在Window这个抽象的概念之上,因此有视图的地方就有Window。
哪些地方有Window呢?Android 可以提供视图的地方有 Activity、Dialog、Toast,除此之外,还有一些依托 Window 而实现的视图,比如 PopUpWindow(自定义弹出窗口)、菜单,它们也是视图,有视图的地方就有 Window。
因此 Activity、Dialog、Toast 等视图都对应着一个 Window。这也是面试中常问到的一个知识点:一个应用中有多少个 Window?下面分别分析 Activity、Dialog以及 Toast 的 Window 创建过程。
1. Activity 的 Window 创建过程
Window本质就是一块显示区域,所以关于Activity的Window创建应该发生在Activity的启动过程。Activity的启动过程很复杂,最终会由ActivityThread中的performanLaunchActivity()来完成整个启动过程,在这个方法内部会通过类加载器创建Activity的实例对象,并调用其attach方法为其关联运行过程中所依赖的一系列上下文环境变量。
Activity的Window实例是怎么创建的?
Activity的Window创建就发生attach方法里,系统会创建Activity所属的Window对象并为其设置回调接口,代码如下:
Activity的attach方法,内部方法,由ActivityThread的调用:
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) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
// 实例化Window对象,是PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
// Activity实现了Window.Callback,以下是设置回调
// 设置Window控制器回调
mWindow.setWindowControllerCallback(this);
// 设置Window回调
mWindow.setCallback(this);
// 设置Window消失的回调
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);
}
// UI线程
mUiThread = Thread.currentThread();
// 主线程
mMainThread = aThread;
mInstrumentation = instr;
// 所依附的Window的token
mToken = token;
mIdent = ident;
// 所在的application信息
mApplication = application;
mIntent = intent;
mReferrer = referrer;
mComponent = intent.getComponent();
// 此activity的信息,和xml中的一致
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());
}
}
// 此activity的Window设置 WindowManager
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());
}
// 赋值WindowManager
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
}
此方法有两个作用:
1,实例化Window对象,是PhoneWindow的实例
2,为此Activity的Window对象设置Callback,是Window.Callback早已被Activity实现
3,赋值其他的线程,Activity信息,Window设置WindowManager等
Window.Callback早已被Activity实现:
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
Callback接口中的方法有很多,有我们熟悉的onAttachedToWindow,onDetachedFromWindow,dispatchTouchEvent等等
可见,Activity immplements Window.Callback 的接口的方法后,Window设置回调mWindow.setCallback(this),所有此Activity的Window的的对外回调状态都是在Activity中,如果想知道Window的状态,重写Activity内实现的Window.Callback方法就好。
下面分析Activity的视图是怎么附属到Window上的?
我们知道Activity的视图由setContentView提供,所以从Activity#setContentView入手:
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
// 可见实际是把 布局文件 设置到 Window上
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
而Window的具体实现是PhoneWindow,只需要看PhoneWindow的相关逻辑:
...
// This is the top-level view of the window, containing the window decor.
// 这是Window的顶层View,包含Content布局
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
// 这是View是 Window的Content布局,要么是Decor自身,要么是Decor的Content的地方,其实例来自上面 mDecor
ViewGroup mContentParent;
...
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
// 1,如果内容布局是null,就初始化DecorView,并产生Content布局
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 {
// 2,把我们的xml布局,添加到 DecorView里的 Content布局
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 3, 回到Activity的 onContentChanged方法,通知我们的xml布局已经附属在 DecorView上了
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
// 就初始化DecorView,并产生Content布局
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
// 如果Content布局是null,就从DecorView 中初始化出来
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
.......其他代码
}
所以,从上面的PhoneWindow的源码可以看出
- PhoneWindow里面含有DecorView和Content布局实例
- 添加xml布局文件时setContentView
- 如果没有 DecorView 就创建一个
DecorView 是 Activity 中的顶级 View,是一个 FrameLayout,一般来说它的内部包含标题栏和内容栏,但是这个会随着主题的变化而改变,不管怎么样,内容栏是一定存在的,并且有固定的 id:”android.R.id.content”,在 PhoneWindow 中,通过 generateDecor 方法创建 DecorView,通过 generateLayout 初始化主题有关布局。 - 将 View 添加到 DecorView 的 mContentParent 中
这一步较为简单,直接将 Activity 的视图添加到 DecorView 的 mContentParent 中即可,由此可以理解 Activity 的 setContentView 这个方法的来历了,为什么不叫 setView 呢?因为 Activity 的布局文件只是被添加到 DecorView 的 mContentParent 中,因此叫 setContentView 更加具体准确。 - 回调 Activity 的 onContentChanged 方法通知 Activity 视图已经发生改变
前面分析到 Activity 实现了 Window 的 Callback 接口,这里当 Activity 的视图已经被添加到 DecorView 的 mContentParent 中了,需要通知 Activity,使其方便做相关的处理。
经过上面的三个步骤,DecorView 已经被创建并初始化完毕,Activity 的布局文件也已经成功添加到了 DecorView 的 mContentParent 中,但是这个时候 DecorView 还没有被 WindowManager 正式添加到 Window 中。
在 ActivityThread 的 handleResumeActivity方法中,首先会调用 Acitivy 的 onResume 方法,接着会调用 Acitivy 的 makeVisible() 方法,正是在 makeVisible 方法中,DecorView 才真正的完成了显示过程,到这里 Activity 的视图才能被用户看到,如下:
void makeVisible(){
if(!mWindowAdded){
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
2. Dialog 的 Window 创建过程
Dialog 的 Window 的创建过程与 Activity 类似,步骤如下:
(1)、创建 Window
Dialog 中 Window 同样是通过 PolicyManager 的 makeNewWindow 方法来完成的,创建后的对象也是 PhoneWindow。
(2)、初始化 DecorView 并将 Dialog 的视图添加到 DecorView 中
这个过程也和 Activity 类似,都是通过 Window 去添加指定布局文件
public void setContentView(int layoutResID){
mWindow.setContentView(layoutResID);
}
(3)、将 DecorView 添加到 Window 中并显示
在 Dialog 的 show 方法中,会通过 WindowManager 将 DecorView 添加到 Window 中,如下:
mWindowManager.addView(mDecor, 1);
mShowing = true;
从上面三个步骤可以发现,Dialog 的 Window 创建过程和 Activity 创建过程很类似,当 Dialog 关闭时,它会通过 WindowManager 来移除 DecorView。普通的 Dialog 必须采用 Activity 的 Context,如果采用 Application 的 Context 就会报错。这是因为没有应用 token 导致的,而应用 token 一般只有 Activity 拥有,另外,系统 Window 比较特殊,可以不需要 token.