渲染机制(一):Android渲染机制的演进

一、背景

常见的渲染问题:画面撕裂、丢帧、卡顿。

画面撕裂的问题:

当刷新率和帧率各做各的事,且刷新频率和帧率不同步时,如果公用同一帧 就会容易出现画面撕裂(Tearing的现象,也就是画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠。

Project Butter (黄油计划):

为了解决 Android 系统中的滑动不流畅的问题,谷歌在 4.1 版本实施了一个Project Butter(黄油计划)。即对 Android Display 系统进行了重构,引入了三个核心元素,即 VSYNCTriple BufferChoreographer

关联文章:

二、概念

在介绍Android的渲染机制之前,我们先来了解几个概念。

  1. 帧率 (Frame Rate): 表示一秒内GPU绘制操作的帧数,例如 30fps,60fps。
  2. 刷新率 (Refresh Rate): 表示一秒内屏幕刷新的次数,这取决于硬件的固定参数,例如 60Hz。
  3. Display: 主要作用是将Android的XML布局文件转换成GPU能够识别并绘制的对象,Display持有所有将要交给GPU绘制到屏幕上的数据信息。
  4. CPU: 中央处理器,主要参与App应用层渲染的3个流程(测量、布局、绘制)。
  5. GPU (Graphics Processing Unit): 主要负责栅格化(Rasterization)操作,栅格化是一个非常耗时的工作。栅格化是指将向量图形格式表示的图像转换成位图(像素)以用于显示设备输出的过程。简单来说就是将我们要显示的视图,转换成用像素来表示的格式。

在这里插入图片描述

  1. 画面撕裂(Tearing): GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。当刷新频率与帧率不同步,且公用一个数据缓冲区时,就容易让相邻两帧的数据被渲染在同一帧画面上(Tearing)。
  2. Jank现象:连续两帧显示同一张图。

下面我们分别介绍一下 VSYNC、Triple Buffer 和 Choreographer 的作用。


三、VSYNC信号

VSYNC(Vertical Synchronization):VSYNC 信号是由屏幕(显示设备)产生的,其目的是为了让显卡的帧率和屏幕的刷新率保持相同,从而避免出现画面撕裂等问题。

3.1 没有VSync信号的渲染过程

下面我们来看下没有VSYNC信号之前的渲染过程(如下图所示):

在这里插入图片描述

上图中涉及3个元素:Display、GPU、CPU。

  • Display:表示显示的屏幕,上图中的 0,1,2,3…i 表示当前处于第 i 帧画面。
  • GPU & CPU:负责渲染帧数据,上图中蓝/绿色方框的数字表示当前CPU&GPU渲染第i帧。

具体步骤:

上图总共涉及5个16.6ms的渲染窗口。

  1. 第1个窗口: 此时Display显示第0帧,CPU&GPU 完成第1帧数据的渲染,所以在第2个窗口时,Display第1帧可以正常显示。
  2. 第2个窗口: 此时Display显示第1帧,但由于 CPU 被占用等原因,所以未能在第2个窗口内完成第2帧数据的渲染。因此在第3个窗口内只能继续显示上一帧(Display第1帧)的内容,这种情况就是所谓的丢帧。
  3. 第3个窗口: 此时Display继续显示第1帧画面。同时 CPU&GPU 正常完成第2帧数据的渲染,CPU并开始了第3帧数据的处理。因此在第4个窗口时,能正常显示在Display第2帧画面。
  4. 第4个窗口: 此时Display显示第2帧,CPU&GPU完成第3帧的渲染。
  5. 第5个窗口: 此时Display显示第3帧,CPU&GPU完成第4帧的渲染。

丢帧的原因:

从图中看出,丢帧是因为在新的一帧开始时,CPU 在处理其他任务,并没有马上执行下一帧的任务。

解决方案:

引入VSync信号来解决在新的一帧开始时,CPU无法马上进行工作的问题。

下面我们来看下引入 VSync信号后,渲染过程有什么变化。

3.2 引入VSync信号的渲染过程

下面我们来看下没有VSYNC信号之前的渲染过程(如下图所示):
在这里插入图片描述

具体步骤:

上图总共涉及5个16.6ms的渲染窗口。

  1. 第1个窗口: 此时Display显示第0帧,CPU&GPU 完成第1帧数据的渲染,所以在第2个窗口时,Display第1帧可以正常显示。
  2. 第2个窗口: 此时Display显示第1帧,并且CPU在收到VSync信号后,马上进行第2帧数据的渲染处理。
  3. 第3个窗口: 此时Display显示第2帧画面。并且CPU在收到VSync信号后,马上进行第3帧数据的渲染处理。
  4. 第4个窗口: 此时Display显示第3帧画面。并且CPU在收到VSync信号后,马上进行第4帧数据的渲染处理。
  5. 第5个窗口: 此时Display显示第4帧画面。并且CPU在收到VSync信号后,马上进行第5帧数据的渲染处理。

小结:

渲染层帧率 >= Display 层帧率:

  • 上图中 CPU 和 GPU 处理数据的速度都能在 16ms 内完成,也就是说 CPU 和 GPU 的帧率要高于 Display 的帧率。所以相当于是通过 Vsync 信号来降低渲染层(CPU&GPU)的帧率,来平衡渲染层与Display层的帧率。

渲染层帧率 < Display 层帧率:

  • 如果 CPU/GPU 的帧率小于 Display 的帧率,情况又不同了,容易引发Jank的问题。

四、三重缓存机制(Triple Buffer)

4.1 二重缓存机制

在引入三重缓存机制之前,一直使用二重缓存机制,Jank产生的过程如下图所示。

在这里插入图片描述

上图中涉及3个元素:Display、GPU、CPU。

  • Display:表示显示的屏幕,上图中的 A、B 表示当前使用的缓存对象。
  • GPU & CPU:负责渲染帧数据,上图中蓝/绿色方框的数字表示当前CPU&GPU正在渲染哪个缓存的数据。

注意:

一旦过了 VSync 时间点,如果没有可用的缓存,CPU 就不能被触发以处理绘制工作了。

具体步骤:

上图总共涉及5个16.6ms的渲染窗口。

  1. 第1个窗口: 此时Display显示由A缓存渲染的帧画面,CPU&GPU 正在渲染B缓存的帧画面,但由于在第1个窗口内,CPU&GPU无法完成数据的渲染,因此再第2个窗口时,Display仍显示A缓存的帧画面,这也就产生了一次Jank。
  2. 第2个窗口: 此时Display仍显示由A缓存渲染的帧画面,GPU执行完B缓存的渲染任务。但由于只有两个缓存的原因,所以CPU&GPU在接收到了VSync信号时,由于没有新的缓存可以使用,因此即使GPU很快完成了缓存B的渲染任务,但 CPU&GPU 也不会再执行下一帧的任务了。
  3. 第3个窗口: 此时Display显示由B缓存渲染的帧画面。并且CPU&GPU在收到VSync信号后,马上进行下一帧(A缓存)的渲染,但是没有渲染完成。因此再第4个窗口时,Display仍显示B缓存的帧画面,这又产生了一次Jank。
  4. 第4个窗口: 此时Display仍显示由B缓存渲染的帧画面,GPU执行完A缓存的渲染任务,等待下一个窗口的使用。
  5. 第5个窗口: 此时Display显示由A缓存渲染的帧画面。

双重缓存机制产生的问题:

当 CPU/GPU 的帧率小于 Display 的帧率时,如果使用双重缓存机制,容易产生大量的Jank问题,那么又如何来解决呢?

解决方案:

Android 引入了 Triple Buffer 机制。

4.2 三重缓存机制

在这里插入图片描述

具体步骤:

上图总共涉及5个16.6ms的渲染窗口。

  1. 第1个窗口: 此时Display显示由A缓存渲染的帧画面,CPU&GPU 正在渲染B缓存的帧画面,但由于在第1个窗口内,CPU&GPU无法完成数据的渲染,因此再第2个窗口时,Display仍显示A缓存的帧画面,这也就产生了一次Jank。
  2. 第2个窗口: 此时Display仍显示由A缓存渲染的帧画面,GPU执行完B缓存的渲染任务。此时因为是三重缓存,所以CPU&GPU在接收到了VSync信号时,会获取到C缓存然后执行下一帧的任务了。
  3. 第3个窗口: 此时Display显示由B缓存渲染的帧画面,同时GPU继续渲染C缓存的任务。当CPU&GPU在收到VSync信号后,获取A缓存进行下一帧的渲染。
  4. 第4个窗口: 此时Display显示由C缓存渲染的帧画面,当CPU&GPU在收到VSync信号后,获取B缓存进行下一帧的渲染。
  5. 第5个窗口: 此时Display显示由A缓存渲染的帧画面。

小结:

  1. 通过引入Vsync信号解决画面撕裂的问题。
  2. 通过引入三重缓存来优化降低Jank问题(当渲染帧率低于Display帧率时出现Jank问题)。

五、Choreographer

  1. Choreographer 是线程单例的,而且必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和 Looper 绑定,一般是 App 主线程的 Looper 绑定

  2. DisplayEventReceiver 是一个 abstract class,其 JNI 的代码部分会创建一个IDisplayEventConnection 的 Vsync 监听者对象。这样,来自 AppEventThread 的 VSYNC 中断信号就可以传递给 Choreographer 对象了。当 Vsync 信号到来时,DisplayEventReceiver 的 onVsync 函数将被调用。

  3. DisplayEventReceiver 还有一个 scheduleVsync 函数。当应用需要绘制UI时,将首先申请一次 Vsync 中断,然后再在中断处理的 onVsync 函数去进行绘制。

  4. Choreographer 定义了一个 FrameCallback interface,每当 Vsync 到来时,其 doFrame 函数将被调用。这个接口对 Android Animation 的实现起了很大的帮助作用。以前都是自己控制时间,现在终于有了固定的时间中断。

  5. Choreographer 的主要功能是,当收到 Vsync 信号时,去调用使用者通过 postCallback 设置的回调函数。目前一共定义了五种类型的回调,它们分别是:

    • CALLBACK_INPUT : 处理输入事件处理有关
    • CALLBACK_ANIMATION : 处理 Animation 的处理有关
    • CALLBACK_INSETS_ANIMATION : 处理 Insets Animation 的相关回调
    • CALLBACK_TRAVERSAL : 处理和 UI 等控件绘制有关
    • CALLBACK_COMMIT : 处理 Commit 相关回调,主要是是用于执行组件 Application/Activity/Service 的 onTrimMemory,在 ApplicationThread 的 scheduleTrimMemory 方法中向 Choreographer 插入的;另外这个 Callback 也提供了一个监测一帧耗时的时机。

说明: 第一步初始化完成后,后续就会在步骤 2-9 之间循环。


六、总结(App 绘制一帧到屏幕显示的流程)

  • step1: 当UI发生改变需要重绘时,最终会触发App渲染层的 DisplayEventReceiver.scheduleVsync() 方法向 EventThread(APP) 线程发起 Vsync-app信号的请求。
  • step2: 当下一个 Vsync-app 信号到达EventThread时,最终会传递给在主线程执行的 Choreographer.doFrame() 方法,在 doFrame() 方法中执行 measure、layout、draw (构建 DisplayList , 里面包含 OpenGL 渲染需要的命令及数据) 等流程。这部分操作在CPU中进行。
  • step3: 接下来是将CPU处理的数据交给GPU进行处理。CPU 通过共享或者拷贝的方式将数据传递给 GPU,这里 ARM设备 内存一般是 GPU 和 CPU 共享内存。
  • step4: CPU将数据传递给GPU后会通知 GPU 进行渲染,此时 CPU 在通知完GPU后就会返回继续执行其他任务 (这里使用 Fence 机制辅助 GPU、CPU 进行同步操作)。
  • step5: 当GPU渲染完成之后,会执行 swapBuffers,与 SurfaceFlinger 进程交换数据,并通知 SurfaceFlinger 图层合成。
  • step6: SurfaceFlinger 在收到App渲染层通知后,会向 EventThread(Surface) 线程发起 Vsync-sf 信号的请求。当收到 Vsync-sf 信号时,就开始合成图层。如果之前提交的 GPU 渲染任务还没结束,则等待 GPU 渲染完成,再合成 (Fence 机制),在合成图层时依然是依赖 GPU,不过这就是下一个任务了。
  • step7: 最终合成好的数据放到屏幕对应的 Frame Buffer 中,屏幕固定刷新的时候就可以看到了。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
浏览器的渲染机制可以分为以下几个步骤: 1. 构建 DOM 树:根据 HTML 文本构建 DOM 树,即将 HTML 文本解析成一个个的节点,然后将这些节点按照其在文档中的层级关系组成一棵树。 2. 构建 CSSOM 树:根据 CSS 文本构建 CSSOM 树,即将 CSS 文本解析成一个个的样式规则,然后将这些样式规则按照其在文档中的层级关系组成一棵树。 3. 合并生成渲染树:将 DOM 树和 CSSOM 树组合成渲染树(也称为呈现树或布局树)。渲染树只包含需要显示的节点和这些节点的样式信息。渲染树的构建过程中,浏览器会忽略掉那些不需要显示的节点,例如 head 标签、display:none 的节点等。 4. 计算布局信息:根据渲染树计算每个节点在屏幕上的位置和大小,这个过程被称为布局或排版(layout 或 reflow)。布局是一个相当昂贵的操作,因为浏览器需要遍历渲染树的每个节点,并根据节点的样式、大小、位置等信息计算其在屏幕上的位置和大小。 5. 绘制页面:根据渲染树和布局信息,将页面绘制到屏幕上,这个过程被称为绘制(painting 或 rasterization)。绘制的过程中,浏览器会将渲染树中每个节点的内容转换成位图,然后将这些位图组合成一张完整的页面。 6. 提交更新:将绘制好的页面提交到 GPU,由 GPU 将其显示到屏幕上。 以上是浏览器渲染机制的主要流程,其中布局(layout 或 reflow)是整个渲染过程中最耗时的步骤,因此在开发中需要尽可能减少布局的次数,以提高页面的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值