百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

移动端渲染原理浅析(渲染动画是什么意思)

myzbx 2025-01-31 14:14 24 浏览

计算机或手机的渲染是一个非常复杂的过程,本文介绍了渲染相关的一些基础知识,并结合 iOS 和安卓的技术框架介绍了移动端渲染原理,最后详细的解析了 iOS 中的离屏渲染以及圆角优化的一些方法。

渲染基础知识

屏幕绘制的原始数据源

位图

我们在屏幕上绘制图像需要的原始数据叫做位图。位图(Bitmap) 是一种数据结构。一个位图是由 n*m 个像素组成,每个像素的颜色信息由 RGB 组合或者灰度值表示。根据位深度,可将位图分为 1、4、8、16、24 及 32 位图像等。每个像素使用的信息位数越多,可用的颜色就越多,颜色表现就越逼真,越丰富,相应的数据量越大。

物理像素和逻辑像素

位图一般存储的是物理像素,而应用层一般用的是逻辑像素,物理像素和逻辑像素之间会存在一定的对应关系。例如,iOS 中物理像素和逻辑像素的对应关系如下:

  1. iOS1 倍屏 1pt 对应 1 个物理像素
  2. iOS2 倍屏 1pt 对应 2 个物理像素
  3. iOS3 倍屏 1pt 对应 3 个物理像素

将位图绘制到显示器

上边讲了屏幕上绘制图像需要的原始数据叫做位图。那么问题来了,有了位图数据之后如何将图像绘制到屏幕上呢?如下图所示:电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到屏幕初始位置进行下一次扫描。为了同步显示器的显示过程和视频控制器的扫描过程,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号;当一帧画面绘制完成后,电子枪回到原位,准备画下一帧前,显示器会发出一个垂直同步信号。显示器通常以固定的频率进行刷新,这个刷新率就是垂直同步信号产生的频率。

CPU、GPU、显示器协同工作流程

前一部分介绍了视频控制器将位图数据显示到物理屏幕上的过程,那么位图数据是怎么得到的呢?其实位图数据是通过 CPU、GPU 协同工作得到的。下图就是常见的 CPU、GPU、显示器协同工作的流程。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,接下来就需要将得到的像素信息显示在物理屏幕上了,这时候视频控制器(Video Controller)会读取帧缓冲器中的信息传递给显示器(Monitor)进行显示。完整的流程如下图所示:

CPU 和 GPU 的区别

讲到 CPU、GPU、显示器的协同工作流程,就不得不提一下 CPU 和 GPU 的区别。

CPU是中央处理器,适合单一复杂逻辑,而GPU是图形处理器,适合高并发简单逻辑。

GPU 有特别多的的计算单元和超长的流水线,但是控制逻辑非常简单,并且还省去了缓存,适合对低延迟要求不高的运算。而 CPU 不仅被 Cache 占据了大量空间,而且还有特别复杂的控制逻辑,相比之下计算能力只是 CPU 很小的一部分。图形渲染涉及到的矩阵运算比较多,矩阵相关的运算可以被拆分成并行的简单的运算,所以渲染处理这件事特别适合 GPU 去做。

总结来说:GPU 的工作计算量大,但技术含量不高,需要简单重复很多次。就好比有个工作需要算成百上千次一百以内加减乘除一样。而 CPU 就像老教授,积分微分都会算,适合处理单一复杂逻辑运算。

通用渲染流水线

我们通常将图像绘制的完整流程称为渲染流水线,这个过程是 CPU 和 GPU 协作完成的。一般一个渲染流程可以分成 4 个概念阶段,分别是:应用阶段(Application Stage),几何阶段(Geometry Stage),光栅化阶段(Rasterizer Stage),像素处理阶段(Pixel Processing)。在《Real–Time Rendering 4th》中非常透彻的讲解了实时渲染的各种知识点,对渲染原理感兴趣的可以看看这本书,这本书堪称“实时渲染圣经”。下边会简单介绍一下这几个过程。

