什么是渲染
渲染是图形程序的核心,无论是我们在电子设备上看到的任何图形或者文字都是利用计算机图形渲染技术给我们呈现出来的结果。在计算机里一开始是直接利用CPU往显示器的FrameBuffer内写入数据即可把图形展示到显示器上,但是随着用户的需求和技术的发展,人们发明了一种专门用于计算数字图形渲染的硬件 — GPU。
- 黄色代表控制器(Control):用于协调控制整个CPU的运行,包括取指令等操作。
- 绿色的ALU(Arithmetic Logic Unit):算数逻辑单元,主要用于进行数学,逻辑计算。
- 橙色的Cache和DRAM分别为缓存和RAM,用于存储信息。
图中可以看到GPU的ALU很多,而CPU的ALU很少。所以GPU主要就用来做并行计算,图像计算正好就很符合这个场景。有人做过一个形象的比喻:
CPU就像是几个数学家,能够进行很多高难度的计算和多功能复杂的控制;而GPU就像是成千上万个小学生,基本只会做计算,但是其他方面的能力很弱。图形处理和渲染是简单的计算密集的工作,所以交给一大堆小学生去做比几个数学家做会快很多。
只需要利用图形API把指令发送给GPU驱动程序即可在短时间内实现极高复杂度的图形渲染结果。目前用得最广泛的API是OpenGL,其兼容性也是最好的。不过也有功能更强大,更高效的API,比如DirectX(Windows),Melta(MacOS/IOS),Vulkan(跨平台)等API供我们选择。
硬件渲染引擎的基本流程
GPU是需要通过CPU来驱动执行的,也就是说图像数据的组织和计算过程其实全都是在我们主控制程序里面需要保存的。所以一个基本的硬件渲染管线大致如下:
Application:
- 创建渲染循环
- 组织GPU渲染的指令和数据
- 把渲染指令和数据输入到GPU内进行渲染
GPU Pipeline:
- 先对传入的顶点进行几何操作
- 对屏幕内的顶点进行光栅化转为像素
- 逐个处理像素颜色
- 结束渲染,继续下一次渲染
在这个渲染流程里面Application部分都是在CPU内执行的,里面调用的绝大部分渲染相关的函数也都只是先放入到Buffer内,并不会真正的去执行,只有在flush之后才会让GPU去执行这些指令。如果看过Android应用渲染的同学应该知道一个叫做DisplayList
和Draw OP
的概念,每一个View在硬件加速开启的情况下都会生成一个DisplayList,这个DisplayList内就是一堆DrawOp的组合,这些DrawOp其实就是GPU渲染的指令了。然后通过有序的组织,每一个View在invalidate的时候只会重建自身的DisplayList或者某个DrawOP的属性,实现高效的渲染流程。
在提交任务给GPU之后,其实CPU的资源也就被释放出来了,不用一直等待GPU执行结束,这个时候我们完全可以准备下一次渲染所需的GPU Operation,这样就可以最大限度的提升渲染的流畅度。Android在5.x时代就加入了RenderThread的线程去专门执行OpenGL指令,在4.x时代的OpenGL指令都是在主线程执行的。
我这里只是粗略的提了下渲染的概念,其实【渲染/计算机图形学】是相当大的一个技术方向,涉及到很多计算机科学、线性代数、微积分、几何学、物理学、数字信号处理等知识。对于平面渲染还只是2D渲染,相对简单。在游戏或建模领域需要很深的数学和物理学功底才能深入。零基础的同学可以看看 LearnOpengl入门部分先了解一下图形编程。
什么是比较好的UI渲染系统
衡量UI渲染系统最重要的一点就是流畅度。这个是直接和用户的体验正相关的,很多人并不懂技术,但是就觉得iOS比Android好用很多时候就是因为iOS的UI比Android的UI在体验上更流畅得来的结论。
流畅度
流畅度的核心是帧率(fps):每一秒人们所看到的画面数。一般来说,对于电影或者录像来说,24fps我们就会觉得画面是流畅的,但是对于一套UI渲染系统来说,帧率需要达到60fps。原因是电影或者录像的每一帧其实是带有运动模糊的,所以能够欺骗人眼觉得画面的连续性更好。但是对于渲染出的画面是绝对高清,没有运动模糊,也就必须达到60fps才能够拥有流畅的用户体验。这里可以看这个视频了解更多:why 60fps。要实现60fps,我们每一秒的渲染时间必须控制在16.6ms以内,才能够让用户看起来画面是连续的。这个对系统的设计和硬件的性能是很大的挑战。工程师们在各个系统在实现UI模块的时候,可谓是费尽心思、尽其所能。比如Android的黄油计划,在渲染底层引入了vsync和3 buffer等技术。此外由于渲染计算本身是比较耗时的,但是如果用提前计算好的结果用于合成则会比较高效,所以一般在在应用内部的渲染系统会做分层和分块处理:纵向分层+横向分块。
- 所谓纵向分层,就是在Z轴方向上按层来划分UI,这样带来的好处在渲染UI的每一帧时,不必每一层都进行重绘。这种分层渲染策略使用到了一种称为“绘制-合成”的UI渲染技术。也就是各层负责绘制好自己的UI,然后再由一个单独的模块对它们进行合成。这样在渲染UI的每一个帧时,只有UI发生了变化的层才需要重新进行绘制,没有发生UI变化的层只需要参与合成这一步即可。
- 所谓横向分块,就是对于UI的每一个层,按照一定的规则对其进行分块,这样带来的好处就是在渲染UI的每一帧遇到一个需要进行重新绘制的层时,不必对该层的所有内容都进行重新绘制,只需要绘制那些在可视区间的块即可。这样也可以在某种程度上减少渲染操作,从而获得更流畅的UI体验。
比如下面这个页面结构,由两层组成,每一层内部又由多个块组成。如果一层内任意一个元素发生变化,只需要重新绘制发生变化的部分即可,不需要整个画面全都重新绘制,从而实现高效的绘制:
我们这里用Android支持硬件加速的的视图系统举个例:
- 每一个View都被抽象为一个RenderNode
- 所有的View被组合成为一个整体的DisplayList交给RenderThread渲染。
- 当视图需要改变的时候只需要更变View的RenderNode即可。
这种树形结构的ViewGroup层级我们就可以理解为纵向,而每一层的需要渲染的View就是每一层的Block了。
总结
上面的渲染技术属于比较通用的技术,无论什么系统,只要涉及到UI,或多或少都能看到这些技术的影子。好好的掌握了渲染技术,以后无论遇到什么问题或者学习什么新的系统都能够应对自如。千万不要把自己局限到某一个狭窄的框架内,先了解好通用技术,再去深入到具体技术对于自己的成长是最大的。