Android:关于Window少为人知的一面

前言

大部分情况下,我们和Window打交道的情况比较少,一般都是与Activity和View“交流”。最近做了不少与Window相关的工作,梳理了下Window相关的知识,在此,与大家分享下。关于系统源码相关的知识如何介绍,其实是比较费神,如果只是贴出源码,做出解释,其实不好理解和记忆,并且网上关于源码分析的文章太多太多。因此,我会结合例子来演示,并把常见问题贴出来并加以分析。我相信,以后面试的时候,你可能会碰到此类问题。

知识点

Window是什么
首先看下官方的定义:

Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc.
The only existing implementation of this abstract class is PhoneWindow, which you should instantiate when needing a Window.

大概翻译如下:
Window类是一个抽象类,它定义了顶级窗体样式和行为。一个Window实例应作为顶级View添加到WindowManager中。它提供标准的UI规则,例如背景、标题、默认关键过程等。

Window有且仅有一个实现类PhoneWindow,如果我们需要Window实例化PhoneWindow即可。

看到这,其实还是云里雾里,并没有一个切身的体会,所以还是要继续分析。

Window的类型

在WindowManager中定义了许多Window类型,总共分为以下三类。
1、Application Window。Value from 1 to 99 .
比如:activity、dialog…
2、Sub Window。Value from 1000 to 1999.
比如:ContextMenu、PopupWindow…
3、System Window。Value from 2000 to 2999.
比如:Toast、SystemAlert、 InputMethod...
关于此处更详细的讲解请参考浅析Android的窗口,本文不做重复介绍。此文解释的非常全面。

Window token

token这个词我们在开发的时候可能碰到过,比如某些Dialog的异常信息就经常会报bad Token错误。那么Window token起了什么作用。大部分Window Token都是IBinder或者IInterface对象,之所以用IBinder或者IInterface,因为它们能用于跨进程间通信,能够用于Window的身份验证,是Window与WMS(WindowManagerService)交流的凭证。在浅析Android的窗口"token的含义"中更详细的介绍,可以作为参考。
WindowManager.LayoutParams类中有个token字段。

 /**
         * Identifier for this window.  This will usually be filled in for
         * you.
         */
        public IBinder token = null;

父窗口和其子窗口的token对象是同一个,一般都是父窗口赋给子窗口。
在Window中有个方法adjustLayoutParamsForSubWindow就是用于把父窗口的token复制给子窗口。
如果Window没有父窗口,那么会由WMS为其创建token。

Window的显示过程

window显示过程

Window显示的过程还是挺复杂的,不过其中我们只需关注几个核心类的即可,比如ViewRootImpl、WindowManagerService。
另外这其中有个Session类,该类的作用是用于单独标识每个进程,与token的作用类似,只不过token是代表标识Window。

创建一个Window试试

在Activity中,运行下述方法。

    private void generateSystemAlert() {
        //Activity Context
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        //Application Context
        //WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.width = WindowManager.LayoutParams.MATCH_PARENT;
        params.height = 500;
        params.gravity = Gravity.CENTER;
        final View view = LayoutInflater.from(this).inflate(R.layout.window_system_alert, null);
        wm.addView(view, params);
    }
创建Window

添加Window的过程和往ViewGroup中添加View的过程相当类似。WindowManager管理Window的添加、移除等操作。

WindowManager是一个接口,应用程序可以通过WindowManager来与系统Window服务交流。我们可以通过
Context.getSystemService(Context.WINDOW_SERVICE)
来获取WindowManager实例。
在上述代码中,我采取了两种不同的方式获取WindowManager对象。一种是直接调用Activity中的getSystemService方法,另外一种是通过调用Application Context的getSystemService方法。

WindowManager不同获取方式的效果

通过实践可以得知:
用Application Context获取的WindowManager实例来创建Window并不随Activity的消失而消失,只有当进程被杀的时候才消失。
用Activity Context获取的WindowManager实例来创建Window随Activity的消失而消失。
另外,上面的动态图演示的时候,当按下返回键的时候,应用程序打印如下错误。

