【Android】View的工作流程

前言

DecorView

他是一个真正的View(FrameLayout子类),整个页面最顶层的布局。

我们写的setContentView(R.layout.xxx),只是把写的xml布局塞到了DecorView内部的一块内容区域里面。

DecorView还包含状态栏、标题栏这些系统UI。

ViewRootImpl

它不是View,并不继承于View,只是一个调度控制器:

  • 每一个Activity窗口,就对应一个ViewRootImpI对象。
  • 手里持有DecorView的引用。
  • 负责接收屏幕刷新信号(VSync),驱动整棵View树执行measure测量→layout布局→draw绘制。
  • 触摸事件也是它分发给各个View。

创建时机:onCreate的setContentView()仅仅把xml解析成一堆View对象,此时ViewRootImpI还没有被创建,屏幕还什么都没画。

等到onResume()之后,系统把DecorView交给WindowManager,这时才new出ViewRootImpI,开始工作。

Canvas画布

Canvas就是绘图画板。

系统提前准备好一块画板,传给各个View。所有绘制操作draw图形、draw文字、draw图片,全部在这块画板上。

画板全部画完之后,系统把整块画板的内容渲染到手机屏幕。

当启动一个Activity时,ActivityThread.performLaunchActivity()创建了Activity实例,调用onCreate()方法,在onCreate()中我们调用了setContentView(),此时仅仅只是把布局xml解析成View树,但是View还没有执行measure、layout、draw,屏幕上什么都还没有画出来,那他是怎么画出UI的呢?我们接下来来看一下。

整个View树的调度入口是ViewRootImpl.performTraversals() 。

而performTraversals()就是整个View体系的总调度方法,内部执行三大流程:

  1. performMeasure()测量流程
  2. performLayout()布局流程
  3. performDraw()绘制流程

三大流程全部是递归执行:从根View(DecorView)开始,一层一层遍历所有子View。

Measure测量过程

作用:测量每一个View期望的宽、高,得到mMeasuredWidth、mMeasuredHeight。

注意:测量出来的只是期望尺寸,不一定等于最终屏幕显示尺寸

ViewRootImpI#performMeasure

java 复制代码
private void performMeasure(int childWidthMeasureSpec, int heightMeasureSpec) {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

mView就是DecorView(整个窗口的根布局),从根开始发起测量

需要注意的是,在performMeasure方法中需要传入两个参数childWidthMeasureSpec、heightMeasureSpec;要了解这两个参数,需要先了解MeasureSpec。

MeasureSpec 规则

MeasureSpec是View的内部类,封装了View的尺寸规格,代表了32位的int值:高2位代表mode模式(specMode,测量模式),低30位代表size尺寸(specSize,测量大小)。每一个View都持有一个MeasureSpec;specMode有三种模式:

  • EXACTLY:对应match_parent、具体dp数值。父容器已经确定的View的大小,View直接使用size。
  • AT_MOST:对应wrap_content。View最大不能超过size,View自己计算合适的大小。
  • UNSPECIFIED:父容器不做限制,子View可以任意大小,多用于ScrollView。

View#measure()

java 复制代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //处理缓存、是否需要重新测量标记
    onMeasure(widthMeasureSpec, heightMeasureSpec);
}
  • measure()被final修饰,子类不能重写
  • measure是对外的入口,真正留给我们自定义的是onMeasure()

View#onMeasure()

java 复制代码
protected void omMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
  1. getSuggestedMinimumWidth():返回View最小宽度,来源于android:minWidth或者drawable背景的最小宽度。
  2. getDefaultSize():根据父容器传入的MeasureSpec模式,计算出View尺寸。
  3. setMeasuredDimension():必须调用,把计算好的宽高保存到成员变量mMeasuredWidth,mMeasuredHeight。

ViewGroup的测量逻辑

ViewGroup没有提供统一的onMeasure实现,不同子类(LinearLayout、RelativeLayout)各自重写。

ViewGroup测量两件事:

  1. 遍历所有子View,调用child.measure(childWidthSpec, childHeightSpec),让子View完成测量,得到子View的measuredWidth/measuredHeight。
  2. 根据所有子View的测量结果,计算ViewGroup自身需要多大,调用setMeasuredDimension保存自己的测量宽高。

小结:

  • View:onMeasure -> setMeasuredDimension,确定自己大小
  • ViewGroup:先全部子View measure,再计算自己大小。

Layout布局过程

作用:确定View在父容器当中的位置,给View赋值mLeft、mTop、mRight、mBottom四个坐标。

measure只管大小,layout管放在哪里。

ViewRootImpI#performLayout

java 复制代码
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    mView.layout(0, 0, desiredWindowWidth, desiredWindowHeight);
}

从DecorView开始递归布局

View#layout

java 复制代码
public final void layout(int l, int t, int r, int b) {
    boolean changed = setFrame(l, t, r, b);
    if(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
    }
}
  1. layout同样是final,不能重写。
  2. setFrame(l, t, r, b):核心方法,把传入的左上右下四个坐标保存给成员变量mLeft、mTop、mRight、mBottom,标记这个View在父容器中的矩形位置。返回值changed代表位置是否发生改变。
  3. 如果位置发生变化,调用onLayout()。

View#onLayout

java 复制代码
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
}
  • 普通View:onLayout是空实现,没有子View,不需要摆放子元素。
  • ViewGroup必须重写onLayout :循环遍历子View,调用child.layout(l, t, r, b),把计算好的子View坐标传递过去,完成子View的位置摆放。

重要区分

  • getMeasuredWidth():measure阶段得到的测量宽高
  • getWidth():return right-left,layout之后,由left、right计算出来的实际显示宽度。

