淘宝订单列表Fragment转场动画卡顿解决方案

图片

如何应对产品形态与产品节奏相对确定情况下转变为『在业务需求与产品形态高度不确定性的情况下,如何实现业务交付时间与交付质量的确定性』。我们希望通过混合架构(Native 业务容器 + Weex 2.0)作为未来交易终端架构的重要演进方向,在 Native 容器侧充分发挥原生语言的性能优势、常驻 App 的调控与管控能力、手势识别与交互优势来解决体验问题。本专题《淘宝交易终端架构探索》是我们摸索出的部分实践总结,欢迎大家一起交流进步。

第一篇:《Weex购物车长列表横滑操作优化“编年史”》

第二篇:《淘宝页面首帧优化的经验和心得》

第三篇:《淘宝App交易链路终端混合场景体验探索》

第四篇:《淘宝订单列表Fragment转场动画卡顿解决方案》(本篇)

第五篇:《探索淘宝购物车SurfaceView闪黑的解决方案》(待发布)

图片

背景

我们知道,在Android系统中Fragment的生命周期相对于Activity更加轻量级,Activity需要处理更多复杂的状态转换和资源管理,而Fragment只是在Activity之上叠加的一层视图组件。因此,Fragment的初始化和管理所耗费的系统资源和时间通常较少。直观来说,通过Fragment替换Activity,我们可以实现更短的点击响应耗时和更少的内存占用。

然而,使用Fragment同样也面临着一些问题,例如APM埋点错乱、复杂的状态管理、Fragment嵌套管理等问题。今天我们主要来看Fragment转场动画卡顿问题。

图片

现状分析

在Activity场景下订单列表首屏渲染流程大致如下所示:

8904f2a9b9c698a63260250a8c0d2463.jpeg

可以看到Activity的缓存渲染被我们延后到动画开始之后,这样可以大大缩短点击响应耗时。然而当我们保持渲染流程不变仅仅将页面容器从Activity切换为Fragment之后就会发现原本丝滑流畅的转场动画变得非常卡顿:

我们再来看看在低端机Y50上Fragment冷启动画的Trace情况:

e86430505bc244681a8256ec8d809ea6.png

从Trace中我们可以看出,整个动画过程中UI线程渲染首帧就占据了大半的时间,这正是因为我们将首屏的渲染放在了动画开始之后,阻塞了Fragment动画帧的绘制,导致第一帧动画就已经出现卡顿。此时动画剩余时间已经不多,后续动画则不可避免地出现丢帧现象。

图片

Fragment转场动画卡顿之谜

我们先来了解下为什么Fragment动画会卡顿而Activity则不会。简单来说就是:Activity转场动画不会被主线程阻塞,而Fragment动画则会被主线程任务阻塞。

1. 系统级管理 vs 应用级管理:

  • Activity转场动画:由Android系统的WindowManager组件进行管理。WindowManager在系统级别提供了优化的动画管理。因此,无论主线程在此期间进行何种UI绘制,系统层面的动画渲染都不受影响。

  • Fragment转场动画:通常由应用程序自行管理,并且动画是在主线程上执行的。当主线程被其他UI操作阻塞时(例如绘制复杂UI视图),会直接影响到Fragment的动画流畅性。

2. 硬件加速的不同深度:

  • Activity动画:由于Activity的转场是由系统窗口管理器处理,它们可以在更深层次上利用硬件加速,确保动画的流畅性即使在高负荷情况下也能维持。

  • Fragment动画:虽然也可以利用硬件加速,但由于其动画逻辑和生命周期管理在应用层实现,容易受到主应用线程任务的影响,特别是当没有充分利用异步处理和硬件加速时。

3. 动画与绘制同步机制:

  • Choreographer和VSync(VerticalSynchronization)的作用:系统级动画(如Activity切换)与屏幕的垂直同步(VSync)高度同步,使用Choreographer来安排绘制和动画,确保高效的刷新率。在Activity动画期间,绘图和动画事件能更加自然地与设备刷新率保持同步。

  • Fragment同步机制不强:Fragment管理器缺少像Choreographer这样的系统级优化机制来协调动画和绘图更新,这使得Fragment动画更容易在UI线程繁重时出现问题。

图片

丝滑转场探索

  1. 动画延后
  • 方案简介

顾名思义,动画延后就是在主线程的耗时任务即首屏渲染完成后再进行转场动画,此时主线程空闲,动画则不会卡顿。

  • 具体实现


  1. 取消缓存渲染延后,在Activity里我们将缓存渲染延后到了动画开始之后,在Fragment中我们在onCreate阶段就执行缓存渲染

  2. 转场动画延迟。在低端机冷启时,给动画设置一个offset值。