E/WindowManager: android.view.WindowLeaked: 
Activity com.kisson.windowtest.MainActivity has leaked window 
android.support.v7.widget.AppCompatTextView{1b90179 V.ED.... ......I. 0,0-1080,200 #7f0c0061 app:id/tv} that was originally added here
                         at android.view.ViewRootImpl.<init>(ViewRootImpl.java:363)
                         at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:271)
                         at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
                         at com.kisson.windowtest.MainActivity.generateSystemAlert(MainActivity.java:93)
                         at com.kisson.windowtest.MainActivity.access$100(MainActivity.java:15)
                         at com.kisson.windowtest.MainActivity$2.onClick(MainActivity.java:31)
                         at android.view.View.performClick(View.java:4780)
                         at android.view.View$PerformClick.run(View.java:19866)

这个报错我们应该不陌生--“窗体泄露”。特别是使用Dialog的时候,如果Dialog未dismiss,某种情况下就会导致窗体泄露,所以Android提供了DialogFragment来解决此类问题。

通过以上演示,我们可以得出以下问题:
1、为什么获取WindowManager的Context类型不同,导致Window的生命周期不同。
2、什么情况下发生窗体泄露。

看源码分析问题

1.Application Context的getSystemService方法
Application Context调用的getSystemService方法是Context抽象类中的一个抽象方法,该方法的实现是在ContextImpl类中。

    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

在SystemServiceRegistry类中,找到注册WINDOW_SERVICE方法如下。

 registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx.getDisplay());
            }});

从此,我们可以看到创建WindowManager对象,最后是调用WindowManagerImpl(Display display)构造方法创建。暂且分析到这,读者有兴趣的话可以追踪源码来分析。
2.Activity的getSystemService方法

@Override
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);
    }

从代码可知,Activity重写了getSystemService,当传入的参数ServiceName等于WINDOW_SERVICE,直接返回mWindowManager对象。接着,分析mWindowManager是如何创建。
在Activity的attach方法,创建了mWindowManager实例。

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) {
        //......
        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;
    }

mWindowManager是在mWindow(mWindow是Window的实例)的setWindowManager方法中创建。接着来看下setWindowManager方法做了什么。

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated
                || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

在setWindowManager方法中有个参数也是WindowManager对象(该对象就是ContextImpl中getSystemService获取到的),然后调用该对象的createLocalWindowManager方法创建Activity的mWindowManager实例。

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mDisplay, parentWindow);
    }

到此就明朗,通过调用WindowManagerImpl(Display,Window)的构造方法创建mWindowManager对象。

所以我们可得出Application Context和Activity Context创建的WindowManager对象是通过WindowManagerImpl不同的构造方法来创建的。

    public WindowManagerImpl(Display display) {
        this(display, null);
    }

    private WindowManagerImpl(Display display, Window parentWindow) {
        mDisplay = display;
        mParentWindow = parentWindow;
    }

Application Context创建的WindowManagerImpl对象是没有父窗口(parentWindow)。
Activity Context创建的WindowManagerImpl对象会将Activity的mWindow作为父窗口。
通过分析和前面的演示,可以得出结论。
1.如果某个Window有parentWindow,当parentWindow remove之后,子窗口也会被remove。
2.若有子Window未remove,则会出现窗体泄露错误信息。

有结论了,接下来就验证下。
对Android源码搜索报错关键句“that was originally added here”,发现在WinowManagerGlobal的closeAll方法出现该语句。其实从该方法的名字就可以猜出来,关闭所有窗口。

    public void closeAll(IBinder token, String who, String what) {
        synchronized (mLock) {
            int count = mViews.size();
            //Log.i("foo", "Closing all windows of " + token);
            for (int i = 0; i < count; i++) {
                //Log.i("foo", "@ " + i + " token " + mParams[i].token
                //        + " view " + mRoots[i].getView());
                if (token == null || mParams.get(i).token == token) {
                    ViewRootImpl root = mRoots.get(i);

                    //Log.i("foo", "Force closing " + root);
                    if (who != null) {
                        WindowLeaked leak = new WindowLeaked(
                                what + " " + who + " has leaked window "
                                + root.getView() + " that was originally added here");
                        leak.setStackTrace(root.getLocation().getStackTrace());
                        Log.e(TAG, "", leak);
                    }

                    removeViewLocked(i, false);
                }
            }
        }
    }

接着,看下closeAll在哪调用。继续搜索,发现在ActivityThread的handleDestroyActivity方法中调用了closeAll方法。

