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主要做了什么?
-
请求布局(requestLayout → scheduleTraversals)
- 通过Choreographer投递异步任务,最终驱动measure/layout/draw流程。
-
把窗口注册到WMS
- 调用mWindowSession.addToDisplayAsUser注册到WindowManagerService。
-
事件监听与输入绑定
- 初始化InputEventReceiver
-
建立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流程
核心步骤:
- measureHierarchy: 预测量,主要优化wrap_content布局(最多三次)
- relayoutWindow: 通知WMS调整窗口
- performMeasure: 递归测量控件树
- performLayout: 布局所有子view
- 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问题
-
UI刷新不一定只能在主线程
- 只要ViewRootImpl创建在哪个线程,该线程就能刷新UI(如子线程新建window)。
-
三种方式实现子线程UI操作
- 在ViewRootImpl创建前
- 子线程中主动创建ViewRootImpl和window
- 反射修改标志位(极少用)
-
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,实际项目需知其根本原理
学后检测
一、单选题
-
DecorView被添加到Window的时机是?
- A. setContentView之后
- B. Activity.onCreate之后
- C. Activity.onResume之后,WindowManager.addView时
- D. Activity.onStart之后
答案:C
解析:
实际是在onResume之后,通过WindowManager.addView(decorView, ...)完成。
-
ViewRootImpl主要负责什么?
- A. 只保存窗口参数
- B. 只分发输入事件
- C. 负责与WMS通信,驱动measure/layout/draw,输入、刷新等
- D. 只存放DecorView
答案:C
解析:
ViewRootImpl是连接View和WindowManagerService的桥梁,统筹UI流程。
-
View.measure()最终不调用setMeasuredDimension会怎样?
- A. 正常显示
- B. 不影响onLayout
- C. 抛出IllegalStateException异常
- D. 会重复测量直到调用为止
答案:C
解析:
源码强制要求必须在onMeasure中调用setMeasuredDimension,否则直接抛异常。
二、多选题
-
关于View的测量和布局,下列说法正确的是?
- A. wrap_content可能导致多次预测量
- B. performMeasure和onMeasure的调用次数可能大于1
- C. onMeasure内部可以不调用setMeasuredDimension
- D. View树测量和布局都由ViewRootImpl驱动
答案:A、B、D
解析:
wrap_content、弹窗等多次预测量;ViewRootImpl驱动所有measure/layout/draw。
-
下列关于UI线程与刷新说法正确的是?
- A. UI刷新只能在主线程进行
- B. 子线程可以创建自己的window并刷新UI
- C. invalidate/checkThread机制依赖ViewRootImpl创建时的线程
- D. ViewRootImpl负责checkThread,保证线程安全
答案:B、C、D
解析:
并非只能主线程;ViewRootImpl的线程决定刷新归属,invalidate/checkThread也基于此。
三、判断题
-
ViewGroup默认不会执行onDraw方法。 (对)
解析:
默认PFLAG_SKIP_DRAW只走dispatchDraw,onDraw只有自定义移除该flag才会走。
-
每次UI刷新都必须走主线程。 (错)
解析:
理论上ViewRootImpl创建在哪个线程,就在哪个线程刷新,不一定是主线程。
-
View的onMeasure最多只能被调用一次。 (错)
解析:
measureHierarchy会多次预测量,最多4次。
四、简答题
-
简述DecorView添加到Window的完整流程及相关核心类的作用。
参考答案:
DecorView在Activity生命周期onResume之后,通过WindowManager.addView添加到Window,WindowManagerGlobal负责进程内窗口管理,ViewRootImpl负责窗口的实际驱动和与WMS通信,setView方法里发起UI遍历流程、窗口注册和事件绑定。
-
为什么ViewGroup不会默认走onDraw?如果需要自定义绘制应如何处理?
参考答案:
ViewGroup源码默认设置了PFLAG_SKIP_DRAW,所以只会走dispatchDraw,onDraw不会被调用。如果需要自定义绘制,可在构造方法中clear掉该flag,或者直接重写onDraw并确保flag未被设置。
五、编程题
请写出一段代码,在子线程中创建一个Window,并在该window内动态添加一个可点击的View,实现自定义拖动。
(本题略,参考上文childThreadChangeUI(),强调WindowManager、Looper.prepare/loop、addView/updateViewLayout等关键点)