【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、性能优化提供底层理论支撑。

相关推荐
阿巴斯甜19 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker20 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952721 小时前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android