Flutter 分帧上屏源码浅析

最近技术早读会接触了一下 Flutter 优秀的开源库 Keframe, github 链接 点这里 。下面是 github 中作者对于 Keframe 用途的描述:

这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。

上面分帧渲染我故意加了粗体。分帧渲染,不明觉厉!趁着周末学习了一下。

关于分帧上屏的原理,这里借用作者 Nayuta 的描述,如下:

假设,我们屏幕能显示4个item,每个item构建耗时是10ms。在现有的 ListView布局过程中,会在第一帧的时候,同时构建这四个item,总共40ms。

采用分帧之后,在页面的第一帧我们先通过构建简单的占位item,占位的item可以是个简单的Container。由于其构建基本不耗时,在第一帧的时候构建四个Container不会导致卡顿。之后将实际的四个item,分别延迟到后面四帧进行渲染。这样对于每个16.7ms而言,都没有发生超时渲染,整个流程不会发生卡顿。

建议上面的话读三遍,就着下面的原理图细品两分钟。

下面就着 Keframe 源码分析一下 分帧是如何实现的

以下是使用 Android Studio 打开之后,Keframe 的工程目录结构。下方黄色框内即为 Keframe 库的核心代码,上方蓝色框内为使用了分帧技术的示例 ListView。

这里我们只看分帧实现的一个核心流程,其它细节暂且忽略掉。所以只分析其中一个示例即可。双击打开 list_opt_example1.dart 文件,下面是构建 ListView 的核心代码

可以看到 ListView 的 itemBuilder 是使用 FrameSeparateWidget 构建的,其构造参数分别是为 index,placeholder,child。传入的参数分别为 i,Container 对象,CellWidget 对象。注意,这里的 placeHolder 即为上面作者叙述帧原理中的占位 item,child 传入的参数则是实际的 item。最终,我们会用实际的 item 去替换占位的 item。Let's move on !

点进 FrameSeparateWidget ,看到的是一个 const 类型的构造函数

const FrameSeparateWidget({
    Key? key,
    this.index,
    required this.child,
    this.placeHolder,
  }) : super(key: key);

同时可以发现 FrameSeparateWidget 继承自 StatefulWidget,重点看一下其状态实现类 _FrameSeparateWidgetState_FrameSeparateWidgetState 重写了父类的 initState(),didUpdateWidget(FrameSeparateWidget oldWidget),build(BuildContext context) 三个方法,同时还实现了一个很重要的方法transformWidget()

先来看 initState() 方法

方法有点长,核心实现已经用黄色框圈出。第一个黄色框内是一个赋值表达式,result 用于接收外界专入的 placeholder,用于占位 item 的渲染(具体可以自己查看 _FrameSeparateWidgetStatebuildContext 的实现),第二个黄色框内从函数名上看是替换 widget 。对,这个就是实际的 item 替换占位 item 的入口!

void transformWidget() {
    SchedulerBinding.instance!.addPostFrameCallback((t) {
      FrameSeparateTaskQueue.instance!.scheduleTask(() {
        if (mounted)
          setState(() {
            result = widget.child;
          });
      }, Priority.animation, id: widget.index);
    });
  }

上面函数里面的实现语句没有多余的,都是重点,需要一句一句的读!首先最外面调用的是系统的任务绑定类 SchedulerBindingaddPostFrameCallback方法。根据系统的方法说明我们知道,该方法会在一个帧渲染开始前向帧前调度队列(这是我自己翻译的) _postFrameCallbacks 里面添加一个任务。addPostFrameCallback 方法具体实现如下

void addPostFrameCallback(FrameCallback callback) {
    _postFrameCallbacks.add(callback);
}

很简单,就是简单把任务 callback 加入到 _postFrameCallbacks 队列中去。在 binding.dart 文件中搜索 _postFrameCallbacks,可以发现在 handleDrawFrame方法里面遍历执行了 _postFrameCallbacks 队列中的任务,从而开始执行 addPostFrameCallback 我们传入的 callback 里面的执行语句。

