【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。

创建时机:onCreatesetContentView()仅仅把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期望的宽、高,得到mMeasuredWidthmMeasuredHeight

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

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():必须调用,把计算好的宽高保存到成员变量mMeasuredWidthmMeasuredHeight

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. 判断scrollXscrollY滚动偏移:

    • 如果没有滚动,直接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()     绘制前景、滚动条
相关推荐
_阿南_11 小时前
flutter 展示的字体突然奔放了
android·flutter·ios
文人sec14 小时前
MySQL事务与索引做了什么?
android·笔记·mysql
刘洋浪子15 小时前
AndroidStudio保存日志菜单
android
朱涛的自习室15 小时前
从 Prompt 到 Graph:AI 工程的进化史
android·前端·人工智能
1570925113417 小时前
ViewBinding与DataBinding区别与选型
android
小乖写代码19 小时前
49款开源Android小游戏
android·游戏
三少爷的鞋19 小时前
小团队的最优解:单 Module + 包内 Clean 架构
android
爱和冰阔落19 小时前
【Linux】两个毫无关系的进程怎么通信?命名管道 FIFO 从原理到 Server/Client 实战
android·linux·数据库·c++·vim
Android-Flutter19 小时前
kotlin 协程 - 核心概念
android·kotlin
年小个大1 天前
受 go-zero 启发,我给 Flutter 写了一套 CLI 脚手架
android·flutter·架构