【Android FrameWork】第二十八天:Activity 的 UI 绘制全过程

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的调用最终会转发到PhoneWindowsetContentView方法,核心逻辑是 "初始化 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 树" 的关键,核心流程如下:

  1. 资源解析 :通过Resources加载 XML 布局文件,解析 XML 标签(如<TextView><LinearLayout>)。

  2. View 创建 :根据标签名通过反射创建对应的 View 实例(如解析创建TextView对象),同时解析XML中的属性(如layout_widthtextSize)并设置到View的LayoutParams` 中。

  3. 层级构建 :若遇到 ViewGroup(如LinearLayout),递归解析其内部子标签,将子 View 添加到 ViewGroup 中,最终形成以开发者布局根 View 为顶层的 "View 树"。

  4. 注入 ContentParent :将生成的 View 树作为子 View 添加到mContentParent(DecorView 的内容区),此时 View 树已挂载到 DecorView 上,但尚未开始绘制。

绘制触发

View 树挂载到 DecorView 后,并不会立即绘制 ------真正触发 UI 绘制的是 ViewRootImpl,它是 "Activity 的 View 树" 与 "底层 Surface/WindowManager" 之间的关键桥梁,负责发起绘制请求并执行绘制流程。

1. ViewRootImpl 的创建时机

ViewRootImpl 的创建发生在 Activity 的onResume()生命周期之后,由WindowManager(窗口管理器)触发,核心流程如下:

  1. Activity 启动至onResume()时,系统会调用ActivityThreadhandleResumeActivity()方法。

  2. 在该方法中,通过wm.addView(decorView, layoutParams)将 DecorView 交给WindowManager管理。

  3. 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_parent100dp
AT_MOST 子 View 尺寸不超过参考尺寸(如wrap_content),子 View 需根据自身内容计算大小 wrap_content
UNSPECIFIED 父 View 不限制子 View 尺寸,子 View 可自由设置大小(如ScrollView的子 View) ScrollView子 View、ListView Item
(2)Measure 的递归执行流程
  1. 根 View(DecorView)测量ViewRootImpl.performMeasure()会调用DecorView.measure(),并传递基于屏幕尺寸的MeasureSpec(如屏幕宽度 1080px,模式 EXACTLY)。

  2. ViewGroup 测量子 View :ViewGroup(如 LinearLayout)在onMeasure()中,会根据自身的布局规则(如水平排列、垂直排列),为每个子 View 计算对应的MeasureSpec,再调用childView.measure(childMeasureSpecWidth, childMeasureSpecHeight)

  3. View 自身测量 :普通 View(如 TextView)在onMeasure()中,会根据父 View 传递的MeasureSpec和自身内容(如文字长度、图片尺寸),计算measuredWidthmeasuredHeight,并通过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);
}
  1. 递归终止:当所有子 View 都完成测量后,Measure 流程结束,View 树的所有 View 都已确定测量尺寸。

2. 第二步:Layout(布局)------ 确定 View 的位置

核心目标 :通过递归计算,确定每个 View 在父 View 中的left(左边界)、top(上边界)、right(右边界)、bottom(下边界)坐标,最终确定 View 在屏幕中的绝对位置。

(1)Layout 的递归执行流程
  1. 根 View(DecorView)布局ViewRootImpl.performLayout()调用DecorView.layout(0, 0, screenWidth, screenHeight),将 DecorView 的位置设置为全屏(左 0、上 0、右屏幕宽、下屏幕高)。

  2. 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();
       }
   }
}
  1. View 自身布局 :普通 View 的layout()方法会保存父 View 传递的坐标,并调用onLayout()(空实现,可重写),完成自身位置的确定。

  2. 关键注意点GONE状态的 View 会跳过 Layout 流程(不占用空间),而INVISIBLE状态的 View 会参与 Layout(占用空间但不绘制)。

3. 第三步:Draw(绘制)------ 将 View 渲染为像素

核心目标:通过 Canvas(画布)将 View 的内容(背景、文字、图片、自定义图形)绘制为像素数据,最终传递给 Surface 进行显示。

(1)Draw 的执行顺序(从里到外)

View 的onDraw()方法遵循固定的绘制顺序,确保内容层级正确(后绘制的内容会覆盖先绘制的内容):

  1. 绘制背景 :调用drawBackground(canvas),绘制 View 的background(如android:background设置的颜色或 Drawable)。

  2. 绘制自身内容 :调用onDraw(canvas),这是开发者自定义绘制的核心方法(如 TextView 绘制文字、ImageView 绘制图片)。

  3. 绘制子 View :调用dispatchDraw(canvas)(ViewGroup 重写此方法),递归绘制所有子 View(子 View 的 Draw 流程与父 View 一致)。

  4. 绘制前景 :调用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 绘制是一套 "从载体关联到像素显示" 的完整链路,可总结为以下步骤:

  1. 载体初始化:Activity 创建时初始化 PhoneWindow,PhoneWindow 创建 DecorView 并确定 ContentParent。

  2. 布局加载setContentView()通过 LayoutInflater 解析 XML,生成 View 树并注入 ContentParent。

  3. 绘制触发 :ActivityonResume()后,WindowManager 创建 ViewRootImpl,ViewRootImpl 调用performTraversals()

  4. 三大流程

  • Measure:递归计算每个 View 的测量尺寸。

  • Layout:递归确定每个 View 的屏幕坐标。

  • Draw:递归通过 Canvas 绘制 View 内容,生成像素数据。

  1. 显示输出:像素数据写入 Surface,由 SurfaceFlinger 合成后显示到屏幕。

理解这一流程,不仅能帮助开发者排查 UI 卡顿、布局错乱等问题,更能为自定义 View、性能优化提供底层理论支撑。

相关推荐
_李小白7 小时前
【Android FrameWork】第三十天:Surface创建流程解析
android
元亓亓亓7 小时前
考研408--操作系统--day8--操作系统--虚拟内存&请求分页&页面置换/分配
android·java·开发语言·虚拟内存
有位神秘人7 小时前
Android的Compose系列之文本TextView
android
Engineer-Jsp7 小时前
Flutter 开发 Android 原生开发神器 flutter_api_stub
android·flutter
qq_205279058 小时前
unity 像素ui的适配问题
ui
惟恋惜8 小时前
Jetpack Compose 多页面架构实战:从 Splash 到底部导航,每个 Tab 拥有独立 ViewModel
android·ui·架构·android jetpack
ab_dg_dp8 小时前
Android bugreportz 源码分析
android
木风小助理8 小时前
如何破解 MySQL 死锁?核心原则与实操方法
android