View的绘制流程简单总结

482 阅读5分钟

Android UI渲染流程核心梳理


一、DecorView的添加时机

1. 添加时机概述

  • DecorView是什么?
    每个Activity界面的最顶层View(整个Window的根节点),承载status bar、navigation bar和内容区。

  • 添加流程
    DecorView真正被添加到Window,是在Activity的onResume之后。主要执行链路如下:

    handleResumeActivity
        → performResumeActivity
            → r.activity.performResume()
                → mInstrumentation.callActivityOnResume(this)
        → wm.addView(decor, l); // 关键点
            → WindowManagerGlobal.addView
                → root = new ViewRootImpl(...)
                → mViews.add(view)         // DecorView
                → mRoots.add(root)        // ViewRootImpl
                → mParams.add(wparams)    // WindowLayoutParams
                → root.setView(view, ...)
    

    WindowManagerGlobal:
    管理整个进程内所有窗口,mViews/mRoots/mParams分别记录View、ViewRootImpl、LayoutParams。

    ViewRootImpl:
    负责与系统WMS通信,实际驱动UI绘制、输入、布局等。


二、ViewRootImpl.setView()核心流程

setView主要做了什么?

  1. 请求布局(requestLayout → scheduleTraversals)

    • 通过Choreographer投递异步任务,最终驱动measure/layout/draw流程。
  2. 把窗口注册到WMS

    • 调用mWindowSession.addToDisplayAsUser注册到WindowManagerService。
  3. 事件监听与输入绑定

    • 初始化InputEventReceiver
  4. 建立View树父子关系

    • view.assignParent(this),View最终父节点是ViewRootImpl。

关键流程代码片段

ViewRootImpl.setView:
    requestLayout() // 发起measure/layout/draw
    res = mWindowSession.addToDisplayAsUser(...)
    mInputEventReceiver = new WindowInputEventReceiver(...)
    view.assignParent(this)

三、ViewRootImpl构造与主流程

  • ViewRootImpl构造

    • mThread:当前线程(一般为主线程)
    • mAttachInfo:保存与WMS通信的binder和窗口参数
  • performTraversals流程
    performTraversals流程图

    核心步骤:

    1. measureHierarchy: 预测量,主要优化wrap_content布局(最多三次)
    2. relayoutWindow: 通知WMS调整窗口
    3. performMeasure: 递归测量控件树
    4. performLayout: 布局所有子view
    5. performDraw: 绘制

四、measureHierarchy & performMeasure机制

  • measureHierarchy
    预测量窗口内容大小,对wrap_content的Dialog/Popup等优化。测量流程可能多达3次(如果大小一直不合适),加上performMeasure的1次,onMeasure最多被调用4次。

  • performMeasure

    • 调用View.measure(),最终执行onMeasure。
    • onMeasure里必须调用setMeasuredDimension,否则会crash(源码有强制校验)。
  • MeasureSpec

    • 32位int,低30位为size,高2位为mode(EXACTLY/AT_MOST/UNSPECIFIED)

五、UI线程、刷新与ViewGroup onDraw问题

  1. UI刷新不一定只能在主线程

    • 只要ViewRootImpl创建在哪个线程,该线程就能刷新UI(如子线程新建window)。
  2. 三种方式实现子线程UI操作

    • 在ViewRootImpl创建前
    • 子线程中主动创建ViewRootImpl和window
    • 反射修改标志位(极少用)
  3. ViewGroup为什么一般不会走onDraw?

    • View.draw()内部:

      • ViewGroup默认设置了PFLAG_SKIP_DRAW
      • 满足条件只会走dispatchDraw,不走onDraw
    • 只有去掉PFLAG_SKIP_DRAW或者自定义ViewGroup重写onDraw才会执行onDraw


六、典型子线程刷新UI代码

public void childThreadChangeUI() {
    new Thread() {
        @Override
        public void run() {
            Looper.prepare();
            WindowManager wm = ...;
            View view = ...;
            wm.addView(view, params);
            // 监听/操作
            Looper.loop();
        }
    }.start();
}