应用阶段(Application Stage)

简而言之,就是图像在应用中的处理阶段。说白了就是一段运行在 CPU 上的程序,这时还没有 GPU 什么事。这一阶段主要是 CPU 负责处理用户的交互和操作,然后做一些应用层布局相关的处理,最后输出图元(点、线和三角形)信息给到下一阶段。

大家可能会疑惑 ,图元只有简单的点、线、三角形,能表示丰富的立体图形么,下边这张立体感很强的海豚就能给出肯定的答案了,简单的三角形再加上不同的着色,就能呈现出立体图形。

几何阶段(Geometry Stage)

1. 顶点着色器(Vertex Shader)

顶点着色器可以对顶点的属性进行一些基本的处理。将顶点信息进行视角转换、添加光照信息、增加纹理等操作。CPU 丢给 GPU 的信息,就好像是站在上帝视角把这个视角看到的所有信息都给到 GPU。而 GPU 则是站在人类的角度,将人类可以观察到的画面,输出在显示器上。所以这里是以人的视角为中心,进行坐标转换。

2. 形状装配(Shape Assembly)这个阶段是将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图元(Primitive)如:点、线、三角形。这个阶段也叫图元装配

3. 几何着色器(Geometry Shader)在图元外添加额外的顶点,将原始图元转换成新图元,来构建更加复杂的模型。

光栅化阶段(Rasterizer Stage)

光栅化阶段会把前三个几何阶段处理后得到的图元(primitives)转换成一系列的像素。

如上图所示,我们可以看到,每个像素的中心有一个点,光栅化便是用这个中心点来进行划分的,如果中心点在图元内部,那么这个中心点所对应的像素就属于该图元。简而言之,这一阶段就是将连续的几何图形转化为了离散化的像素点。

像素处理阶段(Pixel Processing)

1. 片段着色器(Fragment Shader)

通过上述的光栅化阶段之后,我们就拿到了各个图元对应的像素,最后这个阶段要做的事情就是给每个 Pixel 填充上正确的颜色,然后通过一系列处理计算,得到相应的图像信息,最终输出到显示器上。这里会做内插,就像补间动画一样。比如想要把一系列散点连成平滑曲线,相邻的已知点之间可能会缺少很多点,这时候就需要通过内插填补缺少的数据,最终平滑曲线上除已知点之外的所有点都是插值得到的。同样的,三角形的三个角色值给定后,其它的片段则根据插值计算出来,也就呈现来渐变的效果。

2. 测试与混合(Tests and Blending)

这个阶段会检测对应的深度值(z 坐标),来判断这个像素位于其它图层像素的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个像素的透明度),从而对图层进行混合。( 一句话简单说,就是检查图层深度和透明度,并进行图层混合。)

R = S + D * (1 - Sa)

含义:
R:Result,最终像素颜色。
S:Source,来源像素(上面的图层像素)。
D:Destination,目标像素(下面的图层像素)。
a:alpha,透明度。

结果 = S(上)的颜色 + D(下)的颜色 * (1 - S(上)的透明度)

经历了上边漫长的流水线之后我们便可以拿到屏幕绘制所需要的原始数据源-位图数据,然后由视频控制器把位图数据显示在物理屏幕上。

iOS 渲染原理

渲染技术栈

上边铺垫完渲染相关的一些基础知识之后,下面主要介绍 iOS 渲染相关的一些原理和知识。下图是 iOS 的图形渲染技术栈,有三个相关的核心系统框架:Core GraphicsCore AnimationCore Image ,这三个框架主要用来绘制可视化内容。他们都是通过 OpenGL 来调用 GPU 进行实际的渲染,然后生成最终位图数据存储到帧缓冲区,视频控制器再将帧缓冲区的数据显示物理屏幕上。

UIKit

UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit 组件的布局以及相关属性来绘制界面。但是 UIKit 并不具备在屏幕成像的能力,这个框架主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件经过响应链传递。

Core Animation

