Flutter 以其高还原度,匹配原生的性能和高开发效率,已经成为主流的移动跨平台技术。在不断发展过程中,也衍生出了很多优秀的开发框架,帮助开发者提高开发效率和降低开发成本。Fish Redux 就是一款优秀的 Flutter 状态管理框架。
目前零售移动在很多业务中都用到 Flutter,也是基于主流的 Fish Redux + Flutter Boost 模式。新技术的落地总是会伴随着各种踩坑,其中比较深刻的,是 Flutter 界面卡顿的问题,最终通过深入分析 Fish Redux 状态管理机制解决了该问题,也总结了一些经验供大家参考。
商家反馈在收银机上使用进出存单据功能很卡,操作界面切换按钮点击反应都很慢。从商家反馈的视频和我们实际操作的视频中,明显可以感受到在界面过渡、数据加载、点击操作、列表滑动,弹框都存在肉眼可见的卡顿,特别是在一些配置不怎么好的收银设备上。针对这些现象,我们将问题分为两大类:
1、数据加载等耗时操作卡顿
2、UI渲染卡顿
对问题进行分类之后,就开始使用 DevTool 中提供的性能视图对卡顿界面视图渲染情况进行了分析。针对库存盘点场景选取了严重卡顿的操作:添加商品、修改商品数据、动画展示、网络数据请求和加载。
界面布局
添加商品
StockCheckOrderEditMainState:顶层 State
从列表添加一个商品之后,可以看到整个界面都进行了重绘,绘制范围明显不合理。
修改商品数据
修改数据与添加商品类似,也是也是进行了全局刷新
网络数据请求和加载
在网络数据回来之后,发现 Dart_StringToUTF8 耗时长,深入排查之后发现,是 JSON 数据驼峰和下划线转换导致。
经过初步排查之后,基本确定了问题是存在耗时操作和更新渲染范围过大导致。对于渲染范围问题,项目中基本都是按照官方推荐的方式进行了很多界面的组件拆分和复用,为什么没有达到局部渲染的效果呢?带着这个问题,对 Fish Redux 刷新机制进行了探究。
此部分做一些核心概念介绍,已经了解过的同学可以跳过。
Fish Redux 是一个以 Redux 作为数据管理的思想,以数据驱动视图,组装式的 Flutter 应用框架,里面有几个很重要的角色: State、Effect、Reducer 和 Action。
图中的T代表某一个类型的 State,UI 交互产生了交互 action,effect 处理对应的交互 action 之后,又会产生数据更新 action,reducer 收到数据更新 action 之后完成 state 的更新,最终驱动了 UI 的更新,进入下一个循环。
组件(Component)是对视图展现和逻辑功能的封装,一个复杂的界面通常都是由一个个组件组合而成,大组件使用 Dependencies 完成所依赖的小组件、适配器的注册。
Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)
只有实现了 Reducer 的组件才能拥有自刷新的能力,否则都是跟随父组件更新而更新。
Page 是一个页面级的 Component,类似于 Android 中的 Activity,redux 中的 store 就是存储在 Page 组件中,Page 中的所有 Component 都共用这个 store。store 负责 reducer 事件分发。Page 中还有一个 DispatchBus 类型的 bus 属性,负责 Effect 事件分发。
视图创建
在了解界面刷新流程之前,需要先了解一下整个界面的构建流程。构建过程主要任务是构建视图+事件注册。
/// component.dart
abstract class Component<T> extends Logic<T> implements AbstractComponent<T> {
@override
Widget buildComponent(
Store<Object> store,
Get getter, {
required DispatchBus bus,
required Enhancer<Object> enhancer,
}) {...}
}
Component 实现了 AbstractComponent 接口,实现了 buildComponent 方法。框架从触发顶层组件的。
buildComponent 开始整个视图的绘制流程,容器组件将创建自己的ComponentWidget 以及触发子组件 ComponentWidget 的创建,就这样完成整个视图的创建。ComponentWidget 中完成 ComponentState 的创建,在 ComponentState 的 initState 中,会调用 store 的的 subscribe 方法将自己的 onNotify 方法注册到 store 的 listener 中,这样就完成了监听reducer事件监听。
/// component.dart
class ComponentState<T> extends State<ComponentWidget<T>> {
void initState() {
/// ...
/// 注册监听
_ctx.registerOnDisposed(widget.store.subscribe(() => _ctx.onNotify()));
}
}
Effect 的注册是在 Component 的 createContext 方法创建 ComponentContext 时,在ComponentContext 的父类 LogicContext 构造方法中,调用bus.registerReceiver(_effectDispatch) 完成的。
/// logic.dart
abstract class LogicContext<T> extends ContextSys<T> with _ExtraMixin {
LogicContext({...}){
/// ...
/// Register inter-component broadcast
registerOnDisposed(bus.registerReceiver(_effectDispatch));
}
}
这样,就完成了 Effect 与 Reducer 的事件监听。
事件分发与处理
Effect 与 Reducer 的事件处理流程存在重合和不一致的地方,一致的点就是入口都是 dispatch 方法(这个地方有一个隐性要求:Effect 与 Reducer 事件不能一致,否则会死循环),都会先从自己的组件开始寻找能处理这个事件的监听者,如果找不到就会交给顶层组件进行分发。不一致的点是 effect 不关心处理结果,reducer 关心处理结果。
Effect处理流程
流程就比较简单,因为 bus 中已经存储了所有 effect 处理,这个时候只需要遍历一下_dispatchList 就可以广播处理消息了。
Reducer 处理流程
Effect 与 Reducer 的事件处理流程存在重合和不一致的地方,一致的点就是入口都是 dispatch 方法(这个地方有一个隐性要在整个界面创建完成后,父组件通过 connector 将子组件的 reducer 组合在一起,这样在处理事件时,就可以访问到子组件的reducer。而在 Fish Redux 中,reducer 的事件都从是 store 中开始,事件发生后,从根节点开始向下找寻可以处理这个事件的 reducer,如果没有找到就返回原有 state,找到之后会调用其更新方法,更新 state,并且把新的 state 返回。
/// combine_reducers.dart
Reducer<T>? combineReducers<T>(Iterable<Reducer<T>?>? reducers) {
final List<Reducer<T>?>? notNullReducers =
reducers?.where((Reducer<T>? r) => r != null).toList(growable: false);
/// ... 前置处理
return (T state, Action action) {
T nextState = state;
for (Reducer<T>? reducer in notNullReducers) {
/// 这里有问题,必须要重新赋值对象
final T? _nextState = reducer?.call(nextState, action);
nextState = _nextState!;
}
assert(nextState != null);
return nextState;
};
}
而 reducer 的事件是从 store 中发出的。store 的创建是在 Page 组件中,在创建 store 时,会实现dispatch 方法,内容就是分发 reducer 事件,完成分发之后,就会得到整个 page 最新的 state 状态,然后进行 state 更新事件的广播,通知所有组件进行更新。
// create_store.dart
Store<T> _createStore<T>(final T preloadedState, final Reducer<T> reducer) {
// 前置处理
return Store<T>()
..getState = (() => _state)
..dispatch = (Action action) {
// 前置校验
try {
_isDispatching = true;
// reducer 分发处理
_state = _reducer(_state, action);
} finally {
_isDispatching = false;
}
final List<_VoidCallback> _notifyListeners = _listeners.toList(
growable: false,
);
// 广播更新消息
for (_VoidCallback listener in _notifyListeners) {
listener();
}
_notifyController.add(_state);
}//..更多属性初始化
}
而组件的更新逻辑,就是收到更新时间之后,调用 shouldUpdate 方法判断是否需要更新界面, shouldUpdate 默认实现就是判断前后state是否相等。
/// context.dart
class ComponentContext<T> extends LogicContext<T> implements ViewUpdater<T> {
@override
void onNotify() {
final T now = state;
// 默认是 !identical(_latestState, now)
if (shouldUpdate(_latestState, now)) {
_widgetCache = null;
markNeedsBuild?.call();
_latestState = now;
}
}
}
// markNeedsBuild 实现
markNeedsBuild: () {
if (mounted) {
setState(() {});
}
}
但是按道理我们实现了组件化之后,调用的更新方法也是子组件的,应该只刷新子组件才对,但是从实际的表现来看,是会导致整个界面都刷新,说明 Page 的 state 也变了。
Connector 机制
其实在这个过程中,有一个重要且比较容易被忽视的角色,就是 Connector,Connector 存在两个子类:MutableConn 和 ImmutableConn,ImmutableConn 处理更新时,如果是子 state 发生变化,只会更新父 state 中对子 state 的引用,对父 state 没有影响。
// ImmutableConn
SubReducer<T> subReducer(Reducer<P> reducer) {
return (T state, Action action, bool isStateCopied) {
/// ... 前置处理
final P newProps = reducer(props, action);
final bool hasChanged = !identical(newProps, props);
if (hasChanged) {
final T result = set(state, newProps);
/// ... 中间处理
return result;
}
return state;
};
}
MutableConn 处理更新时,如果是子 state 发生变化,不仅会更新子 state,还会将父 state 进行 clone 更新,这样就会导致传递更新导致一个小组件更新触发整个界面更新。了解了这个特性之后,前面的问题就可以得到解释了。
// MutableConn
SubReducer<T> subReducer(Reducer<P> reducer) {
return (T state, Action action, bool isStateCopied) {
/// ... 前置处理
final P newProps = reducer(props, action);
final bool hasChanged = newProps != props;
final T copy = (hasChanged && !isStateCopied) ? _clone<T>(state) : state;
if (hasChanged) {
set(copy, newProps);
}
return copy;
};
}
对于数据加载耗时,最终是定位到使用的 Recase 库存在性能问题。在网络数据请求之后,在业务中需要针对 json 的 key 进行驼峰和下滑线的转换,而 Recase 库在处理转换时,存在对象重复创建和转换逻辑不够高效的问题。针对这点,我们自己实现了转换的逻辑,并且增加了对于 key 转换的缓存,将之前随数据条数增加导致耗时增加的情况变为随不同 key 增加导致耗时增加。大大提升了转换的效率。
class ReCase {
/// 重复创建常量对象
final RegExp _upperAlphaRegex = RegExp(r'[A-Z]');
final symbolSet = {' ', '.', '/', '_', '\\', '-'};
List<String> _groupIntoWords(String text) {
// 重复创建临时对象
StringBuffer sb = StringBuffer();
/// ... 转换逻辑
return words;
}
/// ... 其他逻辑
}
/// 使用场景
/// 在单个单词时并没有太多问题,但是如果用于处理json数据,
/// 在数量大时积累耗时会很长,并且也占用的内存也会增加
final result = ReCase('test_test').camelCase
完成了 Fish Redux 刷新机制的分析之后,其实解决方案也比较清晰了。从刷新机制中,可以得出两个解决方案
1、重写 shouldUpdate 方法
在原则上,如果当前组件只是将其他组件组合在一起,自己并没有特殊的业务逻辑时,可以直接将 shouldUpdate 返回 false,因为子组件完全可以管理自己的状态。有一个判断点:当前组件的 view.dart 中是否只是简单的 buildComponent,一般是不需要更新的。
/// view.dart
Widget buildView(T state, Dispatch dispatch, ViewService viewService) {
return viewService.buildComponent(key)
}
///
class DemoComponent extends Component<T> {
DemoComponent() : super(
shouldUpdate: (_,__) => false,
/// 其他,
);
}
其他情况可以根据当前 state 中的影响界面刷新的子 state 进行判断实现精细化更新。
2、事件分发与处理
修改 connector 类型可以阻断更新传递从而达到减少更新范围的效果,如果明确父组件是不会更新的,就可以在依赖子组件时,使用 ImmutableConn 进行依赖连接,这样就不需要担心子组件更新会影响到父组件。
结合零售的实际情况,最终是采用了方案 1 进行 shouldUpdate 重写,因为在实际业务中,父子组件的联动效果还是存在,不能直接切断联系,还是根据实际场景进行条件刷新,这样在保证业务正确性的同时优化性能。
通过优化更新逻辑,优化数据转换效率,再配合热数据内存缓存、优化动画和更细粒度的组件抽离之后,卡顿的Flutter界面流程度提升 60%,再也没有出现明显的卡顿现象。
在整个治理卡顿的过程中,重新学习了一遍 Fish Redux,体会到框架的优秀,特别是针对复杂的项目,其模板化的开发方式有效降低了理解和沟通成本,每个角色各司其职,在处理问题时方向明确,不需要担心“牵一发动全身”的问题。有一个总结经验就是:如果在使用Fish Redux遇到一些卡顿问题,大概率是组件没有划分或者划分不够细。网上在很多Flutter性能优化的建议总结,特别是Flutter官方的性能优化的指导,推荐阅读。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/_DVhsKAJsMuCyiUseREzzg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。