当前位置: 首页 > 科技观察

从47%到80%,携程酒店APP流畅度提升实践

时间:2023-03-12 17:40:42 科技观察

作者|Jin,携程高级研发经理,专注于移动端技术开发;Dan,携程测试开发经理,专注于数据挖掘和数据在系统质量提升中的应用;Lanbo,携程软件技术专家,专注于移动端技术开发。1、后台APP性能提升一直是研发团队永恒的主题。在APP性能优化的实践中,除了性能技术方案本身,还会存在两个问题:第一,APP的性能优化是不可持续的,往往经过一段时间的优化实践,效果明显,但随着后续需求随着迭代和代码变更,APP性能很难保持在一个好的水平;其次,缺乏一套科学的量化方法来衡量APP性能的提升。引用管理大师彼得·德鲁克的话:如果你不能衡量它,你就不能改进它,如果你不能衡量它,你就不能改进它。基于此,携程酒店APP前端团队进行了深入的思考和探索,希望通过量化、治理、监控,不断提升APP性能和用户体验。2、流畅度指标定义了流畅度,简单来说就是衡量用户使用APP体验的一部分。是用户快速、无障碍地使用APP的体验指标。主要包括稳定性、速度和质量三个方面。稳定是指用户打开特定页面时,没有出现白屏、死机、闪烁等情况。快速是指页面打开速度快,用户与页面交互时操作流畅、自然。质量的意思就是在浏览页面的时候,没有不合理的弹窗拦截打断用户的操作。如下图所示:基于以上理论基础,APP中的白屏、闪退、加载缓慢、卡顿、闪烁、报错等都是导致用户在感知层面感觉不流畅的因素。因此,我们提出了流畅率量化指标,定义用户在页面上触发的用户页面PV和二次加载次数之和作为流畅率的分母,即样本总数,如下:样本量=pagepv+secondaryloading统计pv去重后页面加载慢/页面卡顿/图片/视频加载慢的次数,加上页面崩溃、滑动卡顿、图片/视频加载失败、全局弹窗错误、输入失焦,和按钮点击无效,二次加载失败,二次加载慢等异常情况的总和定义为不平滑因素的个数。那么流畅率的公式定义为:流畅率=(样本量-不流畅因子个数)/样本量用以下公式表示:页面交互加载时间(TTI)=页面本地渲染时间+服务网络加载时间文本在该区域确定用户何时可以交互。我们的技术栈大致分为Flutter和携程ReactNative。下面分别介绍加载时间采集的原理。2.2.1Flutter页面交互加载时间采集原理在Flutter中,最终的UI树其实是由独立的Element节点组成的。UI从创建到渲染的大致流程是:根据Widgets生成Element,然后创建对应的RenderObjects并关联到Element.renderObject属性,最后通过RenderObjects完成布局排列和绘制。如下图所示:因此,可以从根节点开始遍历Elements,直到在扫描窗口中找到Text组件,并且该组件的内容不为空,则可以判断该页面TTI检测成功.Flutter提供了如下接口来支持Element遍历:voidvisitChildElements(ElementVisitorvisitor)2.2.2携程ReactNative页面交互加载时间采集原理我们知道ReactNative最终是由Native组件渲染的。在iOS/Android中,可以从根View递归地从View树中找到Text文本控件。获取页面文字内容,除顶部固定静态显示和页面底部静态显示区域外,如果扫描到的文字数大于1,则认为页面TTI检测成功.2.3渲染卡顿和帧率Google对卡顿的定义:界面渲染是指从应用程序生成帧并将其显示在屏幕上的动作。为确保用户与应用程序的流畅交互,应用程序渲染每帧的时间不应超过16毫秒,以达到每秒60帧。如果应用程序存在界面渲染慢的问题,系统将不得不跳过一些帧,这会导致用户感觉应用程序不流畅。我们称这种情况为口吃。2.3.1卡顿标准判断一个应用是否卡顿,要从应用类型是普通应用还是游戏应用开始。不同类型的应用对应不同的卡顿标准。普通应用可以参考Google的AndroidVitals性能指标,游戏可以参考腾讯的PrefDog性能指标。因为我们的APP是普通应用,所以简单介绍一下GoogleVitals中滞后的定义。GoogleVitals将卡顿分为两类:第一类是渲染速度慢:在渲染速度慢、帧数较多的页面上,当超过50%的帧渲染时间超过16ms毫秒时,用户的感知明显卡顿.第二类是卡顿:卡顿的绘制耗时超过700ms,属于严重的卡顿问题。另外需要注意的是,FPS的高低与卡顿没有必然关系。高帧率FPS不反映平滑度或无冻结。例如:FPS为50帧,前200ms渲染一帧,最后800ms渲染49帧。虽然帧率为50,但感觉还是很卡。同时,低帧率FPS并不意味着卡顿,比如没有卡顿时平均FPS为15帧。2.3.2冻结量化了解冻结的标准和原理后,可以得出结论,只有丢帧才能准确判断是否冻结。Flutter官方提供了一套基于SchedulerBinding.addTimingsCallback回调的实时帧数据监听。当flutter页面有view绘图刷新时,系统会吐出一系列的FrameTiming数据。FrameTiming的数据结构如下:vsyncStart,buildStart,buildFinish,rasterStart,rasterFinishvsyncStart变量表示当前帧绘制的开始时间,buildStart/buildFinish表示WidgetTree的构建时间。rasterStart/rasterFinsih代表上屏光栅化时间,所以一帧的总渲染时间可以用下面的公式得到:totalSpan=>rasterFinish-syncStart对应GoogleAndroidVitalsfreeze标准:如果一帧totalSpan>700ms,则认为发生了帧卡顿,导致卡顿严重;如果1s内超过30帧,绘制时间totalSpan>16ms,导致渲染速度变慢。3.流畅度监测方案在流畅度监测系统中,针对不流畅的感知因素进行单项分析和挖掘,旨在维持或提升现有的用户体验,同时迭代优化。监测体系建设分为现状及优化方向挖掘、依托数据补全的监测指标、多维数据监测、指标监测预警。在监控建设前期,会分析APP现有性能状况,寻找优化方向,初步获取优化带来的预期收益、受影响用户数等信息。例如:希望通过预加载的方式来降低用户加载慢的速度,通过分析各种场景下不同的用户操作,以及客户端和服务端技术实现的现状(酒店主服务返回数据包大小统计)、酒店详情纯前端渲染时间等)来确定慢加载的覆盖范围和触发时机,以达到更好的效果。下一步,在启动流畅度优化业务和技术升级的同时,补充相应的监控场景埋点,支持流畅度测量和测量数据,为后续的监控预警打下坚实的基础。监测体系的核心是大范围、多维度的传播监测。海量数据(如下图)可以快速宏观地了解用户预订体验,明确流畅度提升进度;并通过各个维度的数据表,找到改进目标,监控优化效果。在实际监控中,会针对不同的指标设计不同的监控标准,例如:加载缓慢、白屏、崩溃、卡顿等系统因素。除市场指标外,各指标影响比、酒店首页报错率趋势、版本对比趋势、报错模型TOP分布等。针对业务场景中较为严重的因素,结合业务数据,进行基于桶的监控,如:详情页房型数量关联TTI耗时分布、单体酒店崩溃数据等,并与AB实验系统对接。业务和技改需求可以在AB系统中配置流畅度观察指标,比较业务或技改需求对流畅度指标的影响,作为实验是否通过的考量指标。对各项指标的个别波动进行预警,做到有涨就预警,有新增就预警,做到不漏一漏一漏。例如:填写页面业务错误数(可订购服务、提交订单、失焦错误数),除了监测各种错误率的趋势外,还会结合实际用户流量来区分单个业务错误的流量大小进行预警,并通过拆分多个维度(单用户、单房型等)的触发数量,轻松发现特征badcase,快速定位用户遇到的问题,挖掘更多业务优化点。4、流畅度管理实践在APP流畅度管理方面,主要从页面启动加载速度、长列表冻结管理、页面加载闪退三个方面进行了很多优化实践。这些优化不涉及高级底层引擎优化。技术没有复杂的数学理论基础,更谈不上重新发明轮子。我们坚持以数据为导向,用数据驱动解决方案,用数据验证方案,发现问题,提出方案,解决问题。4.1页面加载速度优化在页面加载速度优化方面,我们从2021年8月开始进行了迭代优化,目前酒店预订流程页面加载缓慢率已从初始值42.90%降低至8.05%当前阶段。在页面启动和加载速度优化方面,一般采用数据预获取方案。其原理是在上一个页面提前获取服务数据,在用户跳转到当前页面时直接从缓存中获取,节省了数据网络传输时间,实现了快速展示当前页面内容的效果。目前酒店的核心预订流程都使用了数据预加载技术,如下图所示:结合酒店业务的特点,数据预加载需要考虑几个方面:第一,酒店的页面PV预订流程高,酒店列表和详情页PV在千万级别。需要考虑数据预加载的时机,避免服务资源的浪费;其次,酒店列表、详情、订单填写页面都有价格信息,对于用户来说是动态信息,可能实时变化,所以数据需要考虑Preloaded缓存策略,避免因价格不一致导致用户误会。4.2Flutter服务通道优化携程APP采用的私有服务协议。目前发送服务的动作还在原生代码中,酒店核心页面已经转移到Flutter中。通过Flutter框架提供的通道技术,从Native到Flutter的数据传输通道,需要对数据进行额外的序列化和反序列化传输。同时传输过程比较耗时,会阻塞UI主渲染线程,影响页面。加载可以产生明显的效果。我们检测到这个环节后,和公司的框架团队一起改造了Flutter的底层框架,可以实现数据流的直接透传,不会阻塞UI主线程,性能有了很大的提升。在优化之前,服务返回的数据流传递给Flutter使用。整个过程要经过以下四个步骤:PB反序列化ReponsetoJsonString编码JsonStringtoFlutter通道传输JsonStringtoReponse解码整个过程链接长度长,数据传输量大,效率低,影响页面加载性能,如下图所示:经过改造后,服务返回的数据流直接传输到Flutter端,直接在Flutter上进行PB反序列化,传输性能大大提升。PB数据流Flutter通道传输PB反序列化到Reponse整个流程链路短,数据传输量小,效率高,如下图所示:4.3卡顿问题分析定位在Flutter中,可以使用性能层(PerformanceOverlay),来分析渲染滞后问题。如果UI卡住了,可以帮助我们分析查找原因。如下图所示:GPU线程的绘制表现在图表的上方,CPUUI线程的绘制表现在图表的下方。蓝色竖线代表渲染帧,绿色竖线代表当前帧。为了保持60Hz的刷新率,每帧应该少于16ms(1/60秒)。如果其中一帧处理时间过长,会导致界面卡顿,图形中会显示红色竖条。下图演示了应用需要时间渲染绘制时性能层的显示样式:如果GPU线程图表上出现红色竖条,说明渲染的图形过于复杂,无法快速渲染;而如果出现了IftheUIthreadgraphismissing,说明Dart代码消耗了大量资源,代码执行时间需要优化。另外,我们可以使用AS中的FlutterPerformance工具来查看Flutter页面的渲染性能。有一个非常有用的函数Widgetrebuildstats,它统计了渲染UI时widget重建的次数,可以帮助我们快速定位出问题的widget,如下图:UICPUthreadproblemLocatingUIthreadproblem其实就是应用性能的瓶颈。比如在构建Widget的时候,在build方法中使用了一些复杂的操作,或者在RootIsolate中进行了一些比较耗时的同步操作(比如IO)。这些将显着增加CPU处理时间并导致延迟。我们可以使用Flutter提供的Performance工具来记录应用程序的执行轨迹。Performance是一个强大的性能分析工具,可以将CPU的调用栈和执行时间以时间轴的形式显示出来,以检查代码中可疑的方法调用。点击FlutterPerformance工具栏中的“OpenDevTools”按钮后,系统会自动打开DartDevTools网页,我们就可以开始分析代码中的性能问题了。GPU问题定位GPU问题主要集中在底层渲染时间上。有时候Widget树很容易构造,但是在GPU线程下渲染很耗时。涉及Widget裁剪、遮罩等多视图叠加渲染,或者因缓存不足而重复绘制静态图片,都会显着降低GPU的渲染速度。您可以使用性能层提供的两个参数来检查多视图叠加。视图渲染开关checkerboardOffscreenLayers和图像开关checkerboardRasterCacheImages负责检查缓存。checkerboardOffscreenLayers多视图叠加通常使用Canvas中的savaLayer方法,在实现一些特定效果(比如半透明)时非常有用,但是由于其底层实现涉及到在GPU渲染上重复绘制多个图层,所以会造成较大的性能问题。为了检查saveLayer方法的使用,我们只需要在MaterialApp的初始化方法中将checkerboardOffscreenLayers开关设置为true,分析工具就会自动帮我们检测多视图叠加。使用saveLayer的Widget会自动以棋盘格式显示,并随着页面刷新而闪烁。但是saveLayer是一个比较底层的绘制方法,所以我们一般不会直接使用,而是在需要裁剪或者半透明遮罩的场景中,通过一些功能Widget来间接使用。所以一旦遇到这种情况,我们就需要思考是否一定要这样做,是否可以通过其他方式来实现。如下图,因为detailheaderbar使用了高斯模糊,并且使用了ClipRRect进行了圆角切割,ClipRRect会被转移到savelayer界面,所以这部分会出现闪烁。checkerboardRasterCacheImages从资源的角度来看,另一种非常耗费性能的操作是渲染图像。这是因为图像的渲染涉及I/O、GPU存储、不同通道的数据格式转换,所以渲染进程的构建会消耗大量的资源。为了减轻GPU的压力,Flutter提供了多级缓存快照,这样重建Widget时就不需要重新绘制静态图像了。与检查多视图叠加渲染的checkerboardOffscreenLayers参数类似,Flutter也提供了检查缓存图片的开关checkerboardRasterCacheImages,用于检测界面重绘时频繁闪烁的图片(即没有静态缓存)。我们可以将需要静态缓存的图片添加到RepaintBoundary中。RepaintBoundary可以确定Widget树的重绘边界。如果图片足够复杂,Flutter引擎会自动缓存,避免重复刷新。当然,由于缓存资源有限,如果引擎认为图片不够复杂,可能会忽略RepaintBoundary。4.4携程ReactNative(简称CRN)页面优化下图展示了CRN页面的基本加载流程。每个阶段的优化在之前的文章中都有介绍,比如容器预加载、Bundle拆分、容器复用、框架预加载等容器层面的优化。以酒店订单填写页面为例。本页面采用CRN架构。在优化了各种容器和框架层面后,我们着重于页面重绘的管理,将重绘管理做到极致。主要涉及上图中的“5.首屏第一次渲染”和“7.首屏二次渲染”。4.4.1页面Action集成本页面采用Redux架构。经过前期几年的粗放开发,页面上有很多动作(Action通过异步事件触发状态管理的改变,从而达到页面重绘的目的,可以参考Action-Reducer-StoreRedux模式)。优化前,如下图,页面初始化/加载/载入/载入时触发多个动作。由于动作是异步的,所以每个数据处理模块都是耗时和异步的。页面加载完成后,页面可能已经Refresh了,这里可能会显示未处理的数据,后续动作执行完毕后会再次刷新页面。由于数据的变化,页面中的元素可能会发生变化,所以对于用户来说,页面会抖动,同时也会增加JS<=>Native的流量。页面中元素的不断变化,也会不断刷新native中的渲染树,消耗大量CPU时间,导致页面不流畅,耗时较长。针对上述情况,我们对页面中的action进行了整合:尽量避免使用静态数据触发相同时序的action。尽可能合并非必要的数据。延迟加载多层动作的更新。页面初始化,主服务返回,后续子服务的动作。在这个过程中,我们使用redux-logger方法监听action,使用MessageQueue方法监听action变化触发刷新,如下图所示:4.4.2控制重绘管理为了更好的控制重绘控制重绘频率,我们对控件的拆分方式如下:尽可能拆分组件,降低单个文件的复杂度组件复用更方便,依赖数据少,状态更好管理本地更新数据,不影响其他组件使用Fragments避免多层嵌入,封装拆分后,组件粒度更小。业务相关的弱组件使用PureComponent,业务强的组件使用Component+shouldComponentUpdate+比较属性是否自行改变,避免组件重绘。通过以上方式治理,可以明显看出进入填充页面时页面比较清淡。主服务返回后,页面可以立即刷新,页面渲染速度大大提升。重绘管理,我们采用了https://github.com/welldone-software/why-did-you-render来检测组件为什么重绘,如下图:5.规划总结整个APP的流畅度管理,流畅率从最初的47%提升到现在的80%,页面慢加载率从原来的45%下降到现在的8%,白屏率从1.9%下降到现在的0.3%,以及主进程页面控件闪烁基本消除,APP性能和用户体验得到显着提升。回顾过去六个月中国酒店APP流畅度的实践,整个过程艰辛,也时刻伴随着焦虑。流利度的每一点进步都不是一蹴而就的。但对于整个团队来说,收获是满满的。在整个实践过程中,我们对Flutter工程架构进行了全面的升级,特别是数据传输层的改造,业务层的逻辑关闭等;数据预加载方案也从1.0版本升级到2.0版本。最重要的是,整个团队形成了数据量化的思想和用户的角度去优化和解决问题。目前,流畅度2.0版本也已经上线。2.0在流畅度统计中增加了更多的不流畅感知因素,如主服务二次加载、地图加载慢、图片视频加载慢、图片视频加载失败、弹窗等。窗口和提示信息等,从更多的系统和业务层面提升用户的预订体验。