Core Animation 主要负责组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层也就是我们日常开发过程中常接触的 CALayer,这些图层被存储在图层树中。CALayer 主要负责页面渲染,它是用户能在屏幕上看见的一切的基础。

Core Graphics

Core Graphics 主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影等等。

Core Image

Core Image 与 Core Graphics 正好相反,Core Graphics 是在运行时创建图像,而 Core Image 则是在运行前创建图像。

OpenGL ES 和 Metal

OpenGL ES 和 Metal 都是第三方标准,基于这些标准具体的内部实现是由对应的 GPU 厂商开发的。Metal 是苹果的一套第三方标准,由苹果实现。很多开发者都没有直接使用过 Metal,但却通过 Core Animation、Core Image 这些核心的系统框架在间接的使用 metal。

CoreAnimation 与 UIKit 框架的关系

上边渲染框架中提到的 Core Animation 是 iOS 和 OS X 上图形渲染和动画的基础框架,主要用来给视图和应用程序的其他可视元素设置动画。Core Animation 的实现逻辑是将大部分实际绘图的工作交给 GPU 加速渲染,这样不会给 CPU 带来负担,还能实现流畅的动画。CoreAnimation 的核心类是 CALayer,UIKit 框架的核心类是 UIView,下边详细介绍一下这两个类的关系。

UIView 与 CALayer 的关系

如上图所示,UIView 和 CALayer 是一一对应的关系,每一个 UIView 都有一个 CALayer 与之对应,一个负责布局、交互响应,一个负责页面渲染。

他们的两个核心关系如下:

  1. CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
  2. UIView 提供了对 CALayer 功能的封装,负责了交互事件的处理。

举一个形象一点的例子,UIView 是画板,CALayer 就是画布,当你创建一个画板的时候,会自动绑定一个画布,画板会响应你的操作,比如你可以移动画板,画布则负责呈现具体的图形,二者职责分明。一个负责交互,一个负责渲染绘制。

为什么要分离出 CALayer 和 UIView?

iOS 平台和 MacOS 平台上用户的交互方式有着本质的不同,但是渲染逻辑是通用的,在 iOS 系统中我们使用的是 UIKit 和 UIView,而在 MacOS 系统中我们使用的是 AppKit 和 NSView,所以在这种情况下将展示部分的逻辑分离出来跨平台复用。

CALayer 中的 contents 属性保存了由设备渲染流水线渲染好的位图 bitmap(通常被称为 backing store),也就是我们最开始说的屏幕绘制需要的最原始的数据源。而当设备屏幕进行刷新时,会从 CALayer 中读取生成好的 bitmap,进而呈现到屏幕上。

@interface CALayer : NSObject 
/** Layer content properties and methods. **/

/* An object providing the contents of the layer, typically a CGImageRef,
 * but may be something else. (For example, NSImage objects are
 * supported on Mac OS X 10.6 and later.) Default value is nil.
 * Animatable. */

@property(nullable, strong) id contents;

@end

Core Animation 流水线

其实早在 WWDC 的 Advanced Graphics and Animations for iOS Apps(WWDC14 419,关于 UIKit 和 Core Animation 基础的 session)中苹果就给出了 CoreAnimation 框架的渲染流水线,具体流程如下图所示:

整个流水线中 app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。下边会详细介绍一下整个 pipeline 的流程。

  • 应用阶段
    • 视图的创建
    • 布局计算
    • 对图层进行打包,在下一次 RunLoop 时将其发送至 Render Server
    • app 处理用户的点击操作,在这个过程中 app 可能需要更新视图树,如果视图树发生更新,图层树也会被更新
    • 其次,app 通过 CPU 完成对显示内容的计算
  • Render Server & GPU
    • 这一阶段主要执行 metal、Core Graphics 等相关程序,并调用 GPU 在物理层上完成对图像的渲染
    • GPU 将渲染后的位图数据存储到 Frame Buffer
  • Display
    • 视频控制器将帧缓冲区的位图数据一帧一帧的显示在物理屏幕上

