SERVICE PHONE

13988890252
NEWS 新闻中心
你的位置: 首页 > 新闻中心
一文读懂iOS图像显示原理与优化

发布时间:2026-10-02 22:08:32  点击量:

  

一文读懂iOS图像显示原理与优化(图1)

  站在巨人的肩膀上,总结原理实现与优化及卡顿监测,一气呵成,气脉通畅,还要啥自行车~huaixiao~

  图形渲染主要是利用GPU并行运算能力,实现图形渲染并显示在屏幕的每一个像素上。渲染过程最常用的就是光栅化,即将数据转化为可见像素的过程。GPU及相关驱动实现了图形处理的OpenGL和DirectX模型,其实OpenGL不是函数API而是一种标准,制定了相关函数API及其实现的功能,具体的函数库由第三方来实现,通常是由显卡制造商来提供。

  主要包括:顶点着色器(包含了3D坐标系的转换,每个顶点属性值设定)、形状(图元)装配(形成基本的图形)、几何着色器(构造新的顶点来形成其他形状,如上图的另一个三角形)、光栅化(将形状映射到屏幕的相应的像素生成片段,片段包含了像素结构所有的数据)、片段着色器(丢弃超过视图以外的像素并着色)、测试与混合(判断像素位置如是否在其他像素的后面及透明度等决定是否丢弃及混合)。

  要想图形更加真实逼真需要更多的顶点及颜色属性,这样就增加了性能开销,为提升成产和执行效率,经常会使用纹理来表现细节。

  纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理),纹理一般可以直接作为图形渲染流水线的*第五阶段(即片段着色器)*的输入;

  GPU内部包含了若干处理核来实现并发执行,其内部使用了二级缓存(L1、L2cache),其与CPU的架构模型包含如下两种形式:分离式及耦合式,如下图所示:

  CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。

  CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。

  CPU将图形数据通过总线BUS提交至GPU,GPU经过渲染处理转化为一帧帧的数据并提交至帧缓冲区,视频控制器会通过垂直同步信号VSync逐帧读取帧缓冲区的数据并提交至屏幕控制器最终显示在屏幕上。为解决一个帧缓冲区效率问题(读取和写入都是一个无法有效的并发处理),采用双缓冲机制,在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器,如下图所示:

  双缓冲机制虽然提升了效率但也引入了画面撕裂问题,即当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:

  为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

  iOS 设备会始终使用双缓存,并开启垂直同步。而安卓设备直到 4.1 版本,Google 才开始引入这种机制,目前安卓系统是三缓存+垂直同步。

  在VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次VSync信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个VSync时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

  整个图形渲染技术栈:App 使用Core Graphics、Core Animation、Core Image等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都需要通过OpenGL来调用 GPU 进行绘制,最终将内容显示到屏幕之上,结构如下图所示:

  UIKit自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView继承自UIResponder),事件响应的传递大体是经过逐层的视图树遍历实现的。

  Core Animation是一个复合引擎,其职责是尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫做图层树的体系之中。从本质上而言,CALayer是用户所能在屏幕上看见的一切的基础。

  Core Graphics基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。

  Core Image与Core Graphics恰恰相反,Core Graphics用于在运行时创建图像,而Core Image是用来处理运行前创建的图像的。Core Image框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。

  Metal类似于OpenGL ES,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过Metal,但其实所有开发者都在间接地使用Metal。Core Animation、Core Image、SceneKit、SpriteKit等等渲染框架都是构建于Metal之上的。当在真机上调试 OpenGL 程序时,控制台会打印出启用Metal的日志。根据这一点可以猜测,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到Metal上,由Metal担任真正于硬件交互的工作。

  UIKit中的每一个视图控件其内部都有一个关联的CALaye开云电竞,Kaiyun电竞,开云集团官网,开云电竞股份有限公司r,即backing layer;由于这种一一对应的关系,视图采用视图树形式呈现,与之对应的图层也是采用图层树形式。

  视图的职责是创建并管理图层,以确保当子视图在层级关系中添加或被移除时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。

  苹果采用这种结构的目的是保证iOS/Mac平台底层CALayer通用,避免重复代码且职责分离,毕竟采用多点触摸形式与基于鼠标键盘的交互有着本质的区别;

  CALayer基本等同于纹理,本质上是一张图片,因此CALayer也包含一个contents属性指向一块缓存区,称为backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为寄宿图。

  位图(英语:Bitmap,台湾称为点阵图),又称栅格图(Raster graphics),是使用(Pixel-array/Dot-matrix点阵)来表示的图像。位图也可指:一种数据结构,代表了有限域中的稠集(dense set),每一个元素至少出现一次,没有其他的数据和元素相关联。在索引,数据压缩等方面有广泛应用,位图的像素都分配有特定的位置和颜色值。

  图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是手动绘制(custom drawing);另一种是使用图片(contents image)。

  虽然-drawRect:是一个UIView方法,但事实上都是底层的CALayer完成了重绘工作并保存了产生的图片。下图所示为-drawRect:绘制定义寄宿图的基本原理。

  其中iOS中应用并不负责渲染而是由专门的渲染进程负责,即Render Server;

  首先,由 App 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新视图树,相应地,图层树也会被更新;

  其次,App 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,App 对图层进行打包,并在下一次RunLoop时将其发送至Render Server,即完成了一次Commit Transaction操作。

  GPU通过Frame Buffer、视频控制器等相关组件对图层进行渲染到屏幕;

  为了满足屏幕60FPS刷新率,RunLoop每次操作的时间间隔不应超过16.67ms,且上述步骤需要并行执行。

  iOS 的显示系统是由VSync信号驱动的,VSync信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 线)。iOS 图形服务接收到VSync信号后,会通过 IPC 通知到 App 内。App 的Runloop在启动后会注册对应的CFRunLoopSource通过mach_port接收传过来的时钟信号通知,随后Source的回调会驱动整个 App 的动画与显示。

  备注:实际观察App启动后未注册相关的VSync相关的Source,因此上述应用应该是Render Server渲染进程注册Source监听VSync信号来驱动图层的渲染,进而提交至GPU。

  Core Animation在RunLoop中注册了一个Observer,监听了BeforeWaiting和Exit事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被CALayer捕获,并通过CATransaction提交到一个中间状态去(CATransaction的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop即将进入休眠(或者退出)时,关注该事件的Observer都会得到通知。这时Core Animation注册的那个Observer就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,Core Animation会通过CADisplayLink等机制多次触发相关流程。

  为了保证渲染性能,主要是保证CPU及GPU不会阻碍上述渲染流程进而引发“掉帧”现象,因此需要分别针对CPU及GPU影响渲染过程进行分析、评估及优化。

  对象创建会分配内存、调整属性、甚至还有读取文件(如创建UIViewController读取xib文件)等操作,比较消耗CPU资源。因此,尽量使用轻量的对象替代重量的对象,如CALayer比UIView不需要响应触摸事件;如果对象不涉及UI操作,则尽量放到后台线程执行;性能敏感的视图对象,尽量使用代码创建而不是Storyboard来创建;如果对象可以复用,可以使用缓存池来复用。

  对象调整也经常是消耗CPU资源的地方,如CALayer属性修改、视图层次调整、添加和移除视图等;

  CALayer内部并没有属性方法,其内部是通过runtime动态接收方法resoleInstanceMethod方法为对象临时添加一个方法,并把对应属性值保存到内部的一个Dictionary字典里,同时还会通知delegate、创建动画等。UIView的关于显示相关的属性(比如frame/bounds/transform)等实际上是CALayer属性映射来的。

  虽然对象销毁销毁资源不多,但累积起来也不容忽视。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显,因此,可见用于后台线程去释放的对象挪动后台线程去。技巧代码如下:

  视图布局计算是应用最为常见的销毁CPU资源的地方,其最终实现都会通过UIView.frame/bounds/center等属性的调整上,因此,避免CPU资源消耗尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。

  Auotlayout是苹果提倡的技术,可大部分情况下能很好地提升开发效率,但是其对于复杂视图来说尝尝会带来严重的性能问题,具体可参阅36/,因此对于性能要求高的视图尽量使用代码实现视图。

  Core Text 是为一些必须处理底层字体处理和文字布局的开发者准备,如无必要,你应该使用 TextKit(Text Programming Guide for iOS)、CocoaText(Cocoa Text Architecture Guide)等框架开发你的 App 或 Mac 应用。Core Text 是以上两种文本框架的底层实现,因此它们的速度和效率是共享的。除此之外,以上两种文本框架提供了富文本编辑及页面布局引擎。如果你的 App 只使用 Core Text,则需要为其提供其他的基础实现。Core Text 编程指南

  屏幕上能看到的所有文本内容控件,包括UIWebView,在底层都是通过CoreText排版、绘制为Bitmap显示。常见的文本控件,如UILabel、UITextView等,其排版和绘制都是在主线程进行,当显示大量文本时,CPU的压力会非常大。解决方案只有一个,就是自定义文本控件,并用TextKit或最底层的CoreText对文本异步绘制。

  当使用UIImage或CGImageSource的那几个方法创建图片时,图片数据并不会立即解码。只有图片设置到UIImageView或者中去,并且CALayer被提交到GPU前,CGImage中的数据才会得到解码,且需要在主线程执行。

  解决方法:后台线程先把图片绘制到CGBitmapContext中,然后从Bitmap直接创建图片。目前常见的网络图片库都自带这个功能。

  图像的绘制通常是指用CGxx开头的方法将图像绘制到画布中,然后从画布创建图片并显示这样的一个过程。这个最常见的就是[UIView drawRect:]方法,由于CoreCraphic方法通常都是线程安全的,所以图像的绘制可以很容易放到后台线程进行,示例如下:

  相对于CPU来说,GPU主要就是:接收提交的纹理和顶点描述(三角形),应用变换、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

  所有的Bitmap,包括图片、文本、栅格化的内容,最终都要从内存提交到显存,绑定为GPU纹理。不论是提交到显存的过程,还是GPU调制和渲染纹理的过程,都要消耗不少GPU资源。当在较短时间内显示大量图片时(如UITableView存在非常多的图片并且快速滑动时),CPU占用率很低,GPU占用非常高,因此会导致界面掉帧卡顿。有效避免此情况的方法就是尽量减少在短时间内大量图片的显示,尽可能将多张图片合并为一张进行显示。

  多存在多视图且多层次重叠显示时,GPU会首先将其混合在一起。如果视图结构很复杂,混合的过程也会消耗很多的GPU资源。为了减轻GPU的消耗,应尽量减少视图数量级层次,并在不透明的视图里标明opaque属性以避免无用的Alpha通道合成。

  CALayer的border、圆角、阴影、遮罩(mask),CASharpLayer的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在GPU中。当一个列表视图中存在大量圆角的CALayer且款式滑动时,会消耗大量的GPU资源,进而引发界面卡顿。为避免此种情况,可以尝试开始CALayer.shouldRasterize属性,这会吧离屏渲染的操作转嫁到CPU上;最好是尽量避免使用圆角、阴影、遮罩等属性。

  GPU屏幕渲染存在两种方式:当前屏幕渲染(On-Screen Rendering)和离屏渲染(Off-Screen Rendering),其中当前屏幕渲染就是正常的GPU渲染流程,GPU将渲染完成的帧放到帧缓冲区,然后显示到屏幕;而离屏渲染会额外创建一个离屏渲染缓冲区(如保存后续复用的数据),后续仍会提交至帧缓冲区进而显示到屏幕。

  离屏渲染需要创建新的缓冲区,渲染过程中会涉及从当前屏幕切换到离屏环境多次上下文环境切换,等到离屏渲染完成后还需要将渲染结果切换到当前屏幕环境,因此付出的代价较高。

  将不需要主线程执行的消耗性能的通过异步执行方式执行,如文本宽高和视图布局计算,文本渲染、图片界面和图形绘制,对象创建、属性调制和销毁;但UIKit和Core Animation相关操作必须在主线程执行,对于不能后台执行的就优化性能。

  对于多层级CALayer情况,GPU需要图层合成,但对于多层级图层中不需要动画和位置调整的情况,就会导致没必要的GPU性能消耗,因此ASDK为此实现了一个pr-composing的技术,将多层级图层合并渲染成一张图片,有效降低了GPU的消耗。

  上文提到的可以后台线程的任务通过GCD异步并发执行,有效利用iPhone处理器多核的特点。

  ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。通过这种机制,ASDK 可以在合适的机会把异步、并发的操作同步到主线程去,并且能获得不错的性能。

  Time Profiler,用来检测CPU的使用情况。它可以告诉我们程序中的哪个方法正在消耗大量的CPU时间。使用大量的CPU并不一定是个问题 - 你可能期望动画路径对CPU非常依赖,因为动画往往是iOS设备中最苛刻的任务。 但是如果你有性能问题,查看CPU时间对于判断性能是不是和CPU相关,以及定位到函数都很有帮助。

  原理就是添加CADisplayLink对象至runloop中统计每秒回调次数,通过次数/时间来获取屏幕刷新率FPS,具体实现如下:

  CADisplayLink是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和NSTimer并不一样,其内部实际是操作了一个Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和NSTimer相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。通过对比CADisplayLink添加至runloop前后modes变化,发现其实现是向runloop中添加Source1回调为IODispatchCalloutFromCFMessage;

  UI绘制并不一定FPS为满60帧,如动画片FPS为24,因此,通过FPs方案监测卡顿是存在问题的。

  FPS 是一秒显示的帧数,也就是一秒内画面变化数量。如果按照动画片来说,动画片的 FPS 就是 24,是达不到 60 满帧的。也就是说,对于动画片来说,24 帧时虽然没有 60 帧时流畅,但也已经是连贯的了,所以并不能说 24 帧时就算是卡住了。

  通过子线程监测主线程的runloop,判断kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting两个状态之间的耗时是否达到一定阈值,若监测到卡顿则记录此时的函数调用信息,具体代码如下:

  而主线程RunLoop闲置时处在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间的kCFRunLoopBeforeWaiting状态,因此导致错误的判断为卡顿,因此优化解决此问题出现了子线程ping方案。具体的原理如下:创建一个子线程通过信号量去ping主线程,因为ping的时候主线程肯定是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间。每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿,ANREye中就是使用子线程Ping的方式监测卡顿的,具体代码如下:

地址:湖北省武汉市东西湖区长青街东吴大道786号  电话:0710-3453356 手机:13988890252
Copyright © 2026 开云电竞游戏有限公司 版权所有