private void handleDestroyActivity(IBinder token, boolean finishing,
            int configChanges, boolean getNonConfigInstance) {
        ActivityClientRecord r = performDestroyActivity(token, finishing,
                configChanges, getNonConfigInstance);
        //......
            if (r.mPendingRemoveWindow == null) {
                //读者可以仔细体会此段英文
                // If we are delaying the removal of the activity window, then
                // we can't clean up all windows here.  Note that we can't do
                // so later either, which means any windows that aren't closed
                // by the app will leak.  Well we try to warning them a lot
                // about leaking windows, because that is a bug, so if they are
                // using this recreate facility then they get to live with leaks.
                WindowManagerGlobal.getInstance().closeAll(token,
                        r.activity.getClass().getName(), "Activity");
            }
    //......
    }

到此,可以知道,在Activity destroy时,会去检查是否有窗体泄露。

这里我们要注意,窗体泄露只会在堆栈中打印错误信息,不会导致应用程序崩溃。

从Activity到Window

Activity使我们开发过程中最经常打交道的组件,因此从Activity和Window的关系,可以更好的帮助我们理解Window是啥玩意。
创建页面布局,我们都是通过调用Activity的setContentView方法

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

从这里,我们可以知道,最终是调用Window的setContentView方法,该方法的实现是在PhoneWindow中。

@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.
        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();
        }
    }

从该代码中,大概做了如下操作:

  1. installDecor.
    1).generateDecor
    2).generateLayout
  2. inflate your layout.

DecorView是一个自定义FrameLayout,它是作为Activity中Window的顶级View,比如状态栏,标题栏等。因为每个非全屏Activity都显示状态栏,所以系统把某些共性的View都抽象出来,避免我们做重复工作。如果各位想要更深入了解DecorView,可以结合网上资料进行理解。generateDecor的过程就是创建DecorView的过程。

每个Activity都是有自己资源ID形式的布局。在填充资源layout时候,会根据不同的feature来选择不同的布局。
大概有如下几种。

R.layout.screen_title_icons
R.layout.screen_progress
R.layout.screen_custom_title
R.layout.screen_action_bar
R.layout.screen_simple_overlay_action_mode;
R.layout.screen_simple

接着我们来来看R.layout.screen_simple布局是什么样子。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

AndroidUI优化实践有介绍,ViewStub用于懒加载actionBar,而id为@android:id/content的FrameLayout,此FrameLayout就是contentView。我们在Activity中调用setContentView方法,设置布局,最终就是添加到该FrameLayout中。

Activity中View层级

从上图中,我们很清楚的看到整个View的层次。DecorView最为Activity Window中的顶级View。

分析到这里,整个Activity需要显示的View创建好了,那么如何显示?
在Activity中,我并未找到显示DecorView的方法。根据阅读源码经验,应该会在ActivityThread中。
在ActivityThread的handleResumeActivity中

if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                }

我们可以看到DecorView通过wm添加到系统中。

创建Window的实质

从以上的分析,我们可以得出创建Window的过程其实就是添加View的过程。比如Activity中decorView是作为顶级View添加到系统中去显示,顶级View的LayoutParams必须是WindowManager.LayoutParams类型,否则会报错。那么为什么会有Window的存在,我们直接用添加View形式,创建Activity所需显示的界面不久就了。
这里就又回到最初Window的定义,Window它定义了顶级窗体样式和行为。因为每个Activity都会标题栏,状态栏等,在Window中它提供了DecorView的创建,省去我们不少麻烦,通过Window提供API,我们可以很方面改变标题栏,状态栏的样式。同时Window也提供某些共性操作的行为,比如返回键操作、触摸事件传递,menu显示与因此等。Window最核心的内容还是它提供的顶级View--DecorView及其相关操作。

最后

Window在我们开发过程中起着至关重要的地位。因此,深入了解Window对我们解决一系列UI问题大有裨益。你深入了解Window后就会知道:
1.为什么Dialog的创建必须要用Activity Context。
2.为什么PopupWindow的生命周期与Activity绑定。
3.为什么我创建的Window不能显示、不能点击等等。

如果觉得对你有帮助,请不要吝惜您的喜欢!谢谢!

推荐阅读更多精彩内容