一、前言
很长时间没有更新Flutter动态化的文章了,这部分的开发主要是在公司正常工作之外做的实验性探索,都是利用零散时间一次一次重构和优化。截至目前已完成较为完整的版本,并且已试验性的上线了公司App一小块业务功能,进行小范围内试用。
Flutter动态化的原理和思路可以参照之前的文章,经过这段时间的优化实践,梳理了下目前动态化执行的流程:
Runtime 的执行过程如下:
其中比较关键的是Runtime Parser ,主要功能是解析Ast语法树,并执行对应的语法功能,包含两个部分:
- 基础语法解析器
- 扩展解析器
详见下图:
///解析器接口,用于解析如下表达式
///* target(int a, String b)
///* target({int a, String b})
///* target.property(int a, String b)
///* target.property({int a, String b})
///* target.property
dynamic parse(
AstRuntime runtime,
dynamic target, {
String property,
List<Expression> arguments,
});
解析器基本上是个体力活,枚举大部分常用语法和公共基础类,同时提供了一个扩展解析器的接口,如果需要解析自定义对象(如自定义UI组件或工具类等),实现此接口并添加到解析器中即可,跟随需求需要不断迭代补充完善解析器。
二、业务场景实战
我们要实践的业务功能是一个消息列表界面,样式如下:
界面的元素不算复杂,也是比较常见的列表界面,我们对这个界面进行动态化处理后的运行效果如下:
可以明显看到在滚动的时候卡顿非常严重,体验非常不好
1. 问题分析
打开DevTool分析:
一般人眼识别连贯动作的帧数是24fps
,那么一帧的时长不宜超过40ms
,通常我们感到比较舒适的帧数是60fps
,也是目前比较常用的标准,那么一帧的时长就用控制在15ms
以内。由上图可见,这个列表在滚动的时候,一帧的时长竟达到了560.4ms
,滑动过程卡顿感非常强烈。问题的原因在DevTool上也能清楚看到,就是在解析器这块,递归调用了很多次,截图不够完整,下面其实还有很多调用,目前没有想到好的办法优化这部分,只能用这种比较粗暴的方法解析Ast树中的每个节点。
2. 解决思路
解析算法优化这条路暂时行不通(能力不足= =...,哪位大侠可以指点一二「抱拳」),剩下有几个优化的方向:
- 在子线程中执行解析器
- 解析器部分使用效率更高的语言实现,比如C/C++
- 利用CPU空闲时间,手动控制渲染时机
对这几个方向进行可行性分析:
首先,第一种方式调研后在Flutter中不太适用,我们知道Flutter中的线程是叫isolate
,并且isolate
不支持共享内存,多个isolate
之间通过内部提供的端口类进行通信,通信的数据类型只能是基本数据类型和内部的SendPort
对象,如果想传输其他类对象,除非序列化成基本数据类型进行传输。那么带来的一个问题就是针对Widget
类型的对象就处理不了,在子isolate
中解析构建好的Widget
对象没办法传给父isolate
,Widget
本身又比较复杂也没有提供序列化的方法,所以第一种方案被Pass掉。
第二种方案存在同样的问题,就是在 C/C++中如何构建Widget
对象?已经如何将构建好的对象传输到Flutter层?
剩下比较可行的就是第三种方案了,思路同目前常见列表滚动优化方法一样,就是在滑动的时候不进行解析,滑动结束的交互空闲时段,在执行解析器任务。
3. 第三种方案实现
实现流程如下:
整个原理过程不算复杂,其中比较重要的是如何获取已显示列表项的index区间,Flutter SDK中是没有提供相关方法的,使用的是第三方的Package:scrollable_positioned_list,也是Google出品,感兴趣的童鞋可以看看源码实现。
最终优化后的效果:
这个方案还算不上完美,虽然滑动体验较之前好了许多,但是加载列表项等待的时长还是挺长的,有其他优化思路的小伙伴欢迎在评论区一同探讨~