如果把把上边的步骤串在一起,会发现它们执行消耗的时间超过了 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要通过流水线的方式将这些步骤并行执行,如下图所示。每一个阶段都在源源不断的给下一个阶段输送产物。这时候就可以满足 16.67 毫秒产生一帧数据的要求了。

安卓渲染原理

安卓上层显示系统

安卓中 Activity 的一个重要的职责就是对界面生命周期的管理,这也就伴随了对视图窗口的管理。这中间就涉及了两个 Android 中两个主要的服务,AMS(ActivityManagerService)WMS(WindowManagerService)

在 Android 中,一个 view 会有对应的 canvas。视图树对应一个 canvas 树,Surfaceflinger 控制多个 canvas 的合成。最终渲染完成输出位图数据,显示到手机屏幕。

应用层布局

View 和 ViewGroup

View 是 Android 中所有控件的基类,View 类有一个很重要的子类:ViewGroup,ViewGroup 作为其他 view 的容器使用。Android 的所有 UI 组件都是建立在 View、ViewGroup 基础之上的,整体采用“组合”的思想来设计 View 和 ViewGroup:ViewGroup 是 View 的子类,所以 ViewGroup 也可以被当做 View 使用。一个 Android app 的图形用户界面会对应一个视图树,而视图树则对应一个 canvas 树。这个有点儿类似于 iOS 中的 UIView 和 CALayer 的概念,一个负责应用层布局,一个负责底层渲染。

系统底层渲染显示

应用层的 view 对应到 canvas,canvas 到系统进程就成了 layer。SurfaceFlinger 主要提供 layer 的渲染合成服务。SurfaceFlinger 是一个常驻的 binder 服务,会随着 init 进程的启动而启动。下面这张图就详细的介绍了上层 view 到底层 layer 的转化,以及 SurfaceFlinger 对多个 layer 的渲染合成。

iOS 离屏渲染

离屏渲染原理以及定义

首先来介绍一下离屏渲染的原理。我们正常的渲染流程是:CPU 和 GPU 协作,不停地将内容渲染完成后得到的位图数据放入 Framebuffer (帧缓冲区)中,视频控制器则不断地从 Framebuffer 中获取内容,显示实时的内容。

而离屏渲染的流程是这样的:

与普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中不同,离屏渲染需要先额外创建离屏渲染缓冲区 ,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果再写入 Framebuffer 中。

为什么先要将数据存放在离屏渲染缓冲区呢?有两个原因,一个是被动的,一个是主动的。

  1. 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态(被动)
  2. 处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。(主动)

被动离屏渲染

常见的触发被动离屏渲染的场景

透明、阴影加圆角通常被称为 UI 三大宝,但这些效果在 iOS 的日常开发过程中却往往会导致被动的离屏渲染,下边是几个常见的会触发被动离屏渲染的场景。

触发离屏渲染的原因

讲离屏渲染的原因不得不提画家算法,画家算法的整体思想是按层绘制,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分。这里的层在 iOS 的渲染技术栈中就可以被对应到 layer。

通常对于每一层 layer,Render Server 会遵循“画家算法”,按次序输出到 frame buffer,后一层覆盖前一层,就能得到最终的显示结果,对于这个 layer 树则是以深度优先的算法将 layer 输出到 frame buffer。

作为“画家”的 GPU 虽然可以一层一层往画布上进行输出,但是却没有办法在某一层渲染完成之后,再回过头来改变其中的某个部分。因为在这一层之前的若干层 layer 像素数据,已经在渲染中被合成在一起了。其实这里和 photoshop 中的图层合并非常像,一旦多个图层被合并在一起,就无法再单独对某一个图层进行修改。所以需要在离屏缓冲区中把子 layer 依次画好,然后把四个角裁剪好之后再和之前的图层进行混合。

GPU 离屏渲染的性能影响

