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

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

背景

在我们的业务场景下,为突出诸如 “利益点”和“利率” 等特性以推动订单成交,引入了 “富文本” 这一概念。富文本具备丰富格式的文本展示与编辑功能。然而,恰是由于富文本具有 “多样式”“复杂排版” 等特质,致使其在复杂元素渲染过程中会耗费更多系统资源。相较于简洁的纯文本,富文本在加载与显示时或许会产生延迟现象,尤其是处理大量富文本内容或在较老旧的 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.mzph.cn/pingmian/59113.shtml

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

相关文章

openstack之guardian介绍与实例创建过程

运行特征 采集模块&#xff1a;扩展Ceilometer&#xff0c;采集存储网、业务网连通性、nova目录是否可读写&#xff1b; 收集模块&#xff1a;将采集到的数据存储到数据库中&#xff1b; 分析模块&#xff1a;根据采集的结果&#xff0c;分析各节点状态&#xff0c;并进行反向检…

AVLTree

1.AVL树的概念 二叉搜索树虽然可以提高查找的效率&#xff0c;但是如果数据有序或者接近有序&#xff0c;二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。为了解决该问题&#xff0c;于是就有了AVLTree。即当向二叉搜索树中插入…

【数据结构二叉树】C非递归算法实现二叉树的先序、中序、后序遍历

引言: 遍历二叉树&#xff1a;指按某条搜索路径巡访二叉树中每个结点&#xff0c;使得每个结点均被访问一次&#xff0c;而且仅被访问一次。 除了层次遍历外&#xff0c;二叉树有三个重要的遍历方法&#xff1a;先序遍历、中序遍历、后序遍历。 1、递归算法实现先序、中序、后…

深入学习 Scrapy 框架:从入门到精通的全面指南

深入学习 Scrapy 框架&#xff1a;从入门到精通的全面指南 引言 在数据驱动的时代&#xff0c;网络爬虫成为了获取信息的重要工具。Scrapy 是一个强大的 Python 爬虫框架&#xff0c;专为快速高效地提取网页数据而设计。本文将深入探讨 Scrapy 的使用&#xff0c;从基础知识到…

蓝桥杯 区间移位--二分、枚举

题目 代码 #include <stdio.h> #include <string.h> #include <vector> #include <algorithm> #include <iostream> using namespace std; struct node{ int a,b; }; vector<node> q; bool cmp(node x,node y){ return x.b <…

SpringBoot+VUE2完成WebSocket聊天(数据入库)

下载依赖 <!-- websocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- MybatisPlus --><dependency><groupId>com.ba…

图文深入介绍Oracle DB link(一)

1. 引言&#xff1a; 本文图文深入介绍Oracle DB link&#xff0c;先介绍基本概念。 2.DB link的定义 数据库链接&#xff08;Database Link&#xff0c;简称 DB Link&#xff09;是 Oracle 数据库中的一个重要功能。它是一种在一个 Oracle 数据库实例中访问另一个 Oracle 数…

MoonBit 双周报 Vol.59:新增编译器常量支持,改进未使用警告,支持跨包函数导入...多个关键技术持续优化中!

2024-11-04 MoonBit更新 增加了编译期常量的支持。常量的名字以大写字母开头&#xff0c;用语法 const C ... 声明。常量的类型必须是内建的数字类型或 String。常量可以当作普通的值使用&#xff0c;也可以用于模式匹配。常量的值目前只能是字面量&#xff1a; const MIN_…

HTB:Shocker[WriteUP]

目录 连接至HTB服务器并启动靶机 1.How many TCP ports are listening on Shocker? 使用nmap对靶机TCP端口进行开放扫描 2.What is the name of the directory available on the webserver that is a standard name known for running scripts via the Common Gateway Int…

力扣——另一个的子树(C语言)

1.题目&#xff1a; 给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree …

【C++】对左值引用右值引用的深入理解(右值引用与移动语义)

