
Activity 的 UI 绘制全过程
Activity 作为 Android 应用的核心组件,其 UI 绘制是用户与应用交互的基础。
不同于 Surface 的底层渲染载体角色,Activity 的 UI 绘制是一套 "从布局解析到像素渲染" 的上层 View 系统工作流,涉及 Window 管理、View 树构建、三大核心绘制流程(Measure/Layout/Draw)等关键环节。
本文将以 Android 12 + 源码为参考,从底层关联到上层执行,详细拆解 Activity UI 绘制的完整链路。
前置基础
在分析绘制流程前,需先明确 Activity 与 UI 载体的底层关系 ------Activity 本身不直接承载 UI,而是通过 Window(窗口)和 DecorView(根视图)实现 UI 的容器功能,三者的关联是 UI 绘制的前提。
1. Window:Activity 的 UI "容器外壳"
- 本质 :Window 是 Android 系统提供的 "抽象窗口载体",每个 Activity 会在
attach()阶段(Activity 创建时由系统调用)初始化一个PhoneWindow(Window 的唯一实现类),代码逻辑如下:
java
// Activity.java 源码片段
final void attach(Context context, ...) {
// 初始化PhoneWindow,作为Activity的窗口实例
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this); // Activity作为Window的回调(如处理点击事件)
// ...
}
- 核心作用:提供 UI 绘制的 "顶层容器",管理标题栏、内容区等系统级 UI 元素,同时作为 ViewRootImpl 与 Activity 的中间桥梁。
2. DecorView:Window 的 "根视图"
-
本质 :DecorView 是
FrameLayout的子类,是 Window 的 "根 View",所有 Activity 的布局(如setContentView加载的布局)最终都会作为子 View 添加到 DecorView 的 "内容区容器" 中。 -
结构组成 :DecorView 的内部结构由系统布局文件(如
screen_simple.xml)定义,核心分为两部分:-
标题栏(ActionBar/Toolbar) :对应布局中的
com.android.internal.widget.ActionBarContainer,可通过主题配置隐藏(如Theme.NoActionBar)。 -
内容区(ContentParent) :对应布局中的
FrameLayout(id 为android.R.id``.content),是setContentView加载布局的 "目标容器"。
-
3. 关联流程总结
Activity → PhoneWindow → DecorView → ContentParent → 开发者布局(如 R.layout.activity_main),形成 "Activity - 窗口 - 根视图 - 自定义布局" 的四层 UI 载体结构,为后续绘制奠定基础。
布局加载
当开发者在 Activity 的onCreate()中调用setContentView(``R.layout.xxx``)时,触发的是 "XML 布局解析→View 树创建→注入 DecorView" 的核心流程,这是 UI 绘制的 "准备阶段"。
1. 第一步:PhoneWindow 处理布局请求
setContentView的调用最终会转发到PhoneWindow的setContentView方法,核心逻辑是 "初始化 DecorView→找到 ContentParent→加载布局并注入":
java
// PhoneWindow.java 源码片段
@Override
public void setContentView(int layoutResID) {
// 1. 若DecorView未初始化,先创建DecorView并加载系统布局(如screen\_simple.xml)
if (mContentParent == null) {
installDecor(); // 关键:初始化DecorView和ContentParent
}
// 2. 清除ContentParent中已有的子View(避免重复加载)
mContentParent.removeAllViews();
// 3. 解析开发者布局XML,生成View树并添加到ContentParent
LayoutInflater.from(mContext).inflate(layoutResID, mContentParent);
// 4. 通知Activity布局已变更(如触发onContentChanged回调)
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null) {
cb.onContentChanged();
}
}
2. 第二步:installDecor () 初始化 DecorView 与 ContentParent
installDecor()是 Window 初始化 DecorView 的核心方法,主要完成两件事:
-
创建 DecorView :通过
generateDecor()创建 DecorView 实例,并将 Window 的回调(如点击、按键事件)设置给 DecorView。 -
绑定系统布局与 ContentParent :通过
generateLayout(mDecor)加载系统布局(根据主题选择不同布局,如带标题栏或无标题栏),并从系统布局中找到android.R.id``.content对应的FrameLayout(即 ContentParent),赋值给mContentParent。
3. 第三步:LayoutInflater 解析 XML 生成 View 树
LayoutInflater.from(mContext).inflate(...)是 "XML 转 View 树" 的关键,核心流程如下:
-
资源解析 :通过
Resources加载 XML 布局文件,解析 XML 标签(如<TextView>、<LinearLayout>)。 -
View 创建 :根据标签名通过
反射创建对应的 View 实例(如解析创建TextView对象),同时解析XML中的属性(如layout_width、textSize)并设置到View的LayoutParams` 中。 -
层级构建 :若遇到 ViewGroup(如
LinearLayout),递归解析其内部子标签,将子 View 添加到 ViewGroup 中,最终形成以开发者布局根 View 为顶层的 "View 树"。 -
注入 ContentParent :将生成的 View 树作为子 View 添加到
mContentParent(DecorView 的内容区),此时 View 树已挂载到 DecorView 上,但尚未开始绘制。
绘制触发
View 树挂载到 DecorView 后,并不会立即绘制 ------真正触发 UI 绘制的是 ViewRootImpl,它是 "Activity 的 View 树" 与 "底层 Surface/WindowManager" 之间的关键桥梁,负责发起绘制请求并执行绘制流程。
1. ViewRootImpl 的创建时机
ViewRootImpl 的创建发生在 Activity 的onResume()生命周期之后,由WindowManager(窗口管理器)触发,核心流程如下:
-
Activity 启动至
onResume()时,系统会调用ActivityThread的handleResumeActivity()方法。 -
在该方法中,通过
wm.addView(decorView, layoutParams)将 DecorView 交给WindowManager管理。 -
WindowManager的实现类WindowManagerImpl会创建ViewRootImpl实例,并将 DecorView 和WindowManager.LayoutParams(如窗口大小、位置)传递给 ViewRootImpl:
java
// WindowManagerImpl.java 源码片段
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// ...
root = new ViewRootImpl(view.getContext(), display); // 创建ViewRootImpl
view.setLayoutParams(wparams); // 设置窗口参数
// 将DecorView与ViewRootImpl关联
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// 触发ViewRootImpl的绘制准备
root.setView(view, wparams, panelParentView);
}
2. ViewRootImpl 触发绘制:performTraversals ()
ViewRootImpl.setView()方法会调用requestLayout(),最终触发核心方法performTraversals()------ 这是 View 树绘制的 "总开关",负责统筹执行Measure(测量)、Layout(布局)、Draw(绘制) 三大流程:
java
// ViewRootImpl.java 源码片段
private void performTraversals() {
// 1. 检查是否需要重新测量/布局/绘制(如布局参数变化、View树更新)
boolean needLayout = mLayoutRequested;
if (needLayout) {
// 2. 执行测量流程:确定View树中每个View的大小
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 3. 执行布局流程:确定View树中每个View的位置
performLayout(lp, mWidth, mHeight);
// 4. 执行绘制流程:将View树渲染为像素数据
performDraw();
}
- 关键逻辑 :
performTraversals()会先计算MeasureSpec(测量规格,决定 View 的测量模式和最大尺寸),再依次触发三大流程,且每个流程都是 "从根 View(DecorView)到子 View" 的递归执行。
核心流程拆解
Activity UI 绘制的核心是 View 树的 "Measure→Layout→Draw" 三步递归流程,每个步骤都有明确的职责和执行逻辑,直接决定 UI 的最终显示效果。
1. 第一步:Measure(测量)------ 确定 View 的大小
核心目标 :通过递归计算,确定每个 View 的measuredWidth(测量宽度)和measuredHeight(测量高度),为后续布局提供尺寸依据。
(1)MeasureSpec:测量的 "规则说明书"
MeasureSpec是父 View 传递给子 View 的 "测量规则",由 32 位整数表示(高 2 位为测量模式 ,低 30 位为参考尺寸),共三种模式:
| 测量模式 | 含义 | 适用场景 |
|---|---|---|
| EXACTLY | 子 View 尺寸固定(如match_parent或具体数值100dp),父 View 已确定子 View 的精确大小 |
match_parent、100dp |
| AT_MOST | 子 View 尺寸不超过参考尺寸(如wrap_content),子 View 需根据自身内容计算大小 |
wrap_content |
| UNSPECIFIED | 父 View 不限制子 View 尺寸,子 View 可自由设置大小(如ScrollView的子 View) |
ScrollView子 View、ListView Item |
(2)Measure 的递归执行流程
-
根 View(DecorView)测量 :
ViewRootImpl.performMeasure()会调用DecorView.measure(),并传递基于屏幕尺寸的MeasureSpec(如屏幕宽度 1080px,模式 EXACTLY)。 -
ViewGroup 测量子 View :ViewGroup(如 LinearLayout)在
onMeasure()中,会根据自身的布局规则(如水平排列、垂直排列),为每个子 View 计算对应的MeasureSpec,再调用childView.measure(childMeasureSpecWidth, childMeasureSpecHeight)。 -
View 自身测量 :普通 View(如 TextView)在
onMeasure()中,会根据父 View 传递的MeasureSpec和自身内容(如文字长度、图片尺寸),计算measuredWidth和measuredHeight,并通过setMeasuredDimension()保存结果:
java
// TextView.java 源码片段(简化)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 计算文字宽度和高度(根据textSize、text内容)
int textWidth = measureTextWidth();
int textHeight = measureTextHeight();
// 2. 根据MeasureSpec模式确定最终测量尺寸
int width = getDefaultSize(textWidth, widthMeasureSpec);
int height = getDefaultSize(textHeight, heightMeasureSpec);
// 3. 保存测量结果(必须调用,否则报错)
setMeasuredDimension(width, height);
}
- 递归终止:当所有子 View 都完成测量后,Measure 流程结束,View 树的所有 View 都已确定测量尺寸。
2. 第二步:Layout(布局)------ 确定 View 的位置
核心目标 :通过递归计算,确定每个 View 在父 View 中的left(左边界)、top(上边界)、right(右边界)、bottom(下边界)坐标,最终确定 View 在屏幕中的绝对位置。
(1)Layout 的递归执行流程
-
根 View(DecorView)布局 :
ViewRootImpl.performLayout()调用DecorView.layout(0, 0, screenWidth, screenHeight),将 DecorView 的位置设置为全屏(左 0、上 0、右屏幕宽、下屏幕高)。 -
ViewGroup 布局子 View :ViewGroup 在
onLayout()中,会根据自身的布局规则(如 LinearLayout 的gravity、RelativeLayout 的alignParentRight),为每个子 View 计算坐标:
- 以 LinearLayout(垂直排列)为例:
java
// LinearLayout.java 源码片段(简化)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTop = t; // 子View的顶部起始位置(从父View的顶部开始)
for (int i = 0; i (); i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) { // GONE的View不参与布局
// 1. 获取子View的测量尺寸
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 2. 计算子View的坐标(左:父View左边界,右:左+子View宽,上:childTop,下:childTop+子View高)
int childLeft = l;
int childRight = childLeft + childWidth;
int childBottom = childTop + childHeight;
// 3. 调用子View的layout()方法,设置其坐标
child.layout(childLeft, childTop, childRight, childBottom);
// 4. 更新下一个子View的顶部起始位置(垂直排列,向下偏移)
childTop = childBottom + getPaddingVertical();
}
}
}
-
View 自身布局 :普通 View 的
layout()方法会保存父 View 传递的坐标,并调用onLayout()(空实现,可重写),完成自身位置的确定。 -
关键注意点 :
GONE状态的 View 会跳过 Layout 流程(不占用空间),而INVISIBLE状态的 View 会参与 Layout(占用空间但不绘制)。
3. 第三步:Draw(绘制)------ 将 View 渲染为像素
核心目标:通过 Canvas(画布)将 View 的内容(背景、文字、图片、自定义图形)绘制为像素数据,最终传递给 Surface 进行显示。
(1)Draw 的执行顺序(从里到外)
View 的onDraw()方法遵循固定的绘制顺序,确保内容层级正确(后绘制的内容会覆盖先绘制的内容):
-
绘制背景 :调用
drawBackground(canvas),绘制 View 的background(如android:background设置的颜色或 Drawable)。 -
绘制自身内容 :调用
onDraw(canvas),这是开发者自定义绘制的核心方法(如 TextView 绘制文字、ImageView 绘制图片)。 -
绘制子 View :调用
dispatchDraw(canvas)(ViewGroup 重写此方法),递归绘制所有子 View(子 View 的 Draw 流程与父 View 一致)。 -
绘制前景 :调用
onDrawForeground(canvas),绘制 View 的前景(如foreground属性、滚动条)。
(2)Canvas 与 Paint:绘制的 "工具集"
-
Canvas :提供绘制各种图形的 API(如
drawRect()画矩形、drawText()画文字、drawBitmap()画图片),本质是 "像素操作的封装",其绘制结果最终会写入 Surface 关联的图形缓冲区。 -
Paint:控制绘制的样式(如颜色、字体大小、线条宽度、透明度),是 Canvas 的 "画笔"。
(3)示例:TextView 的绘制逻辑
java
// TextView.java 源码片段(简化)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // 先执行父类View的draw逻辑(如背景)
// 1. 绘制文字(核心逻辑)
String text = getText().toString();
Paint textPaint = getPaint();
textPaint.setColor(getCurrentTextColor());
textPaint.setTextSize(getTextSize());
// 计算文字绘制的起始位置(根据gravity等属性)
int x = calculateTextX();
int y = calculateTextY();
// 绘制文字到Canvas
canvas.drawText(text, x, y, textPaint);
// 2. 绘制下划线、删除线等(若有)
if (isUnderlineText()) {
drawUnderline(canvas, textPaint, x, y);
}
}
(4)绘制结果的传递
Draw 流程结束后,Canvas 中的像素数据会被写入 ViewRootImpl 关联的Surface(与前文讲解的 Surface 关联),再由SurfaceFlinger将多个 Surface(如 Activity 的 Surface、状态栏的 Surface)合成,最终输出到屏幕。
特殊场景与绘制优化
1. 绘制触发的重新执行
除首次绘制外,以下场景会触发requestLayout()或invalidate(),重新执行绘制流程:
-
requestLayout():触发 Measure→Layout→Draw(如 View 的layout_width修改、View 树结构变化)。 -
invalidate():仅触发 Draw(如 TextView 的文字修改、View 的背景色修改),不重新测量和布局,性能更优。 -
postInvalidate():在子线程中触发invalidate()(invalidate()仅能在 UI 线程调用)。
2. 硬件加速对绘制的影响
Android 4.0 + 默认开启硬件加速,其核心是将 Draw 流程的部分操作交由 GPU 执行,提升绘制效率:
-
原理 :硬件加速下,View 的绘制会生成
DisplayList(绘制命令列表),GPU 直接执行DisplayList而非 CPU 逐像素绘制,减少 CPU 负载。 -
注意点 :部分 Canvas API(如
clipPath()、drawPicture())在硬件加速下存在兼容性问题,需通过setLayerType(View.LAYER_TYPE_SOFTWARE, null)关闭硬件加速。
3. 绘制优化技巧
-
减少绘制层级 :避免嵌套过深的 ViewGroup(如
LinearLayout嵌套LinearLayout),推荐使用ConstraintLayout减少层级,降低递归绘制的开销。 -
避免过度绘制 :通过 "去掉不必要的背景""使用
merge标签减少根 View""开启 GPU 过度绘制检测(开发者选项)" 优化,避免同一像素被多次绘制。 -
复用 View:在列表(如 RecyclerView)中复用 Item View,避免频繁创建和销毁 View 导致的重复绘制。
总结
Activity 的 UI 绘制是一套 "从载体关联到像素显示" 的完整链路,可总结为以下步骤:
-
载体初始化:Activity 创建时初始化 PhoneWindow,PhoneWindow 创建 DecorView 并确定 ContentParent。
-
布局加载 :
setContentView()通过 LayoutInflater 解析 XML,生成 View 树并注入 ContentParent。 -
绘制触发 :Activity
onResume()后,WindowManager 创建 ViewRootImpl,ViewRootImpl 调用performTraversals()。 -
三大流程:
-
Measure:递归计算每个 View 的测量尺寸。
-
Layout:递归确定每个 View 的屏幕坐标。
-
Draw:递归通过 Canvas 绘制 View 内容,生成像素数据。
- 显示输出:像素数据写入 Surface,由 SurfaceFlinger 合成后显示到屏幕。
理解这一流程,不仅能帮助开发者排查 UI 卡顿、布局错乱等问题,更能为自定义 View、性能优化提供底层理论支撑。