然而在实践过程当中我们发现简单的动画延后并不能完全解决问题,往往在动画后段仍然会出现卡顿,这是因为真实请求回来后会进行页面二刷,而此时动画如果没有结束则依然会出现卡顿。

为此,我们可以将二刷也延后:

  1. 在Fragment自定义动画中设置动画结束监听

  2. 请求回来后循环校验当前动画是否完成

动画延后方案渲染流程:

fef91a2f0ec3e22514d83064799e575e.jpeg

  • 效果展示

动画延后方案的Trace如下图所示:

85f06d1d4e67eca16d5b4e4586e7d2b5.png

从Trace中我们可以看出,动画帧基本没有出现卡顿,因为动画期间主线程并没有耗时任务。

  • 结论

通过以上方法我们虽然可以解决动画卡顿的问题,但是很显然这并不是一个优雅的方案:

  1. 代码实现逻辑较为恶心,需要获取精确的一刷渲染结束时间点,同时二刷需要循环等待动画结束,后期很难维护

  2. 动画延后拉长了点击响应耗时,二刷延后又拉长了可交互耗时,得不偿失

一开始我们选择Fragment方案最主要的原因就是为了减少点击响应耗时,在中高端机上采用这种方案无可厚非,然而在低端机上首屏渲染本就耗时,如果再将动画延后至首屏渲染完成后那么点击响应耗时势必增加,这样就和我们的目标背道而驰了。

  2. 分帧渲染
  • 方案简介

既然我们已经知道动画卡顿的直接原因是首帧绘制耗时太长挤占了整个动画时长,从而导致后续动画时间不足出现大量丢帧、卡顿的现象。那么如果我们将UI线程首帧的渲染任务拆分成很多小块的任务,控制每一小段任务的执行耗时都能够在16ms的帧间隔内完成,那么UI线程渲染就能够和动画实现帧同步。

  • 具体实现

在订单列表场景下,列表页通常由顶部的header区域和下方的滚动列表body区域组成。body区域是一整个RecycleView,而RecycleView适配器中的数据是一个List<T>。在正常的渲染流程中我们会将请求返回的一页数据解析、整理好一次性交给RV,然后RV会在当前页面完全渲染完成后一次性上屏,这一点从前面的Trace中也能看出

c560c4724dd35d18ea88d8c90d0f7b14.png

根据分帧渲染的思路,我们考虑将List中的数据均匀分成适当的等份,在每一个帧间隔期间设置一段数据。

首先在转场动画开始前先完成header渲染,然后分帧渲染单段数据。

这里的分帧渲染设置依赖于Android的Choreographer.FrameCallback帧回调,伪代码如下所示:

Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        // 当所有分段数据都设置后移除帧回调
        if (...) {
            Choreographer.getInstance().removeFrameCallback(this);
            return;
        }


        // 分段设置数据
        getAdapter().setData(bodyList.subList(a, b));
        getAdapter().notifyItemRangeChanged(itemCount, range);


        // 注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
};
// 注册当前帧回调
Choreographer.getInstance().postFrameCallback(frameCallback);

通过监听帧回调,将List数据分段设置我们可以实现按照帧维度进行精细化的渲染。

  • 效果展示

从视屏中我们可以看出,分帧渲染后RecycleView的内容确实从上到下分段上屏,卡顿也会明显好很多。

  • Trace对比

分帧渲染Trace

2cd36b01460b5af49d6e04dd05193312.png

正常渲染Trace

aaec4ab2fed1e5fa943a952de7fd89f2.png

从Trace中我们可以看出,分帧渲染将首屏首帧这样一个完整的耗时任务分成了多个子任务,再结合Android的帧回调机制,在动画帧间隔期间完成每个UI线程子任务。相较于正常的渲染流程,分帧渲染明显减少了丢帧数从而达到减少卡顿的目的。

  • 结论

实际上,分帧渲染相对于原始的动画起到了补帧的效果,但丢帧问题仍然存在。这是因为即便将RV的数据分成最小单位(即每份数据只有一个dx模板)也会出现渲染耗时超过16ms的情况,尤其是在低端机冷启首进的情况下。一旦最小渲染单位的渲染耗时超过了动画的帧间隔就无法实现完美的帧同步,从而再导致动画丢帧、卡顿。

  3. 异步转场动画框架

分帧方案实现较为复杂且效果不达预期,在这之后架构组的成兆老师提供了一套新的思路:异步动画。既然Fragment动画卡顿是因为主线程阻塞导致的,那么我们是否可以在异步线程进行独立绘制呢?顺着这个思路我们很容易联想到了Android的SurfaceView。

  • SurfaceView简介

