从富文本窥探苹果的代码秘密

从富文本窥探苹果的代码秘密

背景

在我们的业务场景下,为突出诸如 “利益点”和“利率” 等特性以推动订单成交,引入了 “富文本” 这一概念。富文本具备丰富格式的文本展示与编辑功能。然而,恰是由于富文本具有 “多样式”“复杂排版” 等特质,致使其在复杂元素渲染过程中会耗费更多系统资源。相较于简洁的纯文本,富文本在加载与显示时或许会产生延迟现象,尤其是处理大量富文本内容或在较老旧的 iOS 设备上,延迟表现得更为显著。我们项目内长期存在这一问题,对用户的使用体验及交互效率造成了一定影响。

现状:

直接上视频:

注意看文案:“注意看我 我会闪烁...”这句话,可以非常直观的看到,伴随着每次的刷新,中间侧的富文本都会有一个闪动。

为什么会产生这种情况?

富文本包含多种格式信息,如字体、字号、颜色、段落样式、对齐方式、图片、表格等。简单的纯文本可能只存储字符编码序列,而富文本要记录每个字符或段落对应的格式属性。以 HTML 为例,一段带有加粗、斜体和不同颜色的文本,会有大量的标签(如<b><i><span style="color:red">等)来描述这些格式。在解析富文本时,软件或系统需要花费更多的时间和计算资源来解读这些格式标记。需要识别每个标签的含义,按照标签要求正确地显示文本内容。

仅将标签式的 HTML 格式转化为能够被 iOS 系统直接加载的 UI 控件,就已经是一种对系统资源消耗极大的情况。但在我们的项目中,为了推动订单达成交易转化,非常频繁的使用到了“删除线”“下划线”等元素。在iOS视图的叠加逻辑下,这会非常平常频繁的触发一个iOSer的噩梦 ——离屏渲染

离屏渲染:

在大部分计算机视图的叠加中,都遵循下图,油画算法

