当前位置: 首页 > 科技观察

手机渲染原理解析_0

时间:2023-03-17 17:07:59 科技观察

作者|尚怀君电脑或手机的渲染是一个非常复杂的过程。本文介绍了渲染相关的一些基础知识,并结合iOS和Android的技术框架介绍了移动端渲染的原理,最后详细分析了iOS中的离屏渲染和一些圆角优化的方法。渲染基础屏幕绘制位图的原始数据源我们需要在屏幕上绘制图像的原始数据称为位图。位图(Bitmap)是一种数据结构。一张位图由n*m个像素组成,每个像素的颜色信息用RGB组合或灰度值表示。根据位深度的不同,位图可以分为1位、4位、8位、16位、24位和32位图像等。每个像素使用的信息位越多,可用的颜色越多,图像越逼真和丰富。色彩表现,相应的数据量越大。物理像素和逻辑像素位图一般存储物理像素,而应用层一般使用逻辑像素,物理像素和逻辑像素之间存在一定的对应关系。例如iOS中物理像素和逻辑像素的对应关系如下:iOS1双屏1pt对应1个物理像素iOS2双屏1pt对应2个物理像素iOS3双屏1pt对应3个物理像素绘制位图到display在屏幕上绘制图像所需的原始数据称为位图。那么问题来了,有了位图数据后,如何将图像绘制到屏幕上呢?如下图所示:电子枪从上到下逐行扫描,扫描完成后显示屏会呈现一帧画面。然后电子枪回到屏幕的初始位置进行下一次扫描。为了使监视器的显示过程与视频控制器的扫描过程同步,监视器产生一系列带有硬件时钟的定时信号。当电子枪换行扫描时,显示器会发出水平同步信号;绘制完一帧后,电子枪回到原位,在准备绘制下一帧之前,显示器会发出一个垂直同步信号。显示通常以固定频率刷新,这是生成垂直同步信号的频率。CPU、GPU、显示器协同工作流程上一节介绍了视频控制器在物理屏幕上显示位图数据的过程,那么如何获取位图数据呢?实际上,位图数据是通过CPU和GPU的协同工作得到的。下图是一个常见的CPU、GPU、显示器协同工作的过程。CPU计算显示内容,提交给GPU。GPU渲染完成后,渲染结果存储在帧缓冲区中。接下来,需要将获取到的像素信息显示在物理屏幕上。这时,视频控制器(VideoController)会读取帧缓冲区中的信息,传递给监视器(Monitor)进行显示。完整的流程如下图所示:CPU和GPU的区别说到CPU、GPU、显示器的协同工作流程,就不得不提到CPU和GPU的区别。CPU是中央处理器,适合单一的复杂逻辑,而GPU是图形处理单元,适合高并发的简单逻辑。GPU有特别多的计算单元和超长的流水线,但是控制逻辑很简单,而且还省了缓存,适合对延迟要求不高的运算。CPU不仅在Cache中占用了大量空间,而且控制逻辑也特别复杂。相比之下,计算能力只是CPU的一小部分。图形渲染涉及到很多矩阵运算,而矩阵相关的运算可以拆分成并行的简单运算,所以渲染处理特别适合GPU。总结一下:GPU的工作计算量大,但技术含量不高,需要简单重复多次。就好像有一份工作,需要一百以内的加减乘除数百次。CPU就像一个老教授,会计算积分和微分,适合处理单一的复杂逻辑运算。GeneralRenderingPipeline我们通常把图像绘制的完整过程称为渲染管线,它是由CPU和GPU共同完成的。一般来说,一个渲染过程可以分为四个概念阶段,即:应用阶段、几何阶段、光栅化阶段和像素处理阶段。在《Real–Time Rendering 4th》中,实时渲染的各种知识点讲解的非常透彻。如果你对渲染原理感兴趣,可以看看这本书。本书堪称“实时渲染的圣经”。下面将简要介绍这些过程。应用阶段(ApplicationStage)简称,就是应用程序中的图像处理阶段。说白了就是跑在CPU上的程序,此时没有GPU。这一阶段CPU主要负责处理用户交互和操作,然后做一些与应用层布局相关的处理,最后输出图元(点、线、三角形)信息到下一阶段。你可能会想,难道图元只有简单的点、线、三角形,能代表丰富的三维图形吗?下方立体感很强的海豚可以给出肯定的答案。简单的三角形加上不同的Coloring就可以呈现出立体的图形。几何阶段(GeometryStage)1.顶点着色器(VertexShader)顶点着色器可以对顶点的属性进行一些基本的处理。进行视角转换、添加光照信息、为顶点信息添加纹理等操作。CPU抛给GPU的信息,就好像是把这个角度看到的所有信息,都从上帝的角度给了GPU。GPU是从人的角度出发,在显示器上输出人可以观察到的图像。所以这里就是以人的视角为中心的坐标变换。2.ShapeAssembly(形状组装)该阶段将顶点着色器输出的所有顶点作为输入,将所有的点组装成指定图元的形状。基元,例如点、线和三角形。这个阶段也称为原始组装。3.GeometryShader在图元之外添加额外的顶点,并将原始图元转换为新的图元,以构建更复杂的模型。光栅化阶段(RasterizerStage)光栅化阶段将经过前三个几何阶段处理后得到的图元(primitives)转化为一系列的像素。如上图所示,我们可以看到每个像素点的中心都有一个点,光栅化就是以这个中心点来划分的。如果中心点在图元内部,则中心点对应的像素属于图元。简而言之,这个阶段就是将连续的几何图形转化为离散化的像素。像素处理阶段(PixelProcessing)1.片段着色器(FragmentShader)经过上面的光栅化阶段,我们得到了每个图元对应的像素,而这个阶段最后要做的就是正确填充每个像素的颜色,然后通过一系列的处理计算,得到相应的图像信息,最后输出到显示器上。插值在这里完成,就像补间动画一样。例如,如果要将一系列散乱的点连接成一条平滑的曲线,相邻的已知点之间可能会存在大量的缺失点。这时候就需要通过插值来填补缺失的数据,最后平滑曲线除了已知点之外的所有点都通过插值得到。同理,给出三角形的三个角色值后,其他片段根据插值计算,也呈现出渐变效果。2.TestsandBlending(测试和混合)这个阶段会检测对应的深度值(z坐标)来判断这个像素是在其他图层像素的前面还是后面,并决定是否应该丢弃它。此外,此阶段检查alpha值(alpha值定义像素的透明度)以混合图层。(简单来说就是检查图层深度和透明度,进行图层混合。)R=S+D*(1-Sa)含义:R:Result,最终的像素颜色。S:Source,源像素(上面的图层像素)。D:Destination,目标像素(下层像素)。a:alpha,透明度。Result=S(top)color+D(bottom)color*(1-S(top)transparency)经过上面那条长长的管道,我们就可以得到屏幕绘制需要的原始数据源——位图数据,然后显示视频控制器在物理屏幕上的位图数据。iOS渲染原理在渲染技术栈上铺垫了渲染的一些基础知识之后,下面主要介绍iOS渲染相关的一些原理和知识。下图展示了iOS的图形渲染技术栈。相关的核心系统框架有3个:CoreGraphics、CoreAnimation、CoreImage。这三个框架主要用于绘制可视化内容。它们都是使用OpenGL调用GPU进行实际渲染,然后生成最终的位图数据并存储在帧缓冲区中,视频控制器将帧缓冲区数据显示在物理屏幕上。UIKitUIKit是iOS开发者最常用的框架。您可以通过设置UIKit组件的布局和相关属性来绘制界面。然而,UIKit不具备在屏幕上显示图像的能力。该框架主要负责响应用户操作事件(UIView继承自UIResponder),事件通过响应链传递。CoreAnimationCoreAnimation主要负责将屏幕上不同的视觉内容进行组合。这些可视化内容可以分解成独立的层,也就是我们在日常开发过程中经常接触到的CALayer。这些图层存储在图层树中。CALayer主要负责页面渲染,它是用户在屏幕上看到的一切的基础。CoreGraphicsCoreGraphics主要用于运行时绘制图像。开发人员可以使用这个框架来处理基于路径的绘图、变换、颜色管理、离屏渲染、图案、渐变和阴影等。CoreImageCoreImage与CoreGraphics刚好相反,CoreGraphics在运行时创建图像,而CoreImage在运行前创建图像。OpenGLES和MetalOpenGLES和Metal都是第三方标准,基于这些标准的具体内部实现由相应的GPU厂商开发。Metal是一套来自Apple的第三方标准,由Apple实施。很多开发者从未直接使用过Metal,而是通过CoreAnimation、CoreImage等核心系统框架间接使用Metal。CoreAnimation与UIKit框架的关系上面渲染框架中提到的CoreAnimation是iOS和OSX上图形渲染和动画的基础框架,主要用于对应用程序的视图和其他视觉元素进行动画处理。CoreAnimation的实现逻辑是将大部分实际绘制工作交给GPU加速渲染,不会对CPU造成负担,实现流畅的动画效果。CoreAnimation的核心类是CALayer,UIKit框架的核心类是UIView。下面详细介绍这两个类的关系。UIView和CALayer的关系如上图所示。UIView和CALayer是一对一的关系。每个UIView都有一个CALayer与之对应。一个负责布局和交互响应,另一个负责页面渲染。它们的两个核心关系如下:CALayer是UIView的属性之一,负责渲染和动画,提供视觉内容的呈现。UIView提供了对CALayer功能的封装,负责处理交互事件。举个更形象的例子,UIView是画板,CALayer是画布。当你创建画板时,它会自动绑定一个画布,画板会响应你的操作。比如你可以移动画板,画布负责呈现具体的图形,两者职责明确。一个负责交互,一个负责渲染绘制。为什么要把CALayer和UIView分开?iOS平台和MacOS平台的用户交互方式根本不同,但渲染逻辑是通用的。在iOS系统中,我们使用的是UIKit和UIView,而在MacOS系统中,我们使用的是AppKit和NSView,所以这里在这种情况下,将显示部分的逻辑分离出来,跨平台复用。CALayer中的contents属性保存了设备渲染管线(通常称为backingstore)渲染出来的bitmap位图,也就是我们一开始提到的屏幕绘制最原始的数据源。当设备屏幕刷新时,生成的位图将从CALayer中读取并呈现在屏幕上。@interfaceCALayer:NSObject/**图层内容属性和方法。**//*提供图层内容的对象,通常是CGImageRef,*但也可能是其他对象。(例如,MacOSX10.6及更高版本支持NSImage对象。)默认值为nil。*动画。*/@property(nullable,strong)idcontents;@endCoreAnimationpipeline其实早在WWDC的AdvancedGraphicsandAnimationsforiOSApps(WWDC14419,aboutthesessionbasedonUIKitandCoreAnimation)就开始了,Apple给出了renderingpipelineCoreAnimation框架,具体流程如下图所示:app本身不负责整个pipeline中的渲染,渲染由一个独立的进程负责。即RenderServer进程。下面将详细介绍整个流水线流程。在应用阶段,视图创建布局计算将图层打包,在接下来的RunLoop中发送给RenderServerapp处理用户的点击操作。在此过程中,应用程序可能需要更新视图树。如果更新视图树,层树也将更新。它将被更新。其次,App通过CPU完成显示内容的计算。RenderServer&GPU阶段主要执行metal、CoreGraphics等相关程序,调用GPU完成图像在物理层的渲染。GPU将渲染的位图数据存储在FrameBufferDisplay中,视频控制器将帧缓冲区的位图数据逐帧显示在物理屏幕上。如果将以上步骤串起来,会发现它们的执行时间超过了16.67ms,所以为了满足屏幕对60FPS刷新率的支持,这些步骤需要通过流水线并行执行,如图下图。每个阶段都在不断地向下一阶段输送产品。此时可以满足在16.67毫秒内产生一帧数据的要求。Android渲染原理Android上层显示系统Android中Activity的一个重要职责就是管理界面的生命周期,伴随而来的是对视图窗口的管理。这里涉及到Android中的两个主要服务,AMS(ActivityManagerService)和WMS(WindowManagerService)。在Android中,一个视图有一个相应的画布。视图树对应一个canvas树,Surfaceflinger控制多个canvas的合成。最终渲染完成后,将输出的位图数据显示在手机屏幕上。应用层布局View和ViewGroupView是Android中所有控件的基类。View类有一个非常重要的子类:ViewGroup,作为其他视图的容器。所有的AndroidUI组件都是基于View和ViewGroup的,而View和ViewGroup的设计思路是“组合”:ViewGroup是View的子类,所以ViewGroup也可以作为View使用。AndroidApp的GUI对应一棵视图树,视图树对应一棵画布树。这有点类似于iOS中UIView和CALayer的概念,一个负责应用层的布局,一个负责底层渲染。系统底层渲染显示,应用层的view对应着canvas,canvas到了系统进程就变成了一层。SurfaceFlinger主要提供图层渲染和合成服务。SurfaceFlinger是一个常驻的binder服务,随着init进程的启动而启动。下图详细介绍了从上层视图到底层视图的转换,以及SurfaceFlinger对多个层的渲染合成。iOS离屏渲染离屏渲染原理及定义首先介绍一下离屏渲染的原理。我们正常的渲染流程是:CPU和GPU协同不断将内容渲染后得到的位图数据放入Framebuffer(帧缓冲区),视频控制器不断从Framebuffer中获取内容显示实时内容。离屏渲染的过程如下:与一般情况下GPU直接将渲染内容放入Framebuffer不同,离屏渲染需要先创建一个额外的离屏渲染缓冲区,将预渲染的内容放入进去。等到合适的时机再对OffscreenBuffer中的内容进行进一步叠加渲染,完成后将结果写入Framebuffer。为什么要先将数据存入离屏渲染缓冲区?有两种原因,一种是被动的,一种是主动的。有些特效出于效率的考虑,需要额外使用一个OffscreenBuffer来保存渲染的中间状态(被动),可以将内容提前渲染保存在OffscreenBuffer中,以达到复用的目的。(主动)被动离屏渲染触发被动离屏渲染的场景通常是透明、阴影和圆角通常被称为UI的三宝,但是这些效果在日常开发过程中往往会导致被动离屏渲染的iOS。下面是一些触发被动离屏渲染的常见场景。触发离屏渲染的原因说到离屏渲染的原因,就不得不提到画家的算法。画家算法的总体思路是逐层绘制。先绘制距离远的场景,再用绘制距离近的场景覆盖较远的部分。这里的图层可以映射到iOS渲染技术栈中的图层。通常对于每一层,RenderServer都会按照“画家算法”依次输出到framebuffer中,后一层覆盖前一层得到最终的显示结果。对于这个层树,它会使用深度优先算法将层输出到帧缓冲区。虽然作为“画家”的GPU可以逐层输出到画布上,但是渲染完某一层后,没有办法回头去改变其中的某一部分。因为在渲染的时候,这一层之前的几层像素数据已经合成在一起了。事实上,这与photoshop中的图层合并非常相似。一旦多个图层合并在一起,就不可能单独修改某个图层。所以需要在离屏缓冲区中逐层绘制子图层,然后将四个角切掉,再与上一层混合。GPU离屏渲染对性能的影响说到离屏渲染,我们直观的感觉是会影响性能。因为为了满足60fps的刷新率,GPU操作是高度流水线化的。本来所有的计算工作都是有条不紊的输出到framebuffer中。这时突然有一些特效触发了离屏渲染。有必要切换上下文并将数据输出到另一个内存。这时,流水线中的很多中间产品只能被丢弃,这种频繁的上下文切换对GPU的渲染性能影响非常大。如何防止不必要的离屏渲染?对于一些圆角,可以创建四个底色弧形图层覆盖四个角,视觉上营造出圆角效果。对于视图的圆形边框,如果没有backgroundColor,您可以安全地对所有阴影使用cornerRadius。使用shadowPath避免离屏渲染。对于特殊形状的视图,使用图层蒙版并启用shouldRasterize来缓存渲染结果。圆角优化策略使用CALayer的cornerRadius,并设置cliptobounds触发离屏渲染。裁剪操作需要在滚动时以每秒60帧的速度执行,即使内容没有任何变化。GPU还必须在每个帧之间切换上下文,合成整个帧并进行裁剪。这些性能的消耗直接影响到RenderServer的独立渲染过程,导致掉帧。为了优化渲染性能,我们可以选择一些其他的方案来实现圆角。下面是圆角的具体实现需要考虑的条件。圆角的具体实现需要考虑圆角下是否有滑动(movementunderthecorner)。拐角处是否有动静。四个圆角是否在同一层,是否与其他子层相交。如何根据相应的条件选择圆角的实现方案呢?圆角的优化需要考虑的条件和圆角的不同实现方案上面都提到了。下面的流程图是根据条件和解决方案,给出圆角的最佳实现。小结本文主要介绍移动端渲染原理的相关内容。文章首先介绍了渲染的基础知识,渲染所需的原始数据源——位图以及CPU和GPU如何协同工作获取位图数据。后面结合iOS和Android的技术框架介绍了移动端渲染的相关原理。最后对iOS离屏渲染进行了深入分析,并对现有的圆角优化的一些解决方案进行了说明。