iOS上的UI是如何渲染出来的? 深入浅出UIKit渲染

news2024/11/22 19:40:24

我们在代码中写的View、Image等组件,最终是如何一步步渲染到屏幕上的呢?触摸、动画等是如何实现的?我们可以利用这些知识做哪些优化呢?

本文先从屏幕物理层原理出发,一步步介绍渲染流程,然后介绍iOS的UIKit框架设计,最后介绍如何利用这些知识做优化

先看第一步,屏幕是如何非常细腻的展示图片的

屏幕上的数据是如何一步步渲染出来的?

屏幕的显示原理

我们知道,所有的颜色都可以通过三原色红蓝绿来展示出来,在屏幕上也是一样,屏幕设备上的每个像素点,如果我们把它放大,那么可以看到3个滤光片组成的,简单理解是这样子的

在这里插入图片描述

屏幕类型主要是LCD和OLED,他们的发光原理不同,但是抽象起来就是通过RGBA四个参数的调整来做最后的显示。
PS: 如果对两种屏幕的原理感兴趣的话,可以看下这个视频

[video(video-9IgXCWLT-1714361636297)(type-undefined)(url-undefined)(image-https://img-blog.csdnimg.cn/editor-video.png)(【硬核科普】全网最简洁易懂的OLED与LCD屏幕工作原理与优劣科普
)]

知道了屏幕的显示原理之后,我们就会想到一个问题,那屏幕的RGBA四个参数是从哪里来的呢? 答案是GPU

GPU

在计算机结构的设计中,CPU主要负责逻辑运算,而GPU就负责图片渲染的运算。 GPU从硬件上支持T&L(Transform and Lighting,多边形转换与光源处理),相比通用计算的CPU来讲,其有两个优势:

  • 处理图片计算的能力要强的多,因为硬件支持T&L
  • 并发能力比CPU也要大
    详细对比如下:
    在这里插入图片描述

GPU的渲染流程大致如下:
在这里插入图片描述

针对屏幕的硬件能力,GPU会根据其FPS来组织数据,比如常见的FPS是60,那么GPU每1/60秒内就要提供一次渲染数据供绘制,如果没能及时提供数据,那么就会出现该帧没有重绘,给用户的感觉就是出现了卡顿

从上图可以看到,最终输入GPU的其实还是一堆数据,那么这堆数据是谁给GPU的呢?

GPU的数据从哪里来 - OpenGL和Metal

在不同的系统上会有不同的答案,针对iOS的UIKit,答案就是OpenGL和Metal

OpenGL是苹果最初的采用的渲染框架,其具有跨平台、性能好,包大小可控等优点,但是随着图形技术的发展,其他也暴露出很多问题:

  • 现代 GPU 的渲染管线已经发生变化。
  • 不支持多线程操作。
  • 不支持异步处理。

因此苹果重新设计了Metal框架,其优势如下:

  • 更高效的 GPU 交互,更低的 CPU 负荷。
  • 支持多线程操作,以及线程间资源共享能力。
  • 支持资源和同步的控制。

PS: 具体的两者间细节对比,可以参考: https://juejin.cn/post/6844903619339223048

OpenGL的数据从哪里来

在UIKit中,OpenGL的数据主要从以下三个库获得:

  • Core Graphics : 基于Quartz的绘图引擎,用于运行时绘制图像
  • Core Image : 处理图像
  • Core Animation : 核心动画和图层渲染能力

Metal 的数据从哪里来

目前来看来看UIKit的内容都不走Metal,而是通过OpenGL来实现,直接使用Metal的主要是Unity等框架直接使用,比如RenderTexture

综上所述,我们就可以画出这张图:
在这里插入图片描述

那么问题来了,我们并没有直接调用Core Graphics, Core Animation这些框架,而是使用UIImageView, UIView这些类,UIKit框架是如何调用Core框架来渲染的呢?

UIKit 的实现

上面的内容是从底层往上层讲的,这次我们反过来,看看UIView是一步步渲染的,首先介绍基础概念: UIView和CALayer

UIView和CALayer

