第三方库源码分析第一篇 * Flutter流畅度优化组件 Keframe

505 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情


之前在掘金上发现了Nayuta分享的Keframe,通过使用发现确实可以减少一部分卡顿,本身的作者已经进行了介绍 -> ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案

其中说明了为什么出现了卡顿?哪些场景下容易出现卡顿?如何优化ListView卡顿?,所以本文就不再累赘的描述这些内容了。

本文主要记录对Keframe源码的分析和基本讲解,如果错误,还请大家指出

源码版本:

pub.dev上的keframe 3.0.0

1、源码结构

16540523498977.jpg 源码分为2个widget:FrameSeparateWidgetSizeCacheWidget和一个任务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尺寸大小缓存流程和使用

  1. 首先在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;
  }

  1. 通过在FrameSeparateWidget对应的FrameSeparateWidgetState中使用自定义的widgetItemSizeInfoNotifier来包裹真正使用的子widget
// FrameSeparateWidgetState
class FrameSeparateWidgetState extends State<FrameSeparateWidget> {
  @override
  Widget build(BuildContext context) {
    return ItemSizeInfoNotifier(index: widget.index, child: result);
  }
}
  1. 通过ItemSizeInfoNotifierdispatch尺寸大小通知
//ItemSizeInfoNotifier
@override
  InitialRenderSizeChangedWithCallback createRenderObject(
      BuildContext context) {
    return InitialRenderSizeChangedWithCallback(
        onLayoutChangedCallback: (size) {
      LayoutInfoNotification(index, size).dispatch(context);
    });
  }

一旦有onLayoutChangedCallbackLayoutInfoNotification(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的尺寸大小

  1. 使用itemsSizeCache 具体的使用缓存的大小是在FrameSeparateWidgetStateinitState函数
  @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、总结

  1. SizeCacheWidget提供了获取缓存widget尺寸大小的能力和设置最大任务队列数maxTaskSize,可以让界面对于已渲染过的 widget 设置占位的尺寸。在滚动过程中,已经渲染过的 item 将不会出现跳动情况。将 SizeCacheWidget 的 estimateCount 设置为 10*2。快速滚动场景构建响应更快,并且内存更稳定
  2. FrameSeparateWidget提供了包裹真是widget,填充placeHolder临时界面,真正测量widget大小的能力。同时使用SchedulerBinding.instance.addPostFrameCallback向分帧任务队列中插入任务。
  3. FrameSeparateTaskQueue进行任务的处理,每次会在添加任务之后,开始Timer.run异步执行处理任务:首先会清除掉队列中不处于mounted的任务。在等待SchedulerBinding.instance.endOfFrame当前帧完成后,开始从队列首个任务遍历处理。