二十一、View绘制的核心流程

概述

上一章节,我们从 setContentView为起点,阅读源码,熟悉了 view,Activity和Window的关系。

大概可以总结如下:

  • ActivitysetContentView实际上走到了WindowsetContentView
  • Window有唯一的实现类PhoneWindow,在该类中,利用ViewRootImpl实现了 创建我们指定的 Layout(包括测量布局绘制),以及将该Layout挂载到DecorView作为它的child
  • 其中涉及到了一个重要概念WindowManagerService(简称为WMS,系统级别的窗口管理服务),这个WMS负责将我们的布局绘制到屏幕上。
  • 最后,如果屏幕上的布局要与用户手势发生交互,得手机屏幕的硬件层面先和 AndroidFramework层进行socket交互,然后WMS得知了事件,最后通过binder,告知了当前的app进程,并将事件通过事件链表,传递给了 当前ActivityDecorView
  • 最后执行了 我们很熟悉的 View.dispatchTouchEvent将事件进行一步步下发。

重点关注ViewRootImpl,它 作为一个普通类,没有继承 View,Activity,Window的任何一个,但是却联通了 View,Activity,Window三者。在布局的渲染中起到了以下两个关键作用。

  1. 调用 requestLayout方法,执行了View的测量布局绘制,即View的完整渲染流程。
  2. 通过binder机制,将View添加到Window

ViewRootImpl的作用下,我们的layout才显示在屏幕上。

下面,详细关注以下ViewRootImpl的源代码:

屏幕绘制

其中 requestLayout 就是在执行View的渲染流程。

具体过程如下:

    1. 检查线程,其实就是检查是否为主线程,如果不是,就抛出异常。
    1. 将请求布局的参数 mLayoutRequestd 设置为 true。这个参数决定后续是否要执行 measure,layout等操作。
    1. 执行 scheduleTraversals()
    • 上图1处,利用handler,发送了一个 syncBarrier消息到queue中。在loopernext获取消息时,如果发现没有targetmessage,则在一定时间内跳过同步消息,优先执行异步消息。这里调用此方法,保证 UI绘制优先执行。
    • 上图2处,执行一个 postCallback方法到 主线程的消息队列,发送到消息为:mTraversalRunnable,具体如下:
      • 图中红框标记处,发送了一个异步类型的消息到主线程消息队列中。
      • mTraversalRunnable的具体源代码为:
        • 就是在这个Runnable中,执行了 最为关键的 performTraversals(), 这个方法中,执行了 View的完整渲染流程 measure->layout->draw.

ViewRootImpl 的 performTraversals()

源代码精简如下: 实际上也就对应了 measure->layout->draw

measureHierarchy

View的测量是一个递归过程。递归执行了所有子view的测量过程之后,最终决定了父的宽高。 那么递归的起源是哪里呢?猜到了吧,就是DecorView。 我们Activity上的最高级别的View,就是DecorView

请看源代码:

红框内为 获取根部的MeasureSpec,这里的 lp.widthlp.height, 其实就是屏幕的宽高(Activity默认全屏) 。然后将 获得到的 两个MeasureSpec交给了 performMeasure().

上图中的 mView 就是DecorView, mView.measure()就是 递归测量的起点。

performLayout

也是一个递归调用。结论与 measure的过程类似。都是 DecorView作为起点的 递归layout。

performDraw

图中1处,表示 开启了硬件加速,则使用 硬件渲染器来进行绘制。

图中2处,执行的是软件绘制。

ViewRootImpl 中有一个重要的元素:surface。它的核心功能就是 UI渲染。 ViewRootImpl会将 draw 方法中绘制的UI元素,绑定到 surface上。 如果说 Canvas是画版,那么 surface就是画布上的画纸。 Surface上的内容最终会传递给底层的 SurfaceFlinger 。 最终将surface中的内容合成并显示在 屏幕上。

软件绘制 drawSoftware

图中1处,就是执行了 DecorView的draw方法,将元素绘制到了 canvas上。 图中2处,unlock,其实就是将 surface上的内容提交给 SurfaceFlinger显示在屏幕上。

软件绘制和硬件绘制的区别是:

  • 软件绘制 没有采用GPU,而全部是CPU完成绘制
  • 硬件绘制,完全采用GPU

mView.draw()

  • 图中1处,绘制背景
  • 图-中2处,绘制自身内容
  • 图中3处,对draw事件进行分发,递归执行子view的 draw事件。

关于硬件加速

硬件加速可以指定Application维度,Activity维度

或者 利用java代码设置到View维度。

之所以有这么多的维度划分,是因为并不是所有的2D绘制操作都支持硬件加速。比如如下draw方法:

使用了这些方法的自定义View,如果启用硬件加速,会导致程序运行不正常。

但是硬件加速能够提高 UI渲染的性能。 以下是 ViewRootImpl的draw方法的部分代码:

硬件绘制的核心逻辑在 mThreadRender.draw方法中,源代码如下:

View的动态绘制

当我们自定义一个View,需要它根据不同情况做出不同的动画效果时,那么一定离不开Viewinvalidate方法. 它是一个轻量级UI刷新方式。 因为它大多数情况下只会执行 viewdraw方法,而不会执行 measurelayout

invalidate不会执行measure的原因是: 要执行measure,必须设置 FLAG_FORCE_LAYOUT标志位。而 invalidate 没有设置这个标志位,所以不会执行。

invalidate不会执行layout的原因是:

当调用invalidate时,如果View的位置没有发生改变,那么View不会触发重新布局的操作。

如果要执行 measurelayout,那么就应该执行 View.requestLayout().

invalidate 和 postInvalidate 的区别

前者只能在UI线程中使用,后者则可以在 子线程中使用。

postInvalidate的源码如下:

其实就是利用 handler发送了一个 MSG_INVALIDATE 到消息队列中。然后在 ActivityThread 的 mHandler中执行 view的 invalidate()

ViewRootImpl的 dispatchInvalidateDelay()

View的刷新操作 只能在UI线程,是因为有一个 checkThread,将当前线程与创建ViewRootImpl时的线程比较。 由于创建时,是在 UI线程,所以刷新操作也只能在 UI线程。

理论上,如果能在子线程中创建ViewRootImpl,那么也是能够在这个子线程中进行UI刷新操作的。

总结

  • 本文介绍了ViewRootImpl是如何进行 View渲染的,其核心方法在 performTraversals,按顺序执行 measure-layout-draw
  • 介绍了 软件绘制,硬件加速的区别
  • 介绍了 view的两种刷新方式 invalidate / postInvalidate
相关推荐
测试19983 小时前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
马剑威(威哥爱编程)4 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
独行soc6 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
理想不理想v6 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
sszmvb12347 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺7 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉7 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船7 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
ZL不懂前端8 小时前
Content Security Policy (CSP)
前端·javascript·面试
测试界萧萧8 小时前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展