第3章:Activity的窗口管理与ViewRootImpl

说实话,窗口管理这块,我当年刚接触时也是一头雾水。

Activity、Window、DecorView、ViewRootImpl……这些概念堆在一起,很容易让人懵。但搞懂了它们之间的关系,你才算真正理解了Android的UI体系。

今天我们就来拆解一下,窗口是怎么创建的,ViewRootImpl又是怎么把布局变成屏幕上的像素的。

3.1 Window与WindowManager:窗口的抽象与操作

先问一个问题:Activity是怎么把布局显示到屏幕上的?

很多人以为Activity直接持有View,其实不是。中间还隔着一层——Window。

Window是个抽象类,它定义了窗口的基本行为。比如添加View、设置布局参数、管理触摸事件等等。但Window本身不干活,真正干活的是它的实现类——PhoneWindow

核心要点:每个Activity对应一个PhoneWindow实例。这个PhoneWindow是在Activity的attach()方法里创建的。

我们来看一下源码中的创建过程:

// Activity.java
final void attach(...) {
    // 创建PhoneWindow
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    // 设置WindowManager
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    mWindowManager = mWindow.getWindowManager();
}

嗯,这里要注意:mWindowManager并不是系统服务那个WindowManager,而是WindowManagerImpl的实例。它内部持有WindowManagerGlobal,这才是真正干活的单例。

个人经验:我在做悬浮窗功能时,就踩过这个坑。直接用系统服务拿到的WindowManager添加View,和Activity内部拿到的WindowManager,行为是不一样的。前者需要SYSTEM_ALERT_WINDOW权限,后者不需要。

3.2 DecorView:窗口的根布局

有了Window,接下来就是往里面放View了。

Activity在setContentView()时,会触发一个关键操作——创建DecorView。

// PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        // 首次调用,创建DecorView
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    // 把我们的布局inflate到mContentParent中
    mLayoutInflater.inflate(layoutResID, mContentParent);
}

installDecor()里做了什么?说白了就是创建了一个DecorView,然后从它里面找到android.R.id.content这个FrameLayout,赋值给mContentParent

DecorView的结构大概是这样的:

DecorView
  ├── LinearLayout (垂直)
  │   ├── TitleArea (标题栏区域,可选)
  │   └── ContentFrameLayout (id = content)
  │       └── 你的布局
  └── StatusBarBackground (状态栏背景)

所以,你写的布局其实是被塞进了DecorView的content区域里。DecorView才是真正的根View。

避坑指南:我曾经在自定义Dialog时,直接拿DecorView做动画,结果发现状态栏也跟着动了。原因就是DecorView包含了整个窗口区域,不只是内容区。后来我改用getWindow().getDecorView()的子View做动画,才解决问题。

3.3 ViewRootImpl:窗口管理的核心

DecorView创建好了,但还没显示出来。谁来触发绘制?谁来处理事件?

答案是ViewRootImpl。

当Activity调用onResume()后,WindowManager会通过addView()把DecorView添加进去。这个过程会创建一个ViewRootImpl,并建立关联。

// WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    // 创建ViewRootImpl
    ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
    // 建立关联
    root.setView(view, wparams, panelParentView);
    // 保存到列表中
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
}

ViewRootImpl一创建,就开始干活了。它会发起第一次requestLayout(),触发整个绘制流程。

3.4 performTraversals:一次完整的绘制流程

这是ViewRootImpl里最核心的方法,没有之一。

performTraversals()负责协调测量、布局、绘制三个阶段。我把它称为「三件套」:

  1. performMeasure — 测量每个View的大小
  2. performLayout — 确定每个View的位置
  3. performDraw — 把View画到屏幕上

我们来看简化后的流程:

// ViewRootImpl.java (简化)
private void performTraversals() {
    // 1. 获取窗口参数
    WindowManager.LayoutParams lp = mWindowAttributes;
    
    // 2. 测量阶段
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    // 3. 布局阶段
    performLayout(lp, mWidth, mHeight);
    
    // 4. 绘制阶段
    performDraw();
}

每个阶段内部,都会递归调用子View的对应方法。比如performMeasure会调用DecorView的measure(),然后DecorView再调用子View的measure(),一层层传下去。

关键点:performTraversals并不是只执行一次。每次调用requestLayout()invalidate(),都会触发新一轮的遍历。但requestLayout()只会触发measure和layout,不会触发draw;而invalidate()只会触发draw。

为什么会这样?因为requestLayout()设置了mLayoutRequested = true,而invalidate()设置了mDirty区域。performTraversals里会根据这些标志位决定执行哪些阶段。

3.5 窗口添加的完整链路

最后,我们把整个流程串起来:

步骤 触发者 做了什么
1 Activity.attach() 创建PhoneWindow,设置WindowManager
2 Activity.setContentView() 创建DecorView,inflate布局到content区域
3 Activity.onResume() 触发WindowManager.addView()
4 WindowManagerGlobal.addView() 创建ViewRootImpl,调用root.setView()
5 ViewRootImpl.setView() 发起requestLayout(),触发performTraversals()
6 ViewRootImpl.performTraversals() 执行measure → layout → draw

你看,从Activity到屏幕上的像素,中间经过了这么多层。但每一层各司其职,配合得相当默契。

我的建议:如果你遇到UI显示异常的问题,比如布局没出来、位置不对、闪烁等,可以沿着这条链路排查。先看DecorView有没有创建,再看ViewRootImpl有没有触发遍历,最后看measure/layout的结果对不对。我排查过不下20个UI bug,90%都能在这条链路上找到根因。

嗯,窗口管理这块内容确实不少。但搞懂了Window、DecorView、ViewRootImpl这三者的关系,以及performTraversals的流程,你就掌握了Android UI系统的核心脉络。下一章我们会深入View的测量过程,看看MeasureSpec到底是怎么传递的。