持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
之前在掘金上发现了Nayuta分享的Keframe,通过使用发现确实可以减少一部分卡顿,本身的作者已经进行了介绍 -> ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案
其中说明了为什么出现了卡顿?,哪些场景下容易出现卡顿?,如何优化ListView卡顿?,所以本文就不再累赘的描述这些内容了。
本文主要记录对Keframe源码的分析和基本讲解,如果错误,还请大家指出
源码版本:
pub.dev上的keframe 3.0.0
1、源码结构
源码分为2个widget:
FrameSeparateWidget和SizeCacheWidget和一个任务FrameSeparateTaskQueue管理队列
`frame_separate_task.dart` -> 分帧任务队列
`frame_separate_widget.dart` -> FrameSeparateWidget用于分帧显示的widget
`layout_proxy.dart` -> 为了获取widget尺寸大小的SingleChildRenderObjectWidget
`logcat.dart` -> logcat日志打印
`notification.dart` -> 尺寸大小的notification数据类
`size_cache_widget.dart` -> SizeCacheWidget用于缓存子widget的尺寸大小
2、SizeCache尺寸大小缓存流程和使用
- 首先在SizeCacheWidget对应的SizeCacheWidgetState中订阅了
LayoutInfoNotification通知
// SizeCacheWidgetState
@override
Widget build(BuildContext context) {
return Builder(
builder: (BuildContext ctx) {
return NotificationListener<LayoutInfoNotification>(
onNotification: (LayoutInfoNotification notification) {
logcat(
'size info : index = ${notification.index} size = ${notification.size.toString()}');
saveLayoutInfo(notification.index, notification.size);
return true;
},
child: widget.child,
);
},
);
}
一旦有通知就保存数据
Map<int?, Size> itemsSizeCache = <int?, Size>{};
void saveLayoutInfo(int? index, Size size) {
itemsSizeCache[index] = size;
}
- 通过在
FrameSeparateWidget对应的FrameSeparateWidgetState中使用自定义的widgetItemSizeInfoNotifier来包裹真正使用的子widget
// FrameSeparateWidgetState
class FrameSeparateWidgetState extends State<FrameSeparateWidget> {
@override
Widget build(BuildContext context) {
return ItemSizeInfoNotifier(index: widget.index, child: result);
}
}
- 通过
ItemSizeInfoNotifier来dispatch尺寸大小通知
//ItemSizeInfoNotifier
@override
InitialRenderSizeChangedWithCallback createRenderObject(
BuildContext context) {
return InitialRenderSizeChangedWithCallback(
onLayoutChangedCallback: (size) {
LayoutInfoNotification(index, size).dispatch(context);
});
}
一旦有onLayoutChangedCallback就 LayoutInfoNotification(index, size).dispatch(context);
class InitialRenderSizeChangedWithCallback extends RenderProxyBox {
InitialRenderSizeChangedWithCallback({
RenderBox? child,
required this.onLayoutChangedCallback,
}) : super(child);
final Function(Size size) onLayoutChangedCallback;
Size? _oldSize;
@override
void performLayout() {
super.performLayout();
if (size != _oldSize) onLayoutChangedCallback(size);
_oldSize = size;
}
}
这样当widget绘制的时候performLayout函数中一旦发现size != _oldSize就会更新widget的尺寸大小
- 使用itemsSizeCache
具体的使用缓存的大小是在
FrameSeparateWidgetState中initState函数
@override
void initState() {
super.initState();
result = widget.placeHolder ??
Container(
height: 20,
);
final Map<int?, Size>? size = SizeCacheWidget.of(context)?.itemsSizeCache;
Size? itemSize;
if (size != null && size.containsKey(widget.index)) {
itemSize = size[widget.index];
logcat('cache hit:${widget.index} ${itemSize.toString()}');
}
if (itemSize != null) {
result = SizedBox(
width: itemSize.width,
height: itemSize.height,
child: result,
);
}
transformWidget();
}
上述代码通俗来讲就是,如果定义了placeHolder,就是使用placeHolder来填充,如果没有就是用高度为20的Container。
在此期间,如果发现有对应缓存itemsSizeCache,就再用SizedBox包裹上述placeHolder或者是Container。
其中最重要的context.findAncestorStateOfType方法,它可以向上查找返回给定类型T的实例 最近的祖先StatefulWidget小部件的State对象
static SizeCacheWidgetState? of(BuildContext context) {
return context.findAncestorStateOfType<SizeCacheWidgetState>();
}
额外补充:
还有一个类似的函数
T? findRootAncestorStateOfType<T extends State>();返回给定类型T的实例 最远祖先StatefulWidget小部件的State对象,复杂度为 O(N)
3、分帧队列管理和原理
在FrameSeparateWidgetState类中会在initState()和didUpdateWidget(FrameSeparateWidget oldWidget)函数中添加任务
//FrameSeparateWidgetState
@override
void initState() {
//...
transformWidget();
}
@override
void didUpdateWidget(FrameSeparateWidget oldWidget) {
super.didUpdateWidget(oldWidget);
transformWidget();
}
void transformWidget() {
SchedulerBinding.instance.addPostFrameCallback((Duration t) {
FrameSeparateTaskQueue.instance!.scheduleTask(() {
if (mounted)
setState(() {
result = widget.child;
});
}, Priority.animation, () => !mounted, id: widget.index);
});
}
主要的原理就是使用SchedulerBinding.instance.addPostFrameCallback(FrameCallback callback)函数
函数的意思是:
在此帧结束时安排回调。 不请求新帧。 此回调在一帧期间运行,就在持久帧回调之后(即刷新主渲染管道时)。如果一帧正在进行中且帧后回调尚未执行,则在该帧期间仍会执行已注册的回调。否则,注册的回调将在下一帧期间执行。 回调按照添加的顺序执行。 后帧回调不能取消注册。它们只被调用一次
也就是它会在一帧结束后进行回调,仅仅进行一次,也就利用此次回调来渲染真正需要的widget
在执行FrameSeparateTaskQueue.instance!.scheduleTask后会执行以下函数_
addTask(entry);
_ensureEventLoopCallback();
具体方法:
//添加任务
void _addTask(TaskEntry _taskEntry) {
if (maxTaskSize != 0 && _taskQueue.length >= maxTaskSize) {//如果指定了最大任务数,并超出的话,则移除队列首个任务
logcat('remove Task');
_taskQueue.removeFirst();
}
_taskQueue.add(_taskEntry);
}
//准备执行任务,控制一次只执行一个任务
Future<void> _ensureEventLoopCallback() async {
assert(_taskQueue.isNotEmpty);
if (_hasRequestedAnEventLoopCallback) return;
_hasRequestedAnEventLoopCallback = true;
Timer.run(() {//尽快异步运行给定的callback
_removeIgnoreTasks();//删掉没有挂载的任务
_runTasks();
});
}
//执行任务前,等待在完成当前帧之后 开始任务
Future<void> _runTasks() async {
_hasRequestedAnEventLoopCallback = false;
await SchedulerBinding.instance.endOfFrame;//等待完成当前帧
if (await handleEventLoopCallback()) //如果任务队列不为空,继续执行任务
_ensureEventLoopCallback();
}
//处理任务
Future<bool> handleEventLoopCallback() async {
if (_taskQueue.isEmpty) return false;
final TaskEntry<dynamic> entry = _taskQueue.first;//第一个任务
//如果注册了任何帧回调,则仅运行Priority.animation Priority更高的任务
if (schedulingStrategy(
priority: entry.priority, scheduler: SchedulerBinding.instance)) {
try {
_taskQueue.removeFirst();
entry.run();
} catch (exception, exceptionStack) {
//...
}
return _taskQueue.isNotEmpty;//不为空,继续处理任务队列
}
return true;
}
//删除掉队列中未mounted的任务
void _removeIgnoreTasks() {
while (_taskQueue.isNotEmpty) {
//canIgnore() == () => !mounted
if (!_taskQueue.first.canIgnore()) {
break;
}
_taskQueue.removeFirst();
}
}
额外补充:
SizeCacheWidget可以设定最大任务队列数
maxTaskSize
4、总结
SizeCacheWidget提供了获取缓存widget尺寸大小的能力和设置最大任务队列数maxTaskSize,可以让界面对于已渲染过的 widget 设置占位的尺寸。在滚动过程中,已经渲染过的 item 将不会出现跳动情况。将 SizeCacheWidget 的 estimateCount 设置为 10*2。快速滚动场景构建响应更快,并且内存更稳定FrameSeparateWidget提供了包裹真是widget,填充placeHolder临时界面,真正测量widget大小的能力。同时使用SchedulerBinding.instance.addPostFrameCallback向分帧任务队列中插入任务。- FrameSeparateTaskQueue进行任务的处理,每次会在添加任务之后,开始
Timer.run异步执行处理任务:首先会清除掉队列中不处于mounted的任务。在等待SchedulerBinding.instance.endOfFrame当前帧完成后,开始从队列首个任务遍历处理。