SurfaceView 是 Android 提供的一种视图类,允许在一个独立的绘图表面上进行绘制操作。相对于普通的View,SurfaceView 提供一个独立的 Surface,用于在自己的线程中进行绘图操作。这意味着可以在后台线程上绘制,避免占用主线程资源,从而减轻主线程的负载,提升整体渲染性能。

SurfaceView 的核心组件

  1. Surface: 一个用于绘制的渲染目标,本质上是内存中的一段绘图缓冲区,底层通过 SurfaceFlinger进行管理。

  2. SurfaceHolder: Surface的持有者,提供对 Surface 的访问和控制,并且可以注册回调来监听 Surface 的生命周期变化。SurfaceHolder的所有回调都在主线程

  3. SurfaceView: 拥有独立绘图层的特殊View,由于继承自 View,它负责管理 Surface 和 SurfaceHolder,并分发生命周期事件。

  • SurfaceView 异部渲染原理分析

SurfaceView继承自View,本质上它也是个View。我们知道,普通的View对WMS并不可见,而是通过顶层的DecorView这个桥梁来与WMS完成交互,WMS会为这个根视图分配一个WindowState对象用来管理视图的显示属性。同时,在SF(Surface Flinger)中DecorView会拥有一个Layer,用来在屏幕上合成并最终展示视图。

98411f788b2c5e2619ac1f892c93c5ad.jpeg

而SurfaceView的特殊之处就在于,它拥有自己的Surface,这是用于直接绘图的缓冲区,而这个Surface在WMS中也注册了一个自己的WindowState,并且在SF中也会有对应的Layer。也就是说,虽然SurfaceView仍属于宿主窗口View hierachy中的一个视图节点,但在Server端(WMS和SF),它与宿主窗口却是分离的。得益于这种特殊的设计,SurfaceView能够独立参与窗口管理,享有与DecorView类似的管理机制,并且它被允许在不干扰UI线程的情况下,由专有线程高效地进行图形渲染。

  • 方案简介

既然SurfaceView具备独立渲染的能力,不会被主线程阻塞,那么考虑用SurfaceView来完成动画,这样即使在动画期间主线程任务繁忙也不会影响SurfaceView的动画效果。

SurfaceView + 快照

在打开Fragment并且需要开始动画之前,创建一个SurfaceView,并将事先准备好的占位图即 placeholder添加到SurfaceView上,当动画开始后让这个SurfaceView来展示动画,等Fragment渲染完成后再隐藏这个SurfaceView。

  • 具体实现

  1. 快照获取:即placeholder资源获取,在订单列表场景下,可以在请求回来二刷的时机通过注册ViewTreeObserver.OnGlobalLayoutListener 监听,在布局完成后通过系统方法截取当前首屏,并将其持久化存储到本地。这里获取快照推荐使用Android 8.0(API 26)后提供的 PixelCopy.request() 方法,如果使用View.draw() 方法可能导致图片圆角缺失、Weex组件无法获取快照等问题。

  2. Nav阶段异步获取placeholder,并将其读取到内存中,避免后续在主线程进行耗时的IO操作

  3. 在下一次进入订单列表fragment.onCreate阶段,创建SurfaceView并将内存中的placeholder资源设置给SurfaceView

  4. 开始执行SurfaceView的动画,此时主线程正在渲染Fragment的真实首屏

  5. Fragment首屏渲染完成,隐藏SurfaceView

  • 效果展示

c8c6e9e985361c4a90478ce42a298e7e.png

从Trace中我们可以看到即使在主线程处于繁忙状态下,SurfaceView的动画帧却不受影响。

在订单列表场景下,页面元素相对稳定,采用这套方案可以在最大程度缩减点击响应耗时的同时保证转场动画丝滑流畅,并且在动画期间不存在loading状态,这对于用户体验来说是巨大的提升。当然在使用这套方案的同时也需要注意一些隐藏的风险。


图片

风险

  ANR

快照资源从本地存取的IO操作存在ANR风险,需要在异步异步线程完成。

  获取快照阻塞主线程

对于快照资源获取的时机需要根据具体业务场景来判断,太早可能导致页面元素不全,太晚则可能影响可交互时长。

  存储资源

快照存在本地,对于本地存储资源的占用需要额外注意。

图片

团队介绍

我们是淘天集团-基础交易终端团队,一支专注于手淘APP交易域(购物车、下单、订单、物流等)业务研发和体验优化的技术团队。在丰富的业务场景下,我们通过持续的技术探索、不断的创新突破,给数亿用户提供极致可靠的交易保障、极致流畅的操作交互以及极致顺滑的购物体验。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值