油画算法(Painter's Algorithm)也被称为画家算法,是一种在计算机图形学中用于解决可见性问题的图形渲染算法。其基本思想源于传统绘画过程,就像画家在作画时,先画远处的背景,再画近处的物体,这样近处的物体自然会覆盖远处的部分,从而确定最终画面的可见部分。

图2.油画算法

上面这段话是GPT写的,说人话就是:先画山(最底层/最远处),再画草地(第二层),最后画树(最顶层)。

这样的好处是:当渲染较近的树木时,其像素会覆盖掉之前渲染的山川在相同位置上的像素,从而模拟出近物遮挡远物的视觉效果。并且它主要依靠物体的深度排序来确定渲染顺序,在物体数量较少、深度关系简单的场景中,计算资源的消耗相对较少。

但如果在完成树的绘制之后,我们又想要改变山的形状,颜色,这个时候视图“山”,已经被草地和树遮盖住了,无法直接修改。而iOS对此的改进措施既是:离屏渲染

正常渲染的流程是:APP中的数据经过CPU计算和GPU渲染后,将结果存放在帧缓冲区,利用视频控制器从帧缓冲区中取出,并显示到屏幕上。

离屏渲染(offscreen-rendering)顾名思义为屏幕外的渲染,即渲染的结果不会直接呈现到当前屏幕上,而是等待合适的时机才会被显示。譬如上述“完成树的绘制后,又要改变山的背景”,计算机是无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。

在先前对 iOS 官方文档的研读过程中,得知仅有某些特定场景,例如圆角、遮罩、透明度等会触发离屏渲染,而对于富文本是否会触发离屏渲染,并没有得到苹果官方的验证。但在借用OffScreen 等工具的帮助下,确认了这一点(标绿的即为首页卡片中触发了离屏渲染的场景)

(有背景色的即为触发离屏渲染的场景)

GPU 操作高度流水线化,正常时向帧缓冲区(frame buffer)有序地输出计算工作,当突然接收到"输出到另一块内存"的指令时,流水线中正在进行的所有操作被迫丢弃,转而服务于当前的这一操作指令。完成后,再将计算好的所有内容copy回帧缓冲区(frame buffer)并清空临时内存。在这个操作中,系统会做以下几件事

  1. 开辟一个临时的空间。
  2. 上下文切换
  3. 内存拷贝
  4. ...

以上每一条对CPU & GPU来说都是极其承重的包袱。分析这正是导致项目中的富文本出现闪烁的原因之一。

层级嵌套过深

在其它需求开发的过程中,针对首页卡片的视图层进行了剖析与梳理。结果令人震惊,仅仅一个首页卡片,其层级嵌套竟然高达 9 层。如果将 iOS 本身的 window 等系统层级也计算进来,那么就会有十几层的嵌套。如此高的层级嵌套很难不引发性能问题。

(虽然马赛克,但是不影响我们理解视图层级之深对吧)

其次,我们项目中为了解决"多设备"”多分辨率视图“的UI问题,引入了三方库”Masonry“。

Masonry 是一个轻量级的布局框架,它使用简洁的链式编程语法来创建和更新视图布局。提供了强大的自动布局功能,能够很好地适应不同屏幕尺寸和设备方向。但是在诸多的优点下有一个非常致命的缺陷:在一些非常复杂的布局场景中,大量使用 Masonry 会导致性能下降。因为每次更新布局时,Masonry 都需要重新计算和调整视图的约束,会消耗较多的计算资源。

分析是导致项目中的富文本出现闪烁的原因之一。

改进措施

改进UI层级?

理论上这是最好的解决办法了,但是贸然去改动UI层级是非常有风险的一件事,并且冗长的工期怕也是产品不能接收的,测试同学也得执行一遍所有的用例。为了一行富文本的展示,去站到代码质量,产品,测试的对立面,确实得不偿失。

预排版,提前计算?

Masonry 在多层嵌套的UI层级下有性能问题的核心原因是:

为了正确地应用约束和渲染视图,Masonry 需要遍历整个 UI 层级结构。在多层嵌套的情况下,遍历的路径变长,深度增加。就像在一个有很多分支的树形结构中寻找叶子节点一样,需要花费更多的时间来遍历每个节点。那么我们把这个计算过程置前,或者说,这个计算过程由我们自己来计算,不再交给Masonry 处理。

图7.预计算

业务场景下,富文本最多会由两个”子富文本“ & 一个”分割线“的image拼接而成。这里根据不同的情况, 提前对子view的位置进行了计算。直接赋值给Masonry 去布局。

采用更为轻量级的富文本对象

渲染过慢的原因之一,富文本对象是一个非常重的对象,通常包含了大量的属性和信息。例如,除了基本的文本内容外,它还可能有字体、字号、颜色、段落格式、对齐方式、链接、图片、表格等诸多属性。这些丰富的属性使得富文本对象在存储和处理时占用大量的资源,就像一个装满各种复杂工具和材料的大箱子。若要减少系统处理的信息量,只使用需要用到的属性即可,就像是只从大箱子中挑选当前任务所需的工具和材料譬如只是简单地显示一段富文本的标题部分,只提取文本内容和字号、字体等基本属性进行渲染,就可以避免处理那些与当前任务无关的链接属性、复杂的段落缩进等属性,从而加快渲染速度。

但是他的维护成本真的是太太太高了。首先,确定哪些属性是真正需要用到的这个过程本身就需要耗费大量的精力。其次,iOS的系统更新对开发者来说就是纯黑盒,我们无法猜测apple官方会在什么时间点针对富文本新增or删除什么样的属性。最重要的,我们并不知道富文本内部属性的关系和依赖。例如,如果只选择了字体和字号属性进行渲染,但是在某些情况下,字体颜色也可能会影响到显示效果,这就需要额外的代码来判断是否需要添加字体颜色属性。这种复杂的逻辑关系会使代码变得难以理解和维护,bug率必定飞升。

离屏渲染的避免 or 减少?

得益于iOS对系统安全的绝对保护,iOS代码是不开源的。我们并不能直观的看到iOS离屏渲染的执行情况。所以要想直接第一角度为离屏渲染减负是非常宽泛,复杂且不现实的事儿。既然减少不了单次离屏渲染的耗时,那就珍惜GPU成果,将其缓存下来,以减少离屏渲染的频次。

这里简单阐述一下项目中富文本的逻辑,服务端给到的富文本有两种情况

  1. 一条富文本文案。
  2. 两条富文本文案拼接起来的,中间用 && 进行分割。

针对这种情况,添加了两层缓存。第一层缓存仅记录最近一次富文本的结果,不对&&的情况进行区分。缓存中记录了普通文本对象、计算后产生的富文本对象、富文本布局位置等信息。第二层缓存是一个key-value的字典数据结构,同样保存上述所有信息。不一样的是,二级缓存所有已经计算过的富文本。防止出现:当富文本是由A,B两条富文本拼接而成的,A有改动,B没有改动的情况,A从缓存中取值,B重新计算。最小化CPU的操作频次。

核心代码:

图8.缓存

成果

一套组合拳下来,效果显著,直接上图

当刷新时富文本文案没有变动:

当刷新时富文本文案有变动:

左半部分富文本文案不变动,右半部分富文本文案新增

易用性封装

为了便于日后的富文本场景的简易开发,封装了PPDHTMLLabel,可以直接通过一个方法实现一个不闪动,高性能的富文本视图。调用方法如下:

无心插柳柳成荫:

上述一套操作下来,帮CPU + GPU减负很多,将视图的性能消耗降低在了触发离屏渲染的阈值以下。

猜测小case

在写这篇文章的同时,发现了一个非常有意思的case

为了更清晰的展示富文本的闪动,我将首页的刷新动画慢放了16倍。发现在富文本没有被计算出来之前,苹果为了不出现白屏的情况,会先把渲染的文本直接赋值上页面上。

最重要的几帧见下图:

一个更大胆的猜测是,这种富文本的耗时计算,甚至并不是,在子线程中计算然后callback回来主线程刷新UI。而是直接在主线程计算并刷新的!!!因为在展示普通文本的那几帧画面时,这个页面是完全卡死的,没有任何动画效果,这非常符合主线程做耗时操作卡死UI的特征。看来苹果都有垃圾代码,那我写点bug也是情有可原的吧。(手动狗头) 玩笑归玩笑,其实这种表现倒也是符合Apple近年来的策略方针,Apple一直在致力于推进SwiftUI,这种webView + H5方案或者标签语言转富文本的操作与基于原生的SwiftUI是互为对立面的。那苹果对这方面不上心也就情有可原了。

欧盟努力了十多年,致力于推动苹果将 Lightning 充电线改为 Type-C 。终于在23年的9月份。在iPhone 15系列机型上成功落地。希望反垄断组织继续努力,早日督促苹果开源,在我”有生之年“可以验证下自己的猜测。

作者简介

nuc_zb,移动研发高级工程师

招聘信息

拍码场

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

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

相关文章

恒创科技:如何知道一台服务器能承载多少用户?

如何知道一台服务器能承载多少用户?其实服务器承载能力并非一个单一固定的数值&#xff0c;而是由多种因素共同决定的动态指标&#xff0c;所以想知道能承载的访客量&#xff0c;我们要先搞清楚究竟有哪些因素会影响服务器承载访客的数量。 影响服务器承载访客的因素&#xff…

vue3配置eslint代码规划和prettier自动格式化

eslint 安装依赖&#xff1a;npm install -D eslint/create-config初始化&#xff1a;npx eslint --init初始化后会在项目中自动创建eslint.config.js文件&#xff0c;把以下内容复制粘粘替换 import globals from "globals"; import pluginJs from "eslint/j…

6个步骤让你快速学会甘特图的制作

在项目管理中&#xff0c;一个清晰、详细的进度表格和进度计划表是确保项目顺利进行的重要工具。它们不仅帮助团队成员了解各自的任务和责任&#xff0c;还能有效监控项目的进展&#xff0c;及时发现并解决问题。 制作项目进度表格和制定项目进度计划表是项目管理中的关键步骤…

蒙特卡洛方法(MC Basic算法例子)

本文章中使用的算法和例子来源于bilibili中西湖大学赵世钰老师的【强化学习的数学原理】课程。网址&#xff1a;第5课-蒙特卡洛方法&#xff08;MC Basic算法例子&#xff09;_哔哩哔哩_bilibili 目录 一、任务目标 二、细节分析 三、代码演示 一、任务目标 1、初始的策略已…

数理统计(第4章第1节:1元方差分析)

目录 引例 基本概念 1元方差分析的数学模型 ​编辑离差平方和​编辑​编辑​编辑​编辑​编辑 单因子方差分析的假设检验​编辑 1元方差分析表 例子 引例 基本概念 1元方差分析的数学模型 离差平方和 单因子方差分析的假设检验 1元方差分析表 例子

Python酷库之旅-第三方库Pandas(192)

目录 一、用法精讲 891、pandas.Index.nunique方法 891-1、语法 891-2、参数 891-3、功能 891-4、返回值 891-5、说明 891-6、用法 891-6-1、数据准备 891-6-2、代码示例 891-6-3、结果输出 892、pandas.Index.value_counts方法 892-1、语法 892-2、参数 892-3、…

光控资本:普通股东、控股股东、大股东、实际控制人都是什么意思?

1、一般股东 一般股东是指持有公司一般股股份的出资者。一般股是指在公司的经营管理和盈利及财产的分配上享有一般权力的股份。 一般股的权力&#xff1a; 1、获得股利的权力。一般股股东在股市付出完债息、优先股股息后&#xff0c;能够获得股利&#xff0c;具体有多少要看…

SELS-SSL/TLS

一、了解公钥加密&#xff08;非对称加密&#xff09; 非对称加密中&#xff0c;用于加密数据的密钥与用于解密数据的密钥不同。私钥仅所有者知晓&#xff0c;而公钥则可自由分发。发送方使用接收方的公钥对数据进行加密&#xff0c;数据仅能使用相应的私钥进行解密。 你可以将…

openfoam中生成的3d案例提取得到slice后的2d案例

问题&#xff1a; 由于前期准备做3d的案例&#xff0c;并且模拟也比较费时间&#xff0c;现在生成了几十份3d的数据&#xff0c;但是现在只想要2d的数据来演示&#xff0c;该如何提取或者转换呢&#xff1f; 解决方法&#xff1a; 1.说明图片中的每个2d视图的points都是恒定不…

【SPIE出版,EI检索稳定】2024年人机交互与虚拟现实国际会议(HCIVR 2024,11月15-17日)

2024年人机交互与虚拟现实国际会议&#xff08;HCIVR 2024&#xff09; 2024 International Conference on Human-Computer Interaction and Virtual Reality 官方信息 会议官网&#xff1a;www.hcivr.org 2024 International Conference on Human-Computer Interaction and …

计算机网络 -- HTTP 协议详解

根据以往的内容我们可以得知&#xff0c;大多数网络协议一共有五层标准&#xff0c;今天我们将 探索 应用层的 HTTP 协议。 一 什么是HTTP协议 协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则。 HTTP 协议 &#xff0c;全称超文本传输协议 &#…

算法【Java】—— 动态规划之斐波那契数列模型

动态规划 动态规划的思路一共有五个步骤&#xff1a; 状态表示&#xff1a;由经验和题目要求得出&#xff0c;这个确实有点抽象&#xff0c;下面的题目会带大家慢慢感受状态标识状态转移方程初始化&#xff1a;避免越界访问 dp 表&#xff0c;所以在进行填表之前我们要预先填…

kafka使用指南

文章目录 前言特点架构一、zookeeper安装配置二、kafka安装配置三、快去试一下吧&#xff01;下一章:kafka命令之分区接入创建删除 前言 随着大数据时代的到来&#xff0c;高吞吐量的分布式发布订阅消息系统kafka得到了极大的应用&#xff0c;它具有高吞吐量、 特点 高吞吐量…

Windows 服务器中用户的分类

Windows 服务器中用户的分类 本地用户&#xff08;只能在本地登录&#xff09;如果你的服务器升级为域成员服务器&#xff0c;即刻失去本地服务。 漫游用户&#xff08;域用户就是漫游用户&#xff0c;可用在域内的任何一个设备上、且在权限允许的范围内进行登录和资源使用。 …

基于YOLO11/v10/v8/v5深度学习的建筑墙面损伤检测系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发…

Sublime Text 的PHP格式化插件phpfmt 的 setting 配置参数说明

phpfmt.sublime-settings 是 Sublime Text 中 phpfmt 插件的配置文件&#xff0c;用于定义代码格式化的各种参数。以下是一些常见的配置参数及其说明&#xff1a; 1、version 指定配置文件的版本&#xff0c;根据 phpfmt 插件的版本&#xff0c;此值可能有所不同。 2、php_b…

Oracle视频基础1.2.1练习

1.2.1 需求&#xff1a; 完整格式查看所有用户进程判断oracle启动状态 连接sqlplus不登陆 以sysdba身份登陆&#xff0c;通过登陆信息判断oracle启动状态 启动数据库&#xff0c;查系统全局区动态组件表 使用shell&#xff0c;启动监听然后返回sql ps -ef sqlplus /nolog con…

Ajax学习

目录 一、是什么 二、jQuery.ajax 三、初实现 四、再实现 五、应用 一、是什么 AJAX&#xff1a;Asynchronous JavaScript and XML&#xff08;异步的JavaScript和XML&#xff09; 是一种在无需重新加载整个网页的情况下&#xff0c;能够更新部分网页的技术 应用&#…

音频中sample rate是什么意思?

‌sample rate‌在数字信号处理中&#xff0c;指的是‌采样频率‌&#xff0c;即每秒钟从连续信号中抽取的样本数量。采样频率越高&#xff0c;信号的还原度越高&#xff0c;但同时也会增加计算负担和存储需求‌。 实际应用场景 在音频处理中&#xff0c;设置合适的采样率可以…

RabbitMQ客户端应用开发实战

这一章节我们将快速完成RabbitMQ客户端基础功能的开发实战。 一、回顾RabbitMQ基础概念 这个RabbitMQ的核心组件&#xff0c;是进行应用开发的基础。 二、RabbitMQ基础编程模型 RabbitMQ提供了很多种主流编程语言的客户端支持。这里我们只分析Java语言的客户端。 上一章节提…