两者大多数情况相等,但layout阶段可以人为修改坐标,二者可以不相等。

scrollX、scrollY:不会修改left、top、right、bottom这四个坐标,仅仅是绘制阶段画布偏移,只影响draw流程

Draw绘制过程

作用:把View绘制到屏幕画布Canvas上。

入口:ViewRootImpl.performDraw (),触发DecorView的draw方法,递归绘制整棵View树。

两个易混淆的的方法

  1. boolean draw(Canvas canvas, ViewGroup parent, long drawingTime):绘制总入口;
  2. protected void onDraw(Canvas canvas):空实现,自定义View重写,绘制自己的内容。

draw方法内部完整6步流程

  1. 绘制背景drawBackground();
  2. 保存画布(可选,可跳过);
  3. 绘制View自身内容onDraw();
  4. 绘制子View dispatchDraw();
  5. 绘制褪色边缘阴影效果(可选,可跳过);
  6. 绘制前景装饰 onDrawForefround();

绘制顺序规则:后绘制的内容,会覆盖先绘制的内容。

1. drawBackground 绘制背景

java 复制代码
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    setBackgroundBounds();

    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}
  1. mBackground对应xml中android:background设置的Drawable。

  2. setBackgroundBounds()设置背景Drawable的绘制边界。

  3. 判断scrollX、scrollY滚动偏移:

    • 如果没有滚动,直接background(canvas)绘制背景
    • 如果存在滚动,先canvas.translate(scrollX,scrollY)平移画布,绘制背景,绘制完成再把画布平移复原。

    scrollX/scrollY 在这里生效,仅仅改变画布,不会改变 View 的位置坐标。

3. onDraw()绘制View自身内容

java 复制代码
protected void onDraw(Canvas canvas) {
}

系统是空实现,由我们自定义View重写,在这里绘制图形、文字。

ViewGroup默认开启WILL_NOT_DRAW标记,标记自己不需要绘制自身,onDraw不会被回调。

如果希望ViewGroup绘制自己,调用setWillNotDraw(false)关闭标记。

4. dispatchDraw()绘制子View

java 复制代码
protected void dispatchDraw(Canvas canvas) {
}
  • 普通View:空实现,没有子View,什么都不执行。
  • ViewGroup重写dispatchDraw,遍历所有子View:
java 复制代码
// ViewGroup.dispatchDraw核心逻辑
for (int i = 0; i < childrenCount; i++) {
    View child = getChildAt(i);
    drawChild(canvas, child, drawingTime);
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

drawChild内部调用dhild.draw(),开启递归,每一个子View完整执行整套draw六步流程:绘制背景→onDraw→dispatchDraw→前景。

递归链条:

java 复制代码
ViewGroup.draw()
    → drawBackground()
    → onDraw()
    → dispatchDraw()
        → drawChild(child1) → child1.draw()【子View完整6步】
        → drawChild(child2) → child2.draw()【子View完整6步】
    → onDrawForeground()

6. onDrawForeground()绘制前景装饰

java 复制代码
public void onDrawForeground(Canvas canvas) {
    onDrawScrollIndicators(canvas);
    onDrawScrollBars(canvas);
    //绘制foreground前景Drawable
}

负责绘制:android:foreground前景Drawable、滚动条、滚动指示器。

这一步在绘制流程的最后,绘制内容会覆盖背景、自身内容、所有子View,显示在最顶层。


整体流程

java 复制代码
ViewRootImpl.performTraversals()
├─ performMeasure() 测量阶段
│   └─ DecorView.measure()
│       └─ onMeasure()
│           └─ ViewGroup循环调用child.measure(),递归测量全部子View
├─ performLayout() 布局阶段
│   └─ DecorView.layout()
│       └─ onLayout()
│           └─ ViewGroup循环调用child.layout(),递归摆放全部子View
└─ performDraw() 绘制阶段
    └─ DecorView.draw(canvas)
        ├─ drawBackground()       绘制背景
        ├─ onDraw()               绘制自己的内容
        ├─ dispatchDraw()         遍历子View
        │   └─ child.draw() 每一个子View完整执行draw全部6步(递归)
        └─ onDrawForeground()     绘制前景、滚动条
相关推荐
海水冷却5 小时前
Android 群组聊天怎么实现:基于 ZIM 的完整落地指南(附可运行 Demo)
android
三少爷的鞋8 小时前
现代 Android 架构指南:不要再主动设计 Callback 接口
android
TimeFine14 小时前
AI 编码工作流的设计
android·ai编程
骑着蜗牛撵大象32714 小时前
C++20 协程 × 屏障:手写一个 co_await 版 Barrier,让等待不再钉死线程
android·c++20·协程·并发编程·co_await·屏障同步
山东孙燕姿16 小时前
设备台账重构:从一张表到三层模型
android·java·网络·低代码·重构·rxjava
蒸鱼Yuzheng17 小时前
移动端 CPU 调度分析:线程亲和性、大小核迁移与卡顿
android·perfetto·性能分析·线程优化·cpu调度
booboosui17 小时前
安卓唤醒词:12 维 MFCC 加 DTW,阈值 1.8
android
传奇开心果17 小时前
Compose Multiplatform 共享边界实战:架构契约、互操作同步与可回退设计
android·kotlin
事圆则缓17 小时前
从开机到首帧:Android 系统与应用启动流程源码分析
android
AI吃大瓜18 小时前
指针压力表读数4:Android实现指针压力表读数(可实时检测)
android·指针读数·指针压力表·压力表·压力表读数