一、背景
在 App 运用进程中,页面流通性是仅次于 Crash 的影响用户体会的目标。在苹果新推出的 iPhone 13 Pro 和 Max 上支撑了 ProMotion,最大刷新率达到 120Hz,这使得用户对页面流通性导致的刷新率改改变为敏感。本文总结了雪球 iOS 客户端在社区事务中 feed 流页面和正文页流通性优化方面的工作,首要包括辨认/测验卡顿东西运用和卡顿优化实践两方面内容。
二、东西
界说卡顿
非高刷 iPhone 的刷新率为 60Hz,也便是 VSync 信号的频率,这要求一帧内容需求在 16.67ms 之内完结烘托。假如下一帧 B 的烘托时刻超过了 16.67ms,即在 VSync 信号到来之后才完结烘托,那么当时帧 A 会滞留在屏幕上,帧 B 需求再等待一次 VSync 信号才能烘托给用户。苹果将帧错过预期 VSync 信号称之为卡顿( Hitches )[1]。
当用户在页面上操作时,比如上下滑动页面或许页面跳转时,首要焦点会集在手势的交互上,卡顿表现为用户可感知的“抖动”。杰出的交互体会是提供流通的响应速度,反之用户将会感知到明显的卡顿,卡顿会影响用户体会,乃至让用户失去对 App 的爱好。
辨认卡顿
Instrument Animation Hitches
Instrument 中的 Animation Hitches 模板能够查看卡顿,下图中 Hitches 一栏展现了发生的卡顿,点击其中一个卡顿,下面会显现该卡顿类型,下图中卡顿6的 Hitch Type 为 Expensive Commits,说明是 commit 阶段的耗时过长导致了该卡顿。
在左上角筛选框中输入当时项目姓名,并圈选中形成卡顿的 commit,左下角切换为 Profile,经过 Time Profiler 东西查看该 commit 中耗时的调用。
Animation Hitches 东西能够检测到卡顿,并结合 Time Profiler 能够剖析形成卡顿的调用耗时。Time Profiler 的原理是采集运行线程的调用栈,然后以计算学的办法汇总,所以 Time Profiler 展现的并不是实际代码执行时刻,只是栈在采样计算中出现的时刻,如下图所示。所以 Time Profiler 只适用于粗粒度的剖析。
火焰图
假如需求精细化剖析形成卡顿的耗时调用,os_signpost 是一个牢靠的挑选,可是手动刺进很多 os_signpost 代码计算函数耗时功率比较低。hook objc_msgSend 能够计算音讯发送中的函数耗时,而在剖析 CPU 耗时调用中,火焰图对错常有用的东西,咱们将两者结合起来作为函数耗时细粒度剖析的东西。
Trace Event Format [2] 界说了一种火焰图数据格式,结合 hook objc_msgSend 计划,在办法调用开始和结束的当地打点,即可生成火焰图展现数据 [3]。下图是一段测验代码的火焰图,函数内部的子函数调用表现为笔直方向向下的“火焰”,函数调用栈越深,则向下延伸越深。在火焰图中,较平的底层”火焰”表示该函数或许存在功能问题。在测验代码中,-testFunction1_1_1_1_1 和 -testFunction1_1_1_2 函数内部线程睡眠了一段时刻,在示意图中表现为两个较平的底部,也便是说优先优化这两个函数取得的收益最大。此外,操控火焰图的函数调用深度束缚和最低函数耗时束缚两个变量能够操控计算细化程度。
Hitch ratio
削减函数消耗调用并不能直接转化为页面流通性目标,需求一个客观的目标来点评优化工作作用,WWDC20 [1] 界说了 Hitch time 和 Hitch ratio,Hitch time 是帧延迟显现的时刻(以 ms 为单位),Hitch ratio 是页面滑动或其他动画进程中每秒内 Hitch time 的比率(以 ms/s 为单位)。苹果选用 Hitch ratio 来量化页面卡顿,并给出了 Hitch ratio 的建议数值,以为 Hitch ratio 低于 5ms/s 时用户体会比较好。
XCTest 结构中的 UI 测验能够搜集 Hitch ratio,咱们测验了 feed 流页面和正文页优化前后的 Hitch ratio 来评判优化作用。
三、优化实践
经过上述东西剖析,雪球社区 feed 流页面和正文页的卡顿首要会集在富文本的解析和制作阶段,以及随着页面款式杂乱性增加而积累的束缚 remakeConstraints,下面首要针对这几项进行优化。
富文本优化
雪球的社区事务首要是围绕着富文本处理展开的,当富文本较为杂乱时,解析和制作均消耗很多时刻,是导致页面卡顿的最首要要素,下面首要从解析和制作两方面介绍富文本方面的优化。
富文本解析
上图为原有富文本解析流程,对特别 <a> 标签处刺进 <img> 标签,以及去除 HTML 标签括号等流程,需求多次遍历富文本。而当富文本中包括很多 <a> 标签或 <img> 标签时,占用了很多主线程时刻,形成了严峻的卡顿。
一次性遍历解析
咱们运用 DTCoreText 对现有富文本解析流程进行优化。DTCoreText 是开源的 iOS 富文本组件,能够一次性将 HTML+CSS 富文本转化为 NSAttributedString。DTCoreText 数据解析的流程如上图所示:
- 富文本 HTMLString 字符串传递入 DTAttributedStringBuilder,DTAttributedStringBuilder 接纳 DTHTMLParser 的回调生成 DOM 树,在 DTHTMLParser 的回调中能够增加处理特别 <a> 标签的流程。
- 生成的 DOM 树种每个节点都是自界说的 DTHTMLElement,经过 DTCSSStylesheet 解析每个元素对应的款式,这时每个 DTHTMLElement 已经包括了节点的内容和款式,最终从 DTHTMLElement生成 NSAttributedString。
DTCoreText 在解析富文本时,把解析进程暴露给运用者,经过回调函数告诉调用者当时解析到什么元素,让运用者决议怎么处理。所以 DTCoreText 是边解析边处理,只需求遍历一次富文本,因而咱们能够高效地完结在特别 <a> 标签刺进 <img> 标签等需求。
异步解析
DTAttributedStringBuilder 创立了 3 个行列:解析 html 的 _dataParsingQueue,生成 DOM 树的 _treeBuildingQueue,以及拼装 NSAttributedString 的 _stringAssemblyQueue,将解析进程分派到 3 个行列,经过 dispatch_group_wait 阻塞等待所有使命完结后回来成果。所以解析进程是在非主线程上完结的,能够异步解析富文本进一步削减主线程上的时刻消耗。
富文本制作
自界说文本异步制作
为了满足事务需求,feed 流页面运用 CoreText 来完成富文本制作。当富文本内容比较杂乱,尤其是包括表情较多时,在主线程制作时会形成严峻卡顿,因而选用异步制作来进行优化。
iOS 中 UIView 负责处理事件传递,而制作是经过 CALayer 来完结的,CALayer 经过“-(void)display”办法进行制作。异步制作便是经过继承 CALayer 并重写“-(void)display”办法,在内部将制作使命放在非主线程来完成。YYAsyncLayer [4] 是一个完成了异步制作的 CALayer,当它需求显现内容时,它会向 delegate,也便是 UIView 恳求一个异步制作的使命。
咱们运用 YYAsyncLayer 对 feed 流富文本进行异步制作,SNBTextLabel 为富文本显现组件,完成了YYAsyncLayer 界说的协议 YYTextAsyncLayerDelegate,经过“-(YYTextAsyncLayerDisplayTask *)newAsyncDisplayTask”创立异步制作使命,在异步制作使命内部调用了 SNBTextData 封装的原富文本制作流程。这种异步制作改造,对原制作流程侵入性较小,削减了测验回归点并确保上线质量。
YYLabel 异步制作优化
正文页的谈论区运用了 YYLabel,YYLabel 提供了 displaysAsynchronously 特点来操控是否敞开异步制作。可是当 YYLabel 敞开异步制作之后,谈论区在加载下一页 reloadData 存在闪烁 [5],这是由于 YYLabel 的 clearContentsBeforeAsynchronouslyDisplay 特点默以为 YES,在 YYLabel 重写的修正特点函数内,假如 displaysAsynchronously 和 clearContentsBeforeAsynchronouslyDisplay 一起为YES,则会先整理掉原有的内容。所以 reloadData 时即使 cell 中 YYLabel 的文字内容不变,还是会先整理掉已有的 layer.contents,然后再异步制作出新的 layer.contents,由所以异步的原因,中心会有一段时刻 YYLabel 内容是清空的,导致表现为闪烁。
// YYLabel.m
- (void)setTextColor:(UIColor *)textColor {
if (!textColor) {
textColor = [UIColor blackColor];
}
if (_textColor == textColor || [_textColor isEqual:textColor]) return;
_textColor = textColor;
_innerText.yy_color = textColor;
if (_innerText.length && !_ignoreCommonProperties) {
if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
[self _clearContents]; // 整理掉原有内容
}
[self _setLayoutNeedUpdate];
}
}
所以在当时运用场景下,将 YYLabel 的 displaysAsynchronously 设置为 YES 时,一起将clearContentsBeforeAsynchronouslyDisplay 设置为 NO,防止出现闪烁。
束缚布局优化
削减 remakeConstraints 次数
雪球 feed 流页面的 cell 承载着很多事务,存在着很多的 remakeConstraints 代码,是除了富文本制作和解析之外最耗时的函数调用。改成 Frame 布局能够消除掉这部分耗时,可是涉及到很多测验回归点,风险比较大。从另一个视点出发,在大多数情况下,feed 流 cell 显现的 UI 组件是一样的,并不需求每次设置数据时对各个子视图进行 remakeConstraints。
所以如下面代码所示,对视图组件 viewX,每次设置数据时查看已绑定的历史数据 _model 和新数据model 的区别,判断数据是否发生了需求更新 viewX 束缚的改变,从而削减 remkaeConstriants 的次数。
- (void)setModel:(Model *)model
{
BOOL needReLayoutViewX = [self measureRelayoutViewNecessary:model];
if (needReLayoutViewX) {
[self.viewX mas_remakeConstraints:^(MASConstraintMaker *make) {
// 设置束缚
}];
}
_model = model;
}
- (BOOL)measureRelayoutViewNecessary:(Model *)model
{
if (model && self.model && 'UI组件显现改变不满足') {
return NO;
}
return YES;
}
其他优化
削减视图创立和移除的次数
- 频繁地创立和移除视图也比较耗时。例如 feed 流 cell 中的9宫格图片部分,防止显现图片时每次都创立新的 UIImageView,而应该复用已创立的 UIImageView,当显现图片不够 9 张时只需求隐藏剩余的 UIImageView。
- 防止直接触发 UI 组件懒加载调用,只有当满足显现条件时才触发懒加载,否则能够运用实例变量来代替。
四、试验成果和总结
经过 XCTest 结构测验了 feed 流页面和正文页在模仿极端杂乱数据下的 Hitch ratio。经过多个版本的优化,在 iPhoneXs 上,feed 流页面的 Hitch ratio 由 16ms/s 下降到了 3.5ms/s;在 iPhone6s 上,正文页的 Hitch ratio 由优化前的 60ms/s 下降到了 5.5ms/s。
在雪球 iOS 社区页面的流通性优化实践中,经过 Instrument Animation Hitches 和火焰图能够定位commit 阶段耗时较多的函数调用,并针对几个头部耗时函数调用进行了优化,并经过 Hitch ratio 目标量化了优化作用,在低端手机上实际运用体会也得到了很大的提高。本文涉及到的优化点首要是 commit 阶段的耗时优化,关于烘托阶段的优化能够更多地参考苹果的技术分享 [6]。
五、引用
[1] Session 10077 – Eliminate animation hitches with XCTest
[2] Trace Event Format
[3] www.speedscope.app
[4] iOS 坚持界面流通的技巧
[5] github.com/ibireme/yyk…
[6] WWDC Demystify and eliminate hitches in the render phase