如上 _invoikeFrameCallback(callback, _currentFrameTimeStamp!),感兴趣的可以自己点进行去看看具体实现。Move on !

接下来看一下我们向 addPostFrameCallback 函数传入的 callback 具体的执行语句。 FrameSeparateTaskQueue 是自己维护的一个任务管理队列类,这里直接调用了 scheduleTask 方法。也传入了一个三个参数:**匿名函数,priority,index **。其中匿名函数中的实现很重要:调用了 setState 方法,并将 widget.child 赋值给了 result。我们知道调用 setState 方法意味着 widget 的绘制,这里应该就是作者叙述的使用实际 widget 替换占位 widget 的具体代码实现。


现在就差扣动执行这个替换的 trigger 了!
接下来就开始 FrameSeparateTaskQueue 类的表演了。

Future<T> scheduleTask<T>(TaskCallback<T> task, Priority priority,
      {String? debugLabel, Flow? flow, int? id}) {
    final TaskEntry<T> entry =
        TaskEntry<T>(task, priority.value, debugLabel, flow, id: id);
    _addTask(entry);
    _ensureEventLoopCallback();
    return entry.completer.future;
  }

首先是实例化了一个 TaskEntry 对象 entry,然后将任务通过 _addTask 方法将 entry 添加到队列里面去。之后通过 _ensureEventLoopCallback 方法开启任务执行时机的监听,以便去执行添加队列中的 entry 任务。那显然得看一下 _ensureEventLoopCallback 方法。

void _ensureEventLoopCallback() async {
    assert(_taskQueue.isNotEmpty);
    if (_hasRequestedAnEventLoopCallback) return;
    _hasRequestedAnEventLoopCallback = true;
    await SchedulerBinding.instance!.endOfFrame;
    _runTasks();
  }

注意倒数第二句的 await SchedulerBinding.instance!.endOfFrame 方法。可以看到 等待每一帧渲染结束 的时候,才会去执行 _runTasks() 方法。见名知意,_runTasks 就是执行任务的意思。继续往下看

void _runTasks() async {
    _hasRequestedAnEventLoopCallback = false;
    bool result = await handleEventLoopCallback();
    if (result)
      _ensureEventLoopCallback();
    else {
      if (_taskQueue.isNotEmpty) {
        _ensureEventLoopCallback();
      }
    }
  }

这里核心的语句是 handleEventLoopCallback() 方法。在该方法中,从队列中取出第一个元素判断其执行优先级:如果优先级足够高,就会执行任务 entry 的 run 方法,接下来返回队列是否为空的布尔值。如果优先级不够,则直接返回 false。返回值用途可以继续查看 _runTasks() 方法关于返回值 result 的使用,用于继续循环监听任务执行的时机。下面是 handleEventLoopCallback() 方法的实现。

方法好长,好烦!不过去除一大堆无关逻辑,核心执行语句也就上面黄色框内的三条语句。到这里,还是没有看到 trigger 的扣动。不过看到了 entry.run(),一丝结束的希望,继续点进去看吧。

终于,来到了最后扣动 trigger 的时刻!

completer.complete(task()) 就是这个 trigger ! task() 的执行意味着实际 widget 替换占位 widget 的真正开始,接下来的实际 widget 的渲染就由系统来完成了!如下,回到了开始的 setState() 方法的执行。

到此,一次完整的分帧上屏任务就完成了!

以上只是就着源码简单分析了一下分帧上屏的实现的核心流程,中间省略了很多的细节,尤其是 Flutter 渲染背后的一套复杂的原理。如果想了解更多,可以跟着作者的思路,一点点的探索分帧上屏诞生的一个过程,感受一下整个过程的艰辛!

强烈推荐阅读: https://juejin.cn/post/6940134891606507534
再放一下 Keframe 地址:Keframe

最后
熟真的能生巧!

再最后
坚持很难,但坚持无价!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容