1、View绘制流程总览:从Activity启动到ViewRootImpl,理解整个绘制链路的起点

大家好,我是你们的Android讲师。

今天咱们正式开始啃这块硬骨头——View绘制流程

说实话,我刚入行那会儿,看到屏幕上花花绿绿的界面,总觉得这玩意儿挺神奇的。后来自己动手写自定义View,动不动就卡顿、显示不全,才意识到:不懂底层绘制流程,写出来的UI就是碰运气

这一章,我们先搭个全局框架。不急着抠细节,先把从Activity启动到ViewRootImpl这条链路走通。你想想看,一个Activity从创建到界面显示,中间到底经历了什么?

1.1 起点:Activity的启动

一切从startActivity()开始。

但咱们关心的不是AMS怎么调度,而是界面什么时候开始准备绘制

关键节点在ActivityThread.handleLaunchActivity()里。它会调用Activity.onCreate(),你在onCreate()里写了setContentView(),这时候布局文件被解析成View树,但注意——还没开始测量和绘制

核心认知:onCreate()只是把布局加载进内存,真正的绘制还没开始。

我记得有个同事,在onCreate()里直接拿View.getWidth(),结果全是0。他一脸懵地问我为什么。嗯,这就是典型的时机不对

1.2 关键转折:Window与DecorView

Activity.onCreate()执行完后,ActivityThread会调用handleResumeActivity()。这里有个重要动作:创建PhoneWindow,并把DecorView添加到WindowManager

说白了,DecorView就是整个Activity界面的根View。它包含了一个LinearLayout,里面放着标题栏和内容区域。你写的布局文件,最终被塞进android.R.id.content这个FrameLayout里。

// 伪代码示意
// ActivityThread.handleResumeActivity()
View decor = activity.getWindow().getDecorView();
ViewManager wm = activity.getWindowManager();
wm.addView(decor, layoutParams);

这里有个细节:WindowManager.addView()最终会走到WindowManagerGlobal.addView()。而WindowManagerGlobal会创建一个ViewRootImpl对象,并调用root.setView()

我的经验:调试UI问题时,经常在WindowManagerGlobal里打断点。这里能看到所有Window的添加和移除,对排查窗口泄漏特别有用。

1.3 核心枢纽:ViewRootImpl登场

ViewRootImpl是什么?

它是View树和WindowManager之间的桥梁。没有它,你的View就是一堆孤立的对象,没法被绘制到屏幕上。

ViewRootImpl.setView()里做了三件大事:

  1. 建立连接:通过Binder与WMS(WindowManagerService)通信,请求添加窗口
  2. 请求布局:调用requestLayout(),触发第一次绘制
  3. 接收事件:注册InputChannel,开始接收触摸事件

其中,requestLayout()是绘制流程的真正起点

// ViewRootImpl.java (简化)
public void setView(View view, ...) {
    // 1. 与WMS通信
    res = mWindowSession.addToDisplay(mWindow, ...);
    
    // 2. 触发第一次绘制
    requestLayout();
    
    // 3. 注册输入事件
    mInputChannel = new InputChannel();
    mWindowSession.addToDisplay(mInputChannel, ...);
}

注意:requestLayout()不是立即执行绘制,而是发送一个异步消息。真正干活是在下一个VSync信号到来时。

1.4 绘制三兄弟:measure、layout、draw

ViewRootImpl收到VSync信号后,会调用performTraversals()。这个方法,就是整个绘制流程的总调度中心

它里面依次调用了三个核心方法:

阶段 方法 作用
测量 performMeasure() 确定每个View的宽高
布局 performLayout() 确定每个View的位置
绘制 performDraw() 把View画到Canvas上

这三个阶段,顺序不能乱。你想想看,不知道大小怎么定位置?不知道位置怎么画?

我个人习惯把performTraversals()比作导演,measure、layout、draw就是三个演员。导演喊一声“Action”,演员按剧本依次上场。

1.5 从performTraversals到ViewRootImpl的完整链路

咱们把整个链路串起来,画个流程图:

Activity.startActivity()
  → ActivityThread.handleLaunchActivity()
    → Activity.onCreate()  // 加载布局,但未绘制
    → ActivityThread.handleResumeActivity()
      → WindowManager.addView(decor)
        → WindowManagerGlobal.addView()
          → 创建ViewRootImpl
            → ViewRootImpl.setView()
              → requestLayout()
                → scheduleTraversals()
                  → doTraversal()
                    → performTraversals()
                      → performMeasure()
                      → performLayout()
                      → performDraw()

看到没?从startActivity()performDraw(),中间隔了整整七八层调用。每一层都有它的职责,少一环都不行。

避坑指南:我曾经在onResume()里直接获取View的宽高,结果全是0。后来才知道,onResume()执行时performTraversals()还没跑完。正确的做法是用View.post()或者ViewTreeObserver

1.6 为什么需要理解这条链路?

你可能会问:我平时写布局,不都是直接写XML吗?知道这些底层细节有什么用?

用处大了去了:

  • 性能优化:知道绘制是在performTraversals()里完成的,你就明白为什么频繁requestLayout()会导致卡顿
  • 自定义View:理解measure/layout/draw的调用顺序,你写自定义View时就不会乱调方法
  • 问题排查:UI显示异常时,你能快速定位是哪个阶段出了问题

我记得有一次,项目里有个页面启动特别慢。我顺着performTraversals()往下查,发现是某个自定义View在onMeasure()里做了大量计算。优化后,启动速度提升了30%。

小技巧:Choreographer可以监控每一帧的绘制耗时。在performTraversals()前后打日志,能精确知道measure、layout、draw各花了多少时间。

1.7 本章小结

这一章,咱们把View绘制的宏观链路走了一遍:

  1. Activity启动后,通过WindowManagerDecorView添加进来
  2. ViewRootImpl作为桥梁,连接View树和WMS
  3. performTraversals()是总调度,依次执行measure、layout、draw
  4. 整个流程是异步的,依赖VSync信号触发

下一章,咱们深入performMeasure(),看看MeasureSpec到底是怎么计算出来的。到时候你会发现,很多布局问题,其实都是MeasureSpec没搞明白。

嗯,今天就到这儿。有问题欢迎留言,咱们下章见。