&#x1f308; 个人主页&#xff1a;谁在夜里看海. &#x1f525; 个人专栏&#xff1a;《C系列》《Linux系列》 ⛰️ 天高地阔&#xff0c;欲往观之。 ​ 目录 前言&#xff1a;对引用的底层理解 一、左值与右值 提问&#xff1a;左值在左&#xff0c;右值在右&#xff1f;…

解决 ClickHouse 高可用集群中 VRID 冲突问题:基于 chproxy 和 keepalived 的实践分析

Part1背景描述 近期&#xff0c;我们部署了两套 ClickHouse 生产集群&#xff0c;分别位于同城的两个数据中心。这两套集群的数据保持一致&#xff0c;以便在一个数据中心发生故障时&#xff0c;能够迅速切换应用至另一个数据中心的 ClickHouse 实例&#xff0c;确保服务连续性…

B2C电商平台如何提升转化率 小程序商城如何做好运营

在竞争激烈的电商市场中&#xff0c;提升转化率是每个B2C电商平台的重要目标。转化率直接影响销售业绩和盈利能力&#xff0c;因此&#xff0c;了解如何优化用户体验、增强客户信任和提高购买动机是至关重要的。商淘云分享一些有效的策略&#xff0c;帮助B2C电商平台提升转化率…

RK3568平台开发系列讲解(字符设备驱动篇)Linux设备分类

🚀返回专栏总目录 文章目录 一、字符设备(是以字节为单位进行输入输出)二、块设备:块设备是以块为单位进行输入输出三、网络设备沉淀、分享、成长,让自己和他人都能有所收获!😄 一、字符设备(是以字节为单位进行输入输出) 串口、鼠标 字符设备没有固定的大小,也没…

STM32Fxx读写eeprom(AT24C16)

一.I2C 协议简介 I2C 通讯协议 (Inter &#xff0d; Integrated Circuit) 是由 Phiilps 公司开发的&#xff0c;由于它引脚少&#xff0c;硬件实现简单&#xff0c;可扩展性强&#xff0c;不需要 USART、CAN 等通讯协议的外部收发设备&#xff0c;现在被广泛地使用在系统内多个…

idea使用Translation插件实现翻译

1.打开idea&#xff0c;settings&#xff0c;选择plugins&#xff0c;搜索插件Translation&#xff0c;安装 2.选择翻译引擎 3.配置引擎&#xff0c;以有道词典为例 3.1 获取应用ID&#xff0c;应用秘钥 3.1.1 创建应用 点击进入有道智云控制台 3.1.2 复制ID和秘钥 3.2 idea设…

【论文精读】LPT: Long-tailed prompt tuning for image classification

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;论文精读_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 摘要 2. …

【SQL Server】解决因使用 varchar 类型存储 Unicode 字符串导致的中文显示乱码问题

问题描述 导入 SQL 到 SQL Server 数据库后&#xff0c;存在部分列的中文显示异常的问题。 原因分析 观察发现显示异常的字段的数据类型是 varchar&#xff0c;而显示正常的字段的数据类型是 nvarchar。 而且&#xff0c;SQL 文件中所有字符串前面都带有 N 的前缀。 在 SQL 中…

dify实战案例分享-基于多模态模型的发票识别

1 什么是dify Dify是一个开源的大语言模型&#xff08;LLM&#xff09;应用开发平台&#xff0c;旨在简化和加速生成式AI应用的创建和部署。它结合了后端即服务&#xff08;Backend as Service, BaaS&#xff09;和LLMOps的理念&#xff0c;使开发者能够快速搭建生产级的AI应用…

电机控制储备知识 一 电机驱动本质分析以及磁相关的使用场景

一&#xff1a;电机旋转的原因 1.电机基本认识 &#xff08;1&#xff09;电机是一种动力装置&#xff0c;能够将电能转换为动能 电机拥有体积小 、动力足&#xff0c;控制精细灵活的特点 完整的电机系统&#xff1a;电机&#xff08;减速器 传感器&#xff09; 电机驱动器&a…