UIView和CALayer体验了职责分离的设计思想,其中UIView负责触摸事件的处理,而CALayer负责渲染层。之所以这样设计,是因为Mac等系统没有使用UIKit,而是使用了AppKit,但是底层渲染都使用了CoreAnimation

在UIView创建的同时,系统会创建一个layer绑定到UIView的属性上,被称为backing layer。 当我们修改UIView的圆角、边框等属性时,其实是UIView封装了Layer的修改方法,最终的渲染实现还是在Layer上。
而响应链等触摸响应的能力,则是由UIView来实现(当然,底层还是依赖Layer的-containsPoint:和-hitTest:等方法)

CALayer的绘制

读取图片或者纹理
CALayer有个属性 contents ,用来承载最终在界面上绘制的内容
/* An object providing the contents of the layer, typically a CGImageRef

  • or an IOSurfaceRef, 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;
看介绍可以看到,其在iOS系统上来源就是CGImageRef和IOSurfaceRef,我们来看下这俩分别是什么

  • CGImageRef : A bitmap image or image mask. 用来承载读取的图片
  • IOSurface是用于存储FBO、RBO等渲染数据的底层数据结构,是跨进程的,通常在CoreGraphics、OpenGLES、Metal之间传递纹理数据

这里简单可以理解为,content内容就是CALayer从现成的图片或者纹理来读取内容,然后渲染。

手动绘制

如果不想直接读取图片呢?我们也可以手动来绘制
通常,我们会继承UIView类,然后实现-drawRect:方法来实现自定义的绘制。 当然,就像上面说的UIView和CALayer的关系,虽然看起来是UIView实现了自定义的绘制,但是其实际工作都是CALayer来完成的

CALayer持有一个delegate,CALayerDelegate,其指向关联的UIView,如果UIView实现了drawRect方法,那么会执行drawRect方法来绘制,生成最终的图片到backing store, 关系如下:
在这里插入图片描述

当然,一般都不太推荐使用Core Graphics来自定义绘制,因为CoreAnimation已经提供了很多好用的类,比如CAShapeLayer,CATextLayer等来使用,性能会提升很多

CALayer在实际绘制的过程中,也是通过drawLayer等方法完成,后续可以再研究下细节。

看到CA这个前缀,我们就可以确定CALayer的底层渲染框架是Core Animation,下面就介绍下Core Animation,其功能远超其名称,这里强推一本书《iOS Core Animation: Advanced Techniques中文译本》https://zsisme.gitbooks.io/ios-/content/index.html
Core Animation 当我们聊动画的时候,我们在聊些什么?
当我们说动画的时候,我们通常的意思是,会动的图画,一帧帧的图片快速切换,就让人眼产生了会动的感觉,现在的视频都是这个原理。比如我们通常使用动画来做一些好看的交互效果,图片的切换、弹窗的弹出等等。 而Core Animation的动画做的事情超出了这个意思上的界限。

隐式动画

Core Animation基于一个假设,屏幕上任何变化的东西都可以做动画,不仅仅是位置、大小、角度。 也包括颜色等属性, 我理解只要是能通过数学公式曲线变化的,都可以想办法做动画。
颜色特别好理解,比如有颜色A,其可以数学理解为rgba四个属性,那么其颜色变成B就是rgba变化,在数学上就可以理解为 A(ra,ga,ba,aa) -> B(rb,gb,bb,ba) 的转换

在修改CALayer的属性时,Core Animation都会去做隐式动画的转换,让整个变换的过程更加流畅而不显生硬。比如我们想修改CALayer的颜色时,我们可以观察下:
aView.frame = CGRectMake(100, 100, 100, 100)
aView.backgroundColor = .red

    view.addSubview(aView)
    
    let layer = CALayer()
    layer.frame = CGRectMake(25, 25, 50, 50)
    layer.backgroundColor = UIColor.blue.cgColor
    
    aView.layer.addSublayer(layer)

    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        
        let red = arc4random() % 255
        let green = arc4random() % 255
        let blue = arc4random() % 255
        
        layer.backgroundColor = UIColor(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: 1).cgColor
    }

其变化比较快,但是感觉是平滑过渡的,但是隐式动画的默认动画时间是0.25s,所以肉眼观察比较难,我们可以修改下动画时间:
aView.frame = CGRectMake(100, 100, 100, 100)
aView.backgroundColor = .red

    view.addSubview(aView)
    
    let layer = CALayer()
    layer.frame = CGRectMake(25, 25, 50, 50)
    layer.backgroundColor = UIColor.blue.cgColor
    
    aView.layer.addSublayer(layer)

    

    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        
        CATransaction.begin()
        CATransaction.setAnimationDuration(5.0)
        
        
        let red = arc4random() % 255
        let green = arc4random() % 255
        let blue = arc4random() % 255
        
        layer.backgroundColor = UIColor(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: 1).cgColor
        
        CATransaction.commit()
    }

这样就会很明显的看到,颜色是在一点点平滑过渡的。

事务

在这里我们用了一个类CATransaction,中文翻译为: 事务。 CALayer每个可隐式动画属性的转换都是通过事务来变化的,事务提交之后,其才会从原始值变更为新值。

在一个run loop周期内, CoreAnimation会提交事务,然后runloop会循环收集这些事务,然后执行。

PS: 在UIView中,隐式动画是被禁用的,可以看https://juejin.cn/post/6984716038445203469详细了解

呈现树

刚才提到,修改属性的时候,属性并不是立刻变化的,而是通过隐形动画来变化。但是当我们在动画未完成的时候读取属性时,会发现其属性也变化了, 即系统记录了当前变化的最终目标。那么系统是如何知道变化过程中的值呢?
和图层树对应,系统会创建一个呈现树来记录当前变化中的图层状态。

目前为止我们一共知道了3种树:

  • 视图树(UIView)
  • 图层树(CALayer)
  • 呈现树(变化中的实时渲染属性)

还有一种树,渲染树,我们会在后面讲到

绘制流水线

上面提到,最终的绘制都是通过CALayer来完成,那么Core Animation是如何一步步将CALayer绘制出来呢?
在这里插入图片描述

  • 当App需要变更UI的时候,即视图树发生变化,这时候对应的图层树也会跟随变化,CoreAnimation会提交事务来渲染最终的效果
  • 这时候RenderServer会把数据反序列化为渲染树的内容,然后提交给GPU来渲染
  • GPU收到数据,进行它最擅长的图形数据计算,然后把数据给屏幕
  • 最终,屏幕完成了RGBA在每个像素点上的变更

因为屏幕的渲染是持续的,所以上面只是一个帧的工作,而系统是通过流水线的形式,保证每一帧都能有最终的屏幕刷新,如图:
在这里插入图片描述

性能优化

基于上面学到的知识,我们该如何写代码,提升性能呢?

减少CPU的使用

上面提到,GPU擅长计算图形,而CPU擅长通用计算,所以我们在写代码的时候进行多利用GPU,而减少CPU的抢占。 那么有哪些任务会触发CPU呢?

  • 布局计算,自动布局尤其明显,最好是能提前计算好,比如tableview的cell高度,自适应高度的话卡顿还是比较明显的
  • 减少Core Graphics的计算,比如自己实现-drawRect:方法
  • 解压特别大的图片,尤其是超过屏幕大小的图片
  • 减少IO操作

减少图层的混合,减少图层数量

GPU在计算的时候,需要计算所有图层累计后的展示,那么减少图层就可以减少混合的工作。 另外GPU也会放弃所有被完全遮挡的图层,所以如果可以的话,非必要不使用带透明度的图层

离屏渲染

当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制:

  • 圆角(当和maskToBounds一起使用时)
  • 图层蒙板
  • 阴影
  • 光栅化
    我们可以使用CAShapeLayer,contentsCenter或者shadowPath来获得同样的表现而且较少地影响到性能。

适当使用光栅化技术

上面我们提到光栅化会执行离屏渲染,那为什么这里又提到可以优化性能呢? 别急,我们先看下什么是光栅化。
shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。

如果我们有个cell,它的阴影是固定的,那么就可以考虑光栅化,将其缓存下来,避免重复的阴影绘制。 具体的数据对比可以看: iOS性能优化 —— 一个简单的Layer Rasterize(光栅化)例子

参考文档

  • https://juejin.cn/post/6994075190514679838
  • wwdc 2011 session 121 understanding uikit rendering
    • https://www.youtube.com/watch?v=Qusz9R39ndw&ab_channel=%E5%88%98%E5%85%88%E6%A3%AE
    • https://docs.huihoo.com/apple/wwdc/2011/session_121__understanding_uikit_rendering.pdf
    • https://www.wwdcnotes.com/notes/wwdc11/121/
  • IOS进阶-图层与渲染
  • iOS界面渲染与优化
  • https://zhuanlan.zhihu.com/p/587949539
  • https://www.jianshu.com/p/ab28b7745e30
  • https://juejin.cn/post/6844904106419552269
  • https://juejin.cn/post/6844903619339223048

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1633711.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

go idea 不同区域的字体行距设置

1、代码区域的设置: 2、左侧project导航栏的设置: 3、问:go idea 底部的窗口,比如run、terminal、debug、version control等的设置:

pytorch 实现语义分割 PSPNet

语意分割是指一张图片上包含多个物体,通过语义分割可以识别物体分类、物体名称、像素识别的任务。和物体检测不同,他不会将物体框出来,而是根据像素的归属把物体标注出来。PSPNet 的输入是一张图片,例如300500,那么输出…

全志ARM-修改开发板内核启动日志

修改开发板内核日志输出级别: 默认输出级别为1,需要用超级用户权限修改 sudo vi /boot/orangepiEvn.txt 把第一行内核启动输出权限改为7,第二行把输出方式该为“serial”串口输出

QT:小项目:登录界面 (下一个连接数据库)

一、效果图 登录后&#xff1a; 二、项目工程结构 三、登录界面UI设计 四主界面 四、源码设计 login.h #ifndef LOGIN_H #define LOGIN_H#include <QDialog>namespace Ui { class login; }class login : public QDialog {Q_OBJECTpublic:explicit login(QWidge…

区块链技术下的DApp与电商:融合创新,开启商业新纪元

区块链技术的蓬勃发展正引领着一种新型应用程序的崛起——去中心化应用程序&#xff08;DApp&#xff09;。DApp并非传统的中心化应用&#xff0c;它构建于去中心化网络之上&#xff0c;融合了智能合约与前端用户界面&#xff0c;为用户提供了全新的交互体验。智能合约&#xf…

Leetcode—1146. 快照数组【中等】(ranges::lower_bound、std::prev函数)

2024每日刷题&#xff08;121&#xff09; Leetcode—1146. 快照数组 思路 题意很绕&#xff0c;解释一下&#xff1a; 拍一次照&#xff0c;复制出一个新数组&#xff0c;set 都在这个新的上面进行get 目标是得到第 id 个数组的特定位置的值 实现代码 class SnapshotArray…

ROS 2边学边练(39)-- 调试tf2

前言 这节还是围绕tf2来进行&#xff0c;只不过针对调试相关&#xff0c;把之前有过一面之缘的问题再次拿出来重点说明一下&#xff0c;此过程中我们会碰到之前几期中认识但还不怎么熟络的朋友比如tf2_echo、tf2_monitor、view_frames。 动动手 我们会利用一个有不少问题的例子…

Python-100-Days: Day06 Functions and Modules

函数的作用 编程大师Martin Fowler先生曾经说过&#xff1a;“代码有很多种坏味道&#xff0c;重复是最坏的一种&#xff01;”&#xff0c;要写出高质量的代码首先要解决的就是重复代码的问题。可以将特定的功能封装到一个称之为“函数”的功能模块中&#xff0c;在需要的时候…

JavaScript代码挑战#4

// 编程挑战 #4 /* 朱莉亚和凯特仍在研究狗&#xff0c;这次她们研究的是狗是否吃得太多或太少。 吃得太多意味着狗当前的食物份量比推荐份量大&#xff0c;吃得太少则相反。 吃得适量意味着狗当前的食物份量在推荐份量的正负 10% 的范围内&#xff08;参见提示&#xff09;。 …

企业邮箱哪个性价比高?2024年国内五大企业邮箱功能、价格对比

对于企业来说&#xff0c;更换企业邮箱的成本很高&#xff0c;包括企业邮箱数据迁移&#xff0c;新的通讯录导入等&#xff0c;都会耗时耗力。那么选择能稳定且性价比高的企业邮箱就至关重要&#xff0c;国内的Zoho Mail企业邮箱、网易企业邮箱、阿里企业邮箱、腾讯企业邮箱和新…

2024年宠物行业未来发展趋势(宠物行业增长风口深度报告)

近期&#xff0c;小红书联合第一财经发布了宠物行业洞察报告。报告中指出了宠物行业的三大消费新趋势&#xff1a;科学养宠、专宠专用和双向奔赴。而实际上&#xff0c;这三大趋势在2024年已经有明显的凸显。 趋势一&#xff1a;科学养宠 科学养宠主要体现在宠物主粮的选择上&…

Linux进程概念(七):进程替换 自主shell的编写

目录 进程替换 六种替换函数 自主shell的编写 shell解析命令行字符串的过程 创建makefile 打印输出命令行 获取与分割命令行字符串 多次执行命令 完善工作 完整代码 进程替换 原因&#xff1a;fork创建子进程后&#xff0c;执行的是和父进程相同的代码&#xff0c;…

Spring6 当中 获取 Bean 的四种方式

1. Spring6 当中 获取 Bean 的四种方式 文章目录 1. Spring6 当中 获取 Bean 的四种方式每博一文案1.1 第一种方式&#xff1a;通过构造方法获取 Bean1.2 第二种方式&#xff1a;通过简单工厂模式获取 Bean1.3 第三种方式&#xff1a;通过 factory-bean 属性获取 Bean1.4 第四种…

一例MFC文件夹病毒的分析

概述 这是一个MFC写的文件夹病毒&#xff0c;通过感染USB设备传播&#xff0c;感染后&#xff0c;会向c2(fecure.info:443)请求指令来执行。 样本的基本信息 Verified: Unsigned Link date: 19:52 2007/7/5 MachineType: 32-bit MD5: 4B463901E5858ADA9FED28FC5…

基于SpringBoot+Vue笔记记录分享网站设计与实现

项目介绍&#xff1a; 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0c;还是可操作性等各个方面来讲&#xff0c;遇到了互联网时代…

Docker-Compose单机多容器应用编排与管理

前言 Docker Compose 作为 Docker 生态系统中的一个重要组件&#xff0c;为开发人员提供了一种简单而强大的方式来定义和运行多个容器化应用。本文将介绍 Docker Compose 的使用背景、优劣势以及利用 Docker Compose 简化应用程序的部署和管理。 目录 一、Docker Compose 简…

闲话 Asp.Net Core 数据校验(三)EF Core 集成 FluentValidation 校验数据例子

前言 一个在实际应用中 EF Core 集成 FluentValidation 进行数据校验的例子。 Step By Step 步骤 创建一个 Asp.Net Core WebApi 项目 引用以下 Nuget 包 FluentValidation.AspNetCore Microsoft.AspNetCore.Identity.EntityFrameworkCore Microsoft.EntityFrameworkCore.Re…

leetcode51.N皇后(困难)-回溯法

思路 都知道n皇后问题是回溯算法解决的经典问题&#xff0c;但是用回溯解决多了组合、切割、子集、排列问题之后&#xff0c;遇到这种二维矩阵还会有点不知所措。 首先来看一下皇后们的约束条件&#xff1a; 不能同行不能同列不能同斜线 确定完约束条件&#xff0c;来看看究…

【Linux】yum、vim

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/qinjh_/category_12625432.html 目录 Linux 软件包管理器 yum 什么是软件包 查看软件包 如何安装软件 如何卸载软…

Apache Seata基于改良版雪花算法的分布式UUID生成器分析1

title: Seata基于改良版雪花算法的分布式UUID生成器分析 author: selfishlover keywords: [Seata, snowflake, UUID] date: 2021/05/08 本文来自 Apache Seata官方文档&#xff0c;欢迎访问官网&#xff0c;查看更多深度文章。 Seata基于改良版雪花算法的分布式UUID生成器分析…