一提离屏渲染,我们直观上的感觉是会对性能有影响。因为为了满足 60fps 的刷新频率,GPU 的操作都是高度流水线化的。本来所有的计算工作都在有条不紊地正在向 frame buffer 输出,这时候突然又有一些特殊的效果触发了离屏渲染,需要切换上下文,把数据输出到另一块内存,这时候流水线中很多中间产物只能被丢弃,这种频繁的上下文切换对 GPU 的渲染性能有非常大的影响。

如何防止非必要离屏渲染?

  • 对于一些圆角可以创建四个背景颜色弧形的 layer 盖住四个角,从视觉上制造圆角的效果
  • 对于 view 的圆形边框,如果没有 backgroundColor,可以放心使用 cornerRadius 来做
  • 对于所有的阴影,使用 shadowPath 来规避离屏渲染
  • 对于特殊形状的 view,使用 layer mask 并打开 shouldRasterize 来对渲染结果进行缓存

圆角实现的优化策略

使用CALayercornerRadius并设置 cliptobounds 以后会触发离屏渲染(offscreen rendering)。滚动时每秒需要在 60 帧上执行裁剪操作,即使内容没有发生任何变化。GPU 也必须在每帧之间切换上下文,合成整个帧和裁剪。这些对性能的消耗直接影响到 Render Server 这个独立渲染进程,造成掉帧。为了优化渲染性能,我们可以选择一些其他的实现圆角的方案。下边是圆角的具体实现需要考虑的条件。

圆角的具体实现需要考虑的条件

  1. 圆角下(movement underneath the corner)是否有滑动。
  2. 是否有穿过圆角滑动(movement through the corner)。
  3. 四个圆角是否处于同一个 layer 上,有没有与其他 子 layer 相交。

圆角的具体实现方案

如何根据对应的条件选取圆角的实现方案

上边提到了圆角的优化要考虑的条件以及不同的圆角实现方案,下边这个流程图就是把条件和方案对应起来,给出了圆角的最佳实现方案。

总结

本文主要介绍了移动端渲染原理的相关内容。文章开始介绍了一下渲染相关的基础知识,讲了渲染所需要的原始数据源-位图以及 CPU 和 GPU 如何协同工作得到位图数据的。后面又结合 iOS 和安卓的技术框架介绍了移动端渲染的相关原理。最后深入分析了 iOS 中的离屏渲染,讲解了现有的圆角优化的一些方案。

参考文章

1.iOS 图像渲染原理
http://chuquan.me/2018/09/25/ios-graphics-render-principle/

2.iOS Rendering 渲染全解析
https://juejin.cn/post/6844904162765832206

3.iOS 渲染流程
https://www.jianshu.com/p/464c08d87f75

4.从 Auto Layout 的布局算法谈性能
https://draveness.me/layout-performance/

5.Auto Layout 是怎么进行自动布局的,性能如何?
https://juejin.cn/post/6844904055790108680

6.iOS 界面渲染流程分析
https://www.jianshu.com/p/39b91ecaaac8

7.iOS 浅谈 GPU 及“App 渲染流程”
https://juejin.cn/post/6844904106419552269

8.CPU 和 GPU 的区别是什么?
https://www.zhihu.com/question/19903344

9.IOS 进阶-图层与渲染
https://bytedance.feishu.cn/wiki/wikcnWq4HdGQygFEolgKgAVv9Oh

10.一篇文章搞懂到底什么是渲染流水线
https://segmentfault.com/a/1190000020767062

11.GPU Rendering Pipeline——GPU 渲染流水线简介
https://zhuanlan.zhihu.com/p/61949898

12.关于 iOS 离屏渲染的深入研究
https://zhuanlan.zhihu.com/p/72653360

13.texture https://texturegroup.org/docs/corner-rounding.html

14.Android 的各个渲染框架和 Android 图层渲染原理
https://juejin.cn/post/7021840737431978020

相关推荐

C语言速成之数组:C语言数据处理的核心武器,你真的玩透了吗?

