Android 窗口管理机制

1、概述

这篇文章主要讲Window、WindowManager、WindowManagerService三者之间的关系及其运行机制。总的来说Window表示的是一种抽象的功能集合,具体实现为PhoneWindow。WindowManager是外界访问Window的入口,对Window的访问必须通过WindowManager,而WindowManger和WindowManagerService的交互是一个IPC过程。Android中所有的视图都是通过Window来呈现的,不管是Activity、Dialog、Toast,他们的视图都是附加在Window上的。Window是一个抽象概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。View才是Window存在的实体,可以理解为WindowManager中的addView()方法,即为add一个Window。

窗口关系图

如上图所示我们平时的View由一个根view和一个Window绑定,然后这一个Window统一被WindowManagerService所管理。这些被WindowManagerService所管理的Window按照一定的次序和位置通过SurfaceFlinger显示到最终屏幕上。上图里面没有提到WindowManager ,WindowManager 在其中扮演的是这么一个角色,用户想添加更新或者移除Window是通过调用WindowManager 的方法来操作。而用户的这些操作指令,从WindowManager 通过IPC传递到WindowManagerService,最后由WindowManagerService来统筹安排这些Window的布局和次序。接下来详细讲解一下其中的运作机制。

2、Window

Window表示一个窗口的意思。其实不管是Activity、Dialog、还是Toast他们的视图实际上都是附加在Window上的。Window 有三种类型,分别是应用 Window、子 Window 和系统 Window。应用类 Window 对应一个 Acitivity,子 Window 不能单独存在,需要依附在特定的父 Window 中,比如常见的一些 Dialog 就是一个子 Window。系统 Window是需要声明权限才能创建的 Window,比如 Toast 和系统状态栏都是系统 Window。

Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖在层级小的 Window 上面,这和 HTML 中的 z-index 概念是完全一致的。在三种 Window 中,应用 Window 层级范围是 1~99,子 Window 层级范围是 1000~1999,系统 Window 层级范围是 2000~2999,这些层级范围对应着 WindowManager.LayoutParams 的 type 参数,如果想要 Window 位于所有 Window 的最顶层,那么采用较大的层级即可,很显然系统 Window 的层级是最大的,当我们采用系统层级时,需要声明权限。

Window是一个抽象概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。如下图所示:

Window、View、ViewRootImpl 关系

其中ViewRootImpl负责对View的渲染,具体的如何操作的请看我的这篇文章:

Android UI刷新机制

3、WindowManager

WindowManager是整个窗口管理机制里面的枢纽,也是这边重点要讲的。WindowManager实现了ViewManager,这个接口定义了我们对于Window的基本操作:


public interface ViewManager

{

    public void addView(View view, ViewGroup.LayoutParams params);

    public void updateViewLayout(View view, ViewGroup.LayoutParams params);

    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 floatingButton = new Button(MainActivity.this);

        floatingButton.setText("button");

        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(

                WindowManager.LayoutParams.WRAP_CONTENT,

                WindowManager.LayoutParams.WRAP_CONTENT,

                0, 0,

                PixelFormat.TRANSPARENT

        );

        // flag 设置 Window 属性

        layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

        // type 设置 Window 类别(层级)

        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;

        layoutParams.gravity = Gravity.CENTER;

        WindowManager windowManager = getWindowManager();

        windowManager.addView(floatingButton, layoutParams);

    }

}

如下效果:

WindowManager 添加Window

代码中并没有调用 Activity 的 setContentView 方法,而是直接通过 WindowManager 添加 Window,并将其type设置为TYPE_APPLICATION_OVERLAY。表示在所有应用之上。由效果图可以看到,这个Button并不是在MainActivity对应的那个View里面,而是一个类似独立的存在。其实从这里可以感受到真正承载View的其实是Window,同时也可以猜出,之所以Activity会对应一个页面是因为Activity持有Window从而来持有View,对于就窗口显示来说,Activity 其实不是必须存在的。比如我们常用的Toast ,它其实就没有与之对应的Activity,而是类似于上述Button产生的方式,产生在界面上的。接下来我们用源码的角度看下Activity中是如何创建Window的。

3.1 Activity 的 Window 创建过程

Activity的启动过程请参见我的这篇文章:Android Activity启动过程-从桌面点击图标到调用Activity的OnCreate 。在Activity启动过程中ActivityThread会调用performLaunchActivity()这个函数,这个函数里面会经过层层深入会调用Activity的OnCreate()方法,而在performLaunchActivity()中调用OnCreate()方法前会调用Activity的attach()方法,今天我们要关注的就是这个方法:


// ActivityThread.class

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) {

        ...

        // 创建PhoneWindow并设置其回调接口

        mWindow = new PhoneWindow(this, window, activityConfigCallback);

        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);

    ...

        // 设置管理Activity的Window的WindowManager

        mWindowManager = mWindow.getWindowManager();

    ...

    }

