cover_image

爱彼迎以用户体验驱动的 iOS 性能度量

爱彼迎技术团队 爱彼迎技术团队
2024年09月09日 11:02

这篇文章是爱彼迎页面性能分数系列文章的其中之一,页面性能分数用于测量各个平台上真实用户体验的多个性能指标。系列文章:第一部分第二部分。 


在爱彼迎,我们创建了页面性能分数(Page Performance Score),为工程师和数据科学家提供了多种以用户为中心的性能指标,以帮助他们更好地理解和改进我们的产品。在本文中,我们将深入探讨如何定义这些指标,以及在 iOS 上的应用。

图片


页面系统


爱彼迎上的整个用户旅程被划分为不同的页面,每个页面都有自己的页面性能分数(PPS)。为了支持这个基于页面的性能跟踪系统,我们建立了一个标准化的基础设施,使工程师能够配置页面和功能的对应关系。


在 iOS 上,页面与 UIViewController相关联。我们在 UIViewController 的生命周期中收集性能数据,在 viewDidDisappear 时发送收集到的日志。我们使用 PageName 作为全局的页面标识符,日志必须存在 PageName 时才能创建或发送。


插桩


由于仪表化这些指标涉及许多边缘情况和复杂性,我们创建了一个页面性能分数状态机类,称为 PPSStateMachine。这个类封装了跟踪和计算性能指标并生成日志事件的所有逻辑。工程师都可以获取与其 UIViewController 相关联的 PPSStateMachine,并在 UIViewController 的生命周期事件中调用相关方法来记录 PPS 事件。为了进一步简化工程师的工作,我们还创建了其他工具和基础设施,工程师只需配置页面名称和内容状态 (例如加载中、已加载或错误) 就可以使用 PPSStateMachine。 


页面性能分数状态机


public protocol PPSStateMachineProtocol {  init()
func handleDidLoad() func handleWillAppear() func handleDidLayoutSubviews() func handleDidAppear() func handleDidDisappear()
func handleDidRender( isLoading: Bool, isInitialLoading: Bool?, isError: Bool)
func handleImageLoad( imageView: UIImageView, url: URL?, status: LoadingStatus)}


时间


在测量性能时,所有时间都以纳秒为单位进行测量,然后转换为毫秒。通过为纳秒(UInt64) 和毫秒(Float64) 创建类型别名,我们让工程师更加关注使用跟合适的时间单位。在获取当前时间时,我们使用单调时钟 (monotonic clock),单调时钟所给出的时间是单调递增的,即使系统处于睡眠状态也会继续递增。这个值是 64 位纳秒类型。


public typealias Nanoseconds = UInt64
public final class MonotonicClock { static public func current() -> Nanoseconds { clock_gettime_nsec_np(CLOCK_MONOTONIC) }}


在标记时间的开始和结束时,我们使用一个变量返回以毫秒为单位的当前时间。这样我们就可以避免大多数由于类型转换而导致的精度错误。 


typealias Milliseconds = Float64
private var currentTime: Milliseconds { Float64(MonotonicClock.current()) / 1000000.0}


示例


// When the first loading indicator is set.let currentLoadTime = LoadTime(  startTime: currentTime,  endTime: nil)
content.initialLoadTime = currentLoadTime
// One render cycle after the first content is set.onNextRunLoop { [weak self] in self?.content.initialLoadTime.endTime = currentTime}


视图关联


每个 UIViewController 都有一个关联的 PPSStateMachine。如果开发人员想要在同一个名称下测量一系列页面时,可以重写这个 PPSStateMachine。通过与 UIViewController 关联,可以通过遍历视图响应链在 UIView 上找到这个 PPSStateMachine。 


版本控制


在 PPS 协议中声明生命周期和语义方法能让我们能够抽象出分数的计算方式。大多数对 PPS 公式的更新——除了全新的指标(如视频性能)外——都不会导致开发者需要更新他们各自的功能。在具体的实现中,对公式的任何重大更改首先通过将潜在值放入日志事件的元数据中进行测试。一旦潜在值被验证,它就可以升级为影响页面性能评分的正式值。 


指标定义


首次布局时间(TTFL)


TTFL 从 UIViewController 的 viewDidLoad 开始,直到 UIViewController 的第一个viewDidLayoutSubviews 结束。 


初始加载时间(TTIL)


TTIL 从 UIViewController 的 viewDidLoad 开始,直到内容加载后的一个渲染周期结束。


图片


截图仅用于演示,不表示爱彼迎里的真实内容


滚动线程卡顿(STH)


被报告的 STH 为卡顿持续时间的差值,过滤条件是至少两倍刷新率的最小阈值和最大帧持续时间长。 


CADisplayLink 能够准确观察到大多数 STH。RunLoop.Mode为RunLoop.Mode.Tracking。每次触发 CADisplayLink 时,我们都会基于旧帧和当前帧进行计算。


图片


截图仅用于演示,不表示爱彼迎里的真实内容


主线程挂起(MTH)跟踪可以在 iOS 上实现,但准确跟踪 MTH 会对性能产生小但持续的影响。在我们对 MTH 跟踪的测试中,CPU 无法休眠,电池电量消耗加快,而且这一指标并没有提供比 STH 更多的视觉感知性能信息。因此,我们决定不在 iOS 上测量 MTH。 


额外加载时间(ALT)


ALT 在加载动画显示后开始,并在加载动画消失并出现内容后的一个渲染周期结束。例如,在无限滚动的场景下。如果在下一页加载完成之前到达底部,则记录的 ALT 是加载动画(或底部)可见的时间直到下一页加载完成的时间。如果永远无法到达底部,例如有预加载,那么将记录 ALT 为零。为了准确记录日志,我们需要知道滚动百分比、底部加载动画是否可见,并且需要一个状态机来跟踪旧状态。


图片


截图仅用于演示,不表示爱彼迎里的真实内容


富内容加载时间(RCLT)


我们使用视图抽象层 URLImageView 完全隐藏了RCLT对工程师的影响,URLImageView 能够显示来自 URL 的图像。RCLT 只跟踪加载动画或占位资源可见的时间。那么隐藏的动作标志着 RCLT 的结束。在每次 URLImageView 状态变化时,通过遍历视图的响应链找到相应的 PPSStateMachine,并更新状态机。PPSStateMachine 将计算持续时间,如果持续时间低于指定的阈值,我们会删除URL部分,仅保存持续时间,这样日志不会太大。


图片


截图仅用于演示,不表示爱彼迎里的真实内容


总结


我们在 iOS 上的 PPS 实现使工程师能够快速实施和获取真实的性能数据。我们正在不断发展和扩展我们的工具和基础设施。我们希望您能在您的公司中应用和推进我们的研究成果。 


图片


作者:Nick Miller

译者:Harry Zhang

校对:Ming Shang, Keyao Yang


如果你想了解关于爱彼迎技术的更多进展,欢迎关注我们的 Github 账号(https://github.com/airbnb/) 以及微信公众号(爱彼迎技术团队)。


图片


继续滑动看下一个
爱彼迎技术团队
向上滑动看下一个