程序员Feri一名12年+的程序员,做过开发带过团队创过业,擅长Java、鸿蒙、嵌入式、人工智能等开发,专注于程序员成长的那点儿事,希望在成长的路上有你相伴!君志所向,一往无前!数组:C语言数据处理...

ES6史上最全数JS数组方法合集-02-数组操作

数组生成array.ofletres=Array.of(1,2,3)console.log(res)//[1,2,3]下标定位indexOf用于查找数组中是否存在某个值,如果存...

前端性能拉胯?这 8 个 JavaScript 技巧让你的代码飞起来!

在前端开发的江湖里,JavaScript就是我们手中的“绝世宝剑”。但为啥别人用剑就能轻松斩敌,你的代码却总拖后腿,页面加载慢、交互卡顿?别着急!今天带来8个超实用的JavaScript实...

12种JavaScript中最常用的数组操作整理汇总

数组是最常见的数据结构之一,我们需要绝对自信地使用它。在这里,我将列出JavaScript中最重要的几个数组常用操作片段,包括数组长度、替换元素、去重以及许多其他内容。1、数组长度大多数人都知道可...

手把手教你在Webpack写一个Loader

前言有的时候,你可能在从零搭建Webpack项目很熟悉,配置过各种loader,面试官在Webpack方面问你,是否自己实现过一个loader?如果没有去了解过如果去实现,确实有点尴尬,其...

const关键字到底该什么用?(可以用const关键字定义变量吗)

文|守望先生经授权转载自公众号编程珠玑(id:shouwangxiansheng)前言我们都知道使用const关键字限定一个变量为只读,但它是真正意义上的只读吗?实际中又该如何使用const关键字...

“JavaScript变量声明三兄弟,你真的会用吗?

在JavaScript中,var、let和const是声明变量的关键字,它们在作用域、变量提升、重复声明和重新赋值等方面有显著区别。以下是它们的相同点和不同点,并通过代码示例详细说明。一、相同点声明变...

ES6(二)let 和 const(es6 var let const区别)

let命令let和var差不多,只是限制了有效范围。先定义后使用不管是什么编程语言,不管语法是否允许,都要秉承先定义,然后再使用的习惯,这样不会出幺蛾子。以前JavaScript比较随意,...

js 里面 let 和 const的区别(js中的let)

在JavaScript(包括Vue、Node.js、前端脚本等)中,const和let是用于声明变量的两种方式,它们的主要区别如下:constvslet的区别特性constlet是否...

JDK21新特性:Sequenced Collections

SequencedCollectionsJDK21在JEP431提出了有序集合(SequencedCollections)。引入新的接口来表示有序集合。这样的集合都有一个明确的第一个元素、第二个...

动态编程基础——第 2 部分(动态编程是什么)

有两种方法可以使用动态规划来解决问题。在这篇文章中,我们将了解制表法。请参阅我的动态编程基础——第1部分了解记忆方法。记忆制表什么是动态规划?它是一种简单递归的优化技术。它大大减少了解决给定...

Lambda 函数,你真的的了解吗(lambda函数用法)

什么是lambda函数lambda函数是一个匿名函数,这意味着与其他函数不同,它们没有名称。这是一个函数,它添加两个数字,写成一个命名函数,可以按其名称调用它们:defadd(x,y):...

JavaScript 数组操作方法大全(js数组操作的常用方法有哪些)

数组操作是JavaScript中非常重要也非常常用的技巧。本文整理了常用的数组操作方法(包括ES6的map、forEach、every、some、filter、find、from、of等)...

系列专栏(六):解构赋值(解构赋值默认值)

ES6作为新一代JavaScript标准,已正式与广大前端开发者见面。为了让大家对ES6的诸多新特性有更深入的了解,MozillaWeb开发者博客推出了《ES6InDepth》系列文章。CSDN...

js列表遍历方法解读(js遍历链表)

JavaScript提供了多种遍历数组(或列表)的方法。以下是一些常用的方法及其解读:for循环:vararray=[1,2,3,4,5];for(vari=0;...