在attach()这个方法中创建了Activity的Window,同时为该Window绑定WindowManager,将这个WindowManager传给Activity的成员变量mWindowManager。同时我们可以看到Window 的具体实现类是PhoneWindow。

进去setWindowManager()方法中看一下:


public void setWindowManager(WindowManager wm, IBinder appToken, String appName,

            boolean hardwareAccelerated) {

    ...

        if (wm == null) {

            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);

        }

        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);

    }

该方法里面看到如果传入的wm 是空的调用将其赋值(WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);

这里其实是获取了WindowManagerService,当然由于WindowManagerService和activity的应用不在一个进程里,这里是通过Binder通信获取的一个WindowManagerService代理。进程间Binder通信机制请看我的这篇文章:Android Binder进程间通信机制

获取完WindowManagerService代理后通过它来创建出一个真正要用的WindowManager并赋值给。即这句代码:


mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);

我们来看一下createLocalWindowManager()这个函数:


public WindowManagerImpl createLocalWindowManager(Window parentWindow) {

        return new WindowManagerImpl(mContext, parentWindow);

    }

这个函数很简单,只是创建了一个WindowManagerImpl对象。从这里可以看到WindowManager真正的实现类是WindowManagerImpl。

到此我们对Activity的attach()方法分析的差不多了,这个方法主要做了创建出Activity的Window对象,并且获取了管理该Window的WindowManager。而此时其实并没有将Activity对应的View附属到这个Window中。而将这个View附属到Window的代码其实就是我们在OnCreate() 中常调用的setContentView(int layoutResID):


// Activity.class

public void setContentView(int layoutResID){

  getWindow().setContentView(layoutResID);

   ...

}

该方法调用最终调用的是getWindow()的setContentView(layoutResID),而getWindow()获取的就是在attach()中创建的PhoneWindow,我们再进去看一下:


// PhoneWindow.class

public void setContentView(int layoutResID) {

        ...

        if (mContentParent == null) {

            installDecor();

        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

            mContentParent.removeAllViews();

        }

        ...

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

        ...

        } else {

            mLayoutInflater.inflate(layoutResID, mContentParent);

        }

        ...

final Callback cb = getCallback();

        if (cb != null && !isDestroyed()) {

            cb.onContentChanged();

        }

        ···

    }

这里如果没有DecorView 就创建一个,DecorView 是 Activity 中的顶级 View,是一个 FrameLayout,一般来说它的内部包含标题栏和内容栏,但是这个会随着主题的变化而改变,不管怎么样,内容栏是一定存在的,并且有固定id:”android.R.id.content”。而内容栏里面就是将用户写的layout放进去,通过这行代码:


mLayoutInflater.inflate(layoutResID, mContentParent);

之后再回调onContentChanged()通知Activity 视图已经发生改变。此时就将View和Window关联了起来。不过现在任然没有将对应的画面展示到手机屏幕上。因为此时还没讲Window加入到WindowManager 中,更别提提交给WindowManagerService来统筹安排展示了。

Window加入到WindowManager这一过程要在调用完Acitivy的onResume()方法后,之后会调用Activity的makeVisible():


// Activity.class

void makeVisible() {

        if (!mWindowAdded) {

            ViewManager wm = getWindowManager();

            wm.addView(mDecor, getWindow().getAttributes());

            mWindowAdded = true;

        }

        mDecor.setVisibility(View.VISIBLE);

    }

这个函数里首先判断Window是否已经添加到WindowManager中,没有的话取出在刚刚attach()方法中创建的WindowManager,将DecorView加入进去,这里的DecorView其实就是Window所持有那个。然后再将DecorView设置为显示状态,来显示我们的布局。

至此才正真将Window加入到WindowManager中。接下来我们看一下WindowManager里面的几个核心方法。

3.2 WindowManager的核心方法

在实际使用中无法直接访问 Window,对 Window 的访问必须通过 WindowManager。WindowManager 提供的三个接口方法 addView、updateViewLayout 以及 removeView 都是针对 View 的,而这些View都被其对应的Window所持有,所以上面这些操作实际上相当于对Window 的操作,WindowManager 是一个接口,它的真正实现由上文可知是 WindowManagerImpl类。

看一下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);

        }

这3个方法都交给了mGlobal去实现,而mGlobal是一个WindowManagerGlobal类。我们这边看一下WindowManagerGlobal的addView()方法,updateViewLayout()方法和removeView()方法原理类似,只是一个用来添加View,另两个用来更新和移除view。


// WindowManagerGlobal.class