总结

  • DecorView在onResume后、WindowManager.addView时添加到window
  • ViewRootImpl负责UI测量、布局、绘制、输入事件、WMS通信
  • measure/layout/draw流程清晰,每步都可定制优化
  • UI操作只受限于ViewRootImpl所在线程,主线程是通用约定,但有特殊场景可在子线程
  • ViewGroup默认不走onDraw,实际项目需知其根本原理

学后检测

一、单选题

  1. DecorView被添加到Window的时机是?

    • A. setContentView之后
    • B. Activity.onCreate之后
    • C. Activity.onResume之后,WindowManager.addView时
    • D. Activity.onStart之后

    答案:C

    解析:
    实际是在onResume之后,通过WindowManager.addView(decorView, ...)完成。


  1. ViewRootImpl主要负责什么?

    • A. 只保存窗口参数
    • B. 只分发输入事件
    • C. 负责与WMS通信,驱动measure/layout/draw,输入、刷新等
    • D. 只存放DecorView

    答案:C

    解析:
    ViewRootImpl是连接View和WindowManagerService的桥梁,统筹UI流程。


  1. View.measure()最终不调用setMeasuredDimension会怎样?

    • A. 正常显示
    • B. 不影响onLayout
    • C. 抛出IllegalStateException异常
    • D. 会重复测量直到调用为止

    答案:C

    解析:
    源码强制要求必须在onMeasure中调用setMeasuredDimension,否则直接抛异常。


二、多选题

  1. 关于View的测量和布局,下列说法正确的是?

    • A. wrap_content可能导致多次预测量
    • B. performMeasure和onMeasure的调用次数可能大于1
    • C. onMeasure内部可以不调用setMeasuredDimension
    • D. View树测量和布局都由ViewRootImpl驱动

    答案:A、B、D

    解析:
    wrap_content、弹窗等多次预测量;ViewRootImpl驱动所有measure/layout/draw。


  1. 下列关于UI线程与刷新说法正确的是?

    • A. UI刷新只能在主线程进行
    • B. 子线程可以创建自己的window并刷新UI
    • C. invalidate/checkThread机制依赖ViewRootImpl创建时的线程
    • D. ViewRootImpl负责checkThread,保证线程安全

    答案:B、C、D

    解析:
    并非只能主线程;ViewRootImpl的线程决定刷新归属,invalidate/checkThread也基于此。


三、判断题

  1. ViewGroup默认不会执行onDraw方法。 (对)

    解析:
    默认PFLAG_SKIP_DRAW只走dispatchDraw,onDraw只有自定义移除该flag才会走。


  1. 每次UI刷新都必须走主线程。 (错)

    解析:
    理论上ViewRootImpl创建在哪个线程,就在哪个线程刷新,不一定是主线程。


  1. View的onMeasure最多只能被调用一次。 (错)

    解析:
    measureHierarchy会多次预测量,最多4次。


四、简答题

  1. 简述DecorView添加到Window的完整流程及相关核心类的作用。

    参考答案:
    DecorView在Activity生命周期onResume之后,通过WindowManager.addView添加到Window,WindowManagerGlobal负责进程内窗口管理,ViewRootImpl负责窗口的实际驱动和与WMS通信,setView方法里发起UI遍历流程、窗口注册和事件绑定。


  1. 为什么ViewGroup不会默认走onDraw?如果需要自定义绘制应如何处理?

    参考答案:
    ViewGroup源码默认设置了PFLAG_SKIP_DRAW,所以只会走dispatchDraw,onDraw不会被调用。如果需要自定义绘制,可在构造方法中clear掉该flag,或者直接重写onDraw并确保flag未被设置。


五、编程题

请写出一段代码,在子线程中创建一个Window,并在该window内动态添加一个可点击的View,实现自定义拖动。

(本题略,参考上文childThreadChangeUI(),强调WindowManager、Looper.prepare/loop、addView/updateViewLayout等关键点)