他把闲鱼APP长列表流畅度翻了倍(良心教程),基于android的app开发开题报告

// reducer.dart
// 滑动事件监听
static BottomBarState onScroll(BottomBarState state, Action action) {

return state.clone()…scrollPercent = scrollPercent;

}

4.1.2 使用 fish-redux 性能日志

fish-redux 是闲鱼研发一套在 flutter 上的 redux 框架,闲鱼 APP 中有广泛应用。fish-redux 中自带性能日志,源码查看 performance.dart,若需要打印 profile 或 release 模式下的性能日志,可自行修改源码。

闲鱼详情页滑动时,查看 adb 日志,可以发现大量的滑动广播通知,且存在耗时 1ms 以上事件处理。

11-15 15:03:43.684 27076 27271 I flutter : CommonBuyDetailPage performance: ItemBodyAction.onScrollBroadcast 261
11-15 15:03:43.701 27076 27271 I flutter : CommonBuyDetailPage performance: ItemBodyAction.onScrollBroadcast 1933
11-15 15:03:43.716 27076 27271 I flutter : CommonBuyDetailPage performance: ItemBodyAction.onScrollBroadcast 371

profile 模式下时间日志

因为详情页中存在视图间联动,如标题栏的显示隐藏渐变,问卖家的显示消失均需要根据滑动事件做判断。结合业务逻辑,可以发现,除了问卖家外,其他视图在滑动超出 600 之后,收到滑动事件后不会发生视图内容变化;而问卖家在滑动超出更大的一个值后会永远消失不显示,在一开始未超出这个值时,仅需要判断滑动方向即可。基于以上业务背景,在滑动超出 600 后,若问卖家是不再显示状态,则不发送滑动事件;否则仅在开始滑动的 30 距离内发送事件。

此外,可以利用 fish-redux 的特性:若 reducer.dart 中返回新的 state 对象则表示 widget 重建,检查全部的 reducer.dart 文件内方法实现,排查可能发生的无效 widget 重建。

4.1.3 优化 ClipPath 和 ClipRPath

使用 Timeline 查看渲染线程性能消耗,可以发现有多个 ClipRectLayerClipRRectLayer

打开 Debug flag debugDisableClipLayersdebugDisablePhysicalShapeLayers 重新检查视图,可以发现部分 ClipRectLayer 是因为图片内容超出视图边界产生,部分 ClipRRectLayer 是因为卡片 Widget 圆角设置以及基于外接纹理的图片控件里设置了 ClipRRect 设置(即便 radius 为0也会设置)

理解原理后,我们对闲鱼图片控件新增参数,支持图片内容圆角设置和图片内容宽高裁剪,使 native 层生成的 Bitmap 已经满足圆角和宽高比要求。同时修复 radius 为0也会设置 ClipRRect 的问题。优化后的 Timeline 图如下:

4.1.4 其他优化建议

flutter 性能优化相关的优秀文章很多,本文不再对类似的排查和优化手段做赘述,这里做下简单汇总:

  • widget build 优化
  1. setState 状态刷新位置尽量放置于视图树的低层级
  2. Provider 中获取 Model 的方式会影响刷新范围。推荐使用 Selector 或 Consumer 来获取祖先 Model,以维持最小刷新范围
  3. 对于长列表,避免使用 ListView() 构造函数,推荐使用 ListView.builder 构造函数
  4. reducer 中,state 对象中的视图数据真正发生变化的时候,新建 state 对象
  • 主 isolate 优化 5. 减少或延迟 widget build 中非视图逻辑,如曝光埋点延迟到滑动停止聚合触发 5. 列表 Item 高度可知的情况下,推荐设置 itemExtent,减少滑动中频繁计算列表高度 5. 使用 const 修饰无需变更的 widget 或普通对象 5. 使用 AnimatedBuilder 时,避免在不依赖于动画的 widget 的构造方法中构建 widget 树。动画的每次变动都会重建这个 widget 树。而应该构建子树的那一部分,并将其作为 child 传递给 AnimatedBuilder 5. 避免在动画中剪裁。如果可能,请在动画开始之前预先剪切图像
  • Render 线程优化 10. 对于频繁更新的控件(如动画),使用 RepaintBoundary 隔离它,创建单独 layer 减少重绘区域 10. 使用图片替换半透明效果 10. 减少 saveLayer(ShaderMask、ColorFilter、Text Overflow)、clipPath的使用,提升 render 线程性能 10. 避免使用 Opacity widget,尤其是在动画中避免使用。请用 AnimatedOpacity 或 FadeInImage 进行代替 10. 避免使用带换行符的长文本
  • 工具推荐 15. 官方 DevTools 工具 15. 利用 Debug flags 排查问题(推荐 Flutter Performance 分析工具简介) 15. 善于利用框架日志,如 fish-redux 性能日志

4.2 列表 element 复用优化