public void addView(View view, ViewGroup.LayoutParams params,

            Display display, Window parentWindow) {

            ...

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

            ...

        ViewRootImpl root;

            root = new ViewRootImpl(view.getContext(), display);

            ...

            mViews.add(view);

            mRoots.add(root);

            mParams.add(wparams);

            ...

            try {

                root.setView(view, wparams, panelParentView);

            } catch (RuntimeException e) {

            ...

            }

        }

    }

在 WindowManagerGlobal 内部有如下几个集合比较重要


private final ArrayListmViews = new ArrayList();

private final ArrayListmRoots = new ArrayList();

private final ArrayListmParams = new ArrayList();

private final ArraySetmDyingViews = new ArraySet();

其中 mViews 存储的是所有 Window 所对应的 View,mRoots 存储的是所有 Window 所对应的 ViewRootImpl,mParams 存储的是所有 Window 所对应的布局参数,mDyingViews 存储了那些正在被删除的 View 对象,或者说是那些已经调用了 removeView 方法但是操作删除还未完成的 Window 对象。在addView ()方法中将这些相关对象添加到对应集合中。在最后调用root.setView(view, wparams, panelParentView);方法,而root一个ViewRootImpl类。我们到setView()方法中看一下:


public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

    ...

                requestLayout();

    ...

             res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,

                            getHostVisibility(), mDisplay.getDisplayId(),

                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,

                            mAttachInfo.mOutsets, mInputChannel);

    ...

            }

        }

    }

setView()中会调用一个很重要的方法requestLayout(),其主要是来刷新页面的。具体详情请看我的这篇文章:Android UI刷新机制。我们今天要关注的是这个方法:


res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,

                            getHostVisibility(), mDisplay.getDisplayId(),

                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,

                            mAttachInfo.mOutsets, mInputChannel);

mWindowSession 的类型是 IWindowSession,它是一个 Binder 对象,真正的实现类是 Session,这也就是之前提到的 IPC 调用的位置。也就是说在这里,完成了WindowManager和WindowManagerService 的通信,将Window 信息传给了WindowManagerService:


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);

}

4、WindowManagerService

上文已经通过WindowManagerService 的代理调用了其addWindow()方法,该方法非常复杂。主要的操作就是将传来的Window 根据它的参数,尤其是其type,保存起来。方便SurfaceFlinger渲染。

WindowManagerService服务大致按照以下方式来控制哪些窗口需要显示的以及要显在哪里:

① 由于每一个Activity窗口的大小都等于屏幕的大小,因此,只要对每一个Activity窗口设置一个不同的Z轴位置,然后就可以使得位于最上面的,即当前被激活的Activity窗口,才是可见的。

② 每一个子窗口的Z轴位置都比它的父窗口大,但是大小要比父窗口小,这时候Activity窗口及其所弹出的子窗口都可以同时显示出来。

③ 对于非全屏Activity窗口来说,它会在屏幕的上方留出一块区域,用来显示状态栏。这块留出来的区域称对于屏幕来说,称为装饰区(decoration),而对于Activity窗口来说,称为内容边衬区(Content Inset)。

④ 输入法窗口只有在需要的时候才会出现,它同样是出现在屏幕的装饰区或者说Activity窗口的内容边衬区的。

⑤ 对于壁纸窗口,它出现需要壁纸的Activity窗口的下方,这时候要求Activity窗口是半透明的,这样就可以将它后面的壁纸窗口一同显示出来。

⑥ 两个Activity窗口在切换过程,实际上就是前一个窗口显示退出动画而后一个窗口显示开始动画的过程,而在动画的显示过程,窗口的大小会有一个变化的过程,这样就导致前后两个Activity窗口的大小不再都等于屏幕的大小,因而它们就有可能同时都处于可见的状态。事实上,Activity窗口的切换过程是相当复杂的,因为即将要显示的Activity窗口可能还会被设置一个启动窗口(Starting Window)。一个被设置了启动窗口的Activity窗口要等到它的启动窗口显示了之后才可以显示出来。

同时在Android系统中,WindowManagerService服务是通过一个实现了WindowManagerPolicy接口的策略类来计算一个窗口的位置和大小的。例如,在Phone平台上,这个策略类就是PhoneWindowManager。这样做的好处就是对于不同的平台实现不同的策略类来达到不同的窗口控制模式。

5、总结

总的来说,整个流程大致如下:

① 创建Window,将View和ViewRootImpl同Window绑定。

② WindowManager的addView()、updateViewLayout() 和 removeView()方法操作Window。

③ 将WindowManager这些操作方法转移给WindowManagerGobal来调用。

④ 调用与Window绑定的ViewRootImpl的setView()方法。

⑤ setView()方法里面会通过mWindowSession这个Binder对象将Window传给WindowManagerService。

⑥ WindowManagerService来管理各个Window的大小和显示位置,来让SurfaceFlinger渲染。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容