flutter 列表控件划分为可视区域和 Cache 区域,往下滑动时 element 从底部被创建进入底部 Cache 区域后,再进入可视区域,再进去顶部 Cache 区域,最后被销毁。往上滑动逻辑类似。在不使用 keepAlive 的情况下,来回滑动,曾经创建过的 element 需要重新创建。而在我们的业务中,列表 item Widget 结构是接近的,此时如果能根据类型复用 element,就能一定程度的提升性能。

列表控件源码见 sliver_list.dart 中 RenderSliverList.performLayout() element 缓存在 _childElements 数组中,以 index 为索引。源码见 sliver.dart 若 item Widget 结构差异很大,即便复用了 element,Element.updateChild 方法内部最终还是执行了 inflateWidget 方法,对于性能提升就没什么价值了

我们构建 index${widget.key}List<element> 的映射关系:在 widget 创建处建立 index${widget.key} 映射,在 element 应该被销毁移除的逻辑处,将 element 缓存至 ${widget.key} 映射的 List<element> 处(注意 renderObject 对象需要从父节点移除)。列表滑动过程中,优先根据映射关系找到缓存中的 element 并使用(注意更新 element.renderObject.parentData 中的 index 值)

4.3 复杂 Widget 分帧上屏

以上全部优化手段尝试后,在闲鱼的详情页和搜索页上还是远没有达到预期。原因是猜你喜欢卡片和搜索页卡片本身就足够复杂,另外由于我们引入 DX 技术让 Widget 进一步变得巨大,最终导致的结果是:即便高端机,也无法在一帧时间内完成渲染。 然而抛开技术视角,从业务视角看,卡片展现内容和 DX 的动态能力都是必需的。那如何在满足业务诉求的情况下,实现超大 Widget 的高性能呢?

业务侧仅需 Text,但在 DX 技术中使用的是 DXTextWidget

猜你喜欢卡片在 红米 K30Pro(CPU 骁龙 865)的 Timeline 图

搜索结果卡片 Timeline 图,补充了 performLayout、updateChild、Widget build

在已知常见优化手段无法满足的情况下,我们回归 GUI 系统性能优化的起点去思考问题。流畅度优化思路,大体可以分为 3 个方向:

  1. 多线程方案

在 Android 原生开发中很常见。但在 dart 世界中,不同线程(isolate)的内存是隔离的,此外由于 flutter 渲染流程三棵树,我们不好直接操作 RenderObject,多线程方案在 flutter 中较难实施(排除 IO 更新数据后显示等常规场景)

  1. 优化每个任务,挤压 CPU 运算量,保证一帧时间(16.6 ms)完成任务

flutter 中的主流优化思路,前面的优化手段都是这个思路

  1. 快速响应用户,让用户觉得够快,不阻塞用户的交互。即一帧时间内还有任务没有完成,则停止执行,保证列表先执行滑动,未执行任务在后续帧时间片上执行

参考 React Fiber 框架,基于时间分片的思路,协调阶段将一颗任务树转为一条任务链(parent 节点 → child 节点 → sibling 节点 → parent 节点),满足了任务链可中断执行,提前提交渲染,最后实现了将一条任务链拆解到多帧时间分片中消化。

排除方向 1、2 后,只剩下方向 3。再结合猜你喜欢卡片 Timeline 图可以发现,在卡片 Widget 创建的一帧发生时间不足,而后面的几帧内时间消耗都远没到 16.6 ms,可以想到方向 3 是正确的。那剩下的关键问题仅有以下 2 点:

  1. 能否将一个大 Widget build 任务为拆分多个小 Widget build 任务并大致平均的分配到多个时间分片上?
  2. 一个大 widget 分时间片上屏是否会影响体验?

Timeline 上任务耗时图

Flutter widget 拆分和分帧上屏

基于时间分片的大方向,我们把一个大 widget 拆分为一个空白框架和 2 个卡片 widget,再将卡片 widget 拆分为一个卡片框架和多个 FXImage Widget,Widget 框架中不立马显示的部分使用占位 Widget 临时代替。 由此构建一个高优大任务队列和一个低优小任务队列,高优大任务队列中的任务高优执行且独占一帧时间,低优小任务队列低优执行且一帧时间最多能执行 12 个任务。再利用 flutter 逐步标脏,将 build 任务延迟到后续时间分片上。

以上最终将一个超大 widget 构建从 1 帧时间分散到 4 帧时间内消化,优化了卡顿。

优化后猜你喜欢卡片 Timeline 图(红米 K30Pro,CPU 骁龙 865)

在体验方面,前面讲列表控件结构时已知有一个不可见的 Cache 区域,所以分帧上屏大部分是在这个不可见区域完成的,为此在高端机或正常滑动情况下用户并无感知。而在低端机上快速滑动能明显看到卡片空白情况,但整体相比严重顿挫体感要好。

4.4 优化数据

基于上面的优化手段,闲鱼详情页和搜索页流畅度 FPS 提升了 3 个点,低端机大卡顿次数降低一半,中高端机型上流畅度提升到 57 或以上,大卡顿次数接近 0。

详情页线上高可用 fps 数据如下:

线上低端机 fps 曲线。绿色为优化版本 曲线分布越靠右,流畅度越好

线上高端机 fps 曲线。绿色为优化版本

搜索页线上高可用 fps 数据如下:

线上低端机 fps 曲线。绿色为优化版本

线上高端机 fps 曲线。绿色为优化版本

4.5 滑动差值器优化

完成上面优化后,线下自建流畅度检测工具数据和线上 fps 数据曲线都有很大的提升,且数据指标接近原生 APP 流畅度。在中高端机型上,闲鱼详情页 FPS 已经被我们优化到了 57 及以上了,1s 大卡顿次数接近 0。在原生 APP 流畅度 FPS 数值达到 57 及以上时,滑动过程中基本上不会感受到卡顿,然而,flutter 页面的实际滑动操作中,还是能感受到卡顿。

回顾自建流畅度检测工具原理:基于每帧画面比对、无侵入,相同的自动化脚本,所以相信我们线下测试的数据(平均 FPS 和 1s 大卡顿次数)是准确的。性能数据接近,而体感有差异,且性能数据准确可信,所以可以确认流畅度指标(平均 FPS 和 1s 大卡顿次数)还不能完全反应体感。

再回顾 2.2 流畅度指标制定,可以发现我们并没有对空间维度的 offset 跳变(画面内容跳变)做检测。基于此,我们可以对比 Android 原生 RecyclerView 和 Flutter SliverList 在卡顿情况下 offset 变化情况

Android 原生 RecyclerView 和 Flutter SliverList fling 阶段 offset/time 曲线图

由上可以得到,同样在 FPS 值达到 57,Android RecyclerView 在用户体感上比 flutter 列表控件更好的原因:在小卡顿时,offset 偏移值并没有发生翻倍跳变。

查看 flutter 滑动算法,可以发现是基于一条 D/T 曲线计算滑动距离,所以发生卡顿时,输入 timeOffset 值发生翻倍,最终计算出来的 offset 值发生近乎翻倍。

flutter ClampingScrollSimulation D/T 曲线

为消除在发生小卡顿时,offset 跳变的情况,我们自定义了 physics 和 simulation,在 time 发生发生小跳变时,修改滑动距离算法,采用 V/T 曲线算法,distance 通过累加的方式计算,优化了 time offset 发生翻倍而导致曲线跳变的情况

distance = velocity(time) * 16.6ms + distance

注意:需要适配系统频率大于 60 hz 的机型(如 90hz,120hz),在一帧时间内有可能计算多次 distance

以 V/T 曲线为基础,我们提供了以下滑动差值器:

  • SmoothClampingScrollPhysics

无回弹差值器,停顿后偏移值不跳变。 结束滑动的效果同 ClampingScrollSimulation

  • SmoothBouncingScrollPhysics

回弹差值器,停顿后偏移值不跳变

5 总结和展望

经过上述优化,在原生 Android 方面,闲鱼首页流畅度和内容上屏得到明显提升;在 Flutter 方面,闲鱼详情页和搜索页流畅度 FPS 提升了 3 个点,低端机大卡顿次数降低一半,中高端机型上流畅度提升到 57 或以上,大卡顿次数接近 0,相同小卡顿在体验上得到了提升。

流畅度优化是每一个 GUI 系统都一直在努力的事情,有很多优秀的工具介绍、官方和非官方的优化文章。这次优化过程中,我们也借鉴了很多别人的文章,发现和优化了一些问题,但本文尽量不去重复描述,推荐读者阅读相关优化文章或官方文档。 在以上优化手段尚无法实现最终目标时,我们也做了一些不一样的优化,期望能抛砖引玉,对读者有所帮助和启发:

  • 基于用户体验为导向构建了流畅度指标:平均 FPS,1s 大卡顿次数
  • 针对指标,自建了流畅度检测工具,支持无侵入、跨平台、自动化
  • [Android] 显示 ViewDataUnbinder 组件在复杂业务逻辑中快速抽离 UI 操作
  • [Flutter] 修改 Flutter engine 源码,支持列表 element 复用
  • [Flutter] 实现大 Widget 分帧上屏组件
  • [Flutter] 差值器算法优化

后续我们会继续思考以下内容:

  • 如何将流畅度检测工具内部产品化,支持非研发同事使用?
  • 如何使用已有的经验、工具、组件快速优化其他业务页面?
  • 如何在研发阶段及时发现和防止无效 rebuild 等问题?
  • 如何在 CI 平台及时发现页面流畅度恶化情况?

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

考虑到文章的篇幅问题,我把这些问题和答案以及我多年面试所遇到的问题和一些面试资料做成了PDF文档

喜欢的朋友可以关注、转发、点赞 感谢!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

[外链图片转存中…(img-rjFkHssv-1712702565885)]

[外链图片转存中…(img-pk22mEQ8-1712702565885)]

喜欢的朋友可以关注、转发、点赞 感谢!

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-2vlaTscd-1712702565886)]

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值