12月小报|读小报,涨知识
本期知识小集的主要内容包括:
• Flutter桥调用请注意结果反馈
• Flutter await代码带来的潜在并发
• Flutter FPS 高不代表一定流畅
• Flutter新渲染引擎impeller尝鲜
Flutter桥调用请注意结果反馈
通过桥来拓展Flutter的能力,是非常通用的Flutter开发场景。常见的包括:网络请求,本地存储,异构页面通信等 。实现功能固然重要,但是如果控制不好返回值,可能会是一个灾难。例如:Dart侧代码:
void getResult() async {
dynamic result = await MethodChannel('methodChannelName')
.invokeMethod<dynamic>('methodName', {'params': content});
doOtherJobs();
}
Native 侧代码,以Android代码为例:
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
case "methodName": {
doJob();
break;
}
}
}
如果按上述代码实现,就会出现一个非常隐藏的问题:dart侧代码会一直wait,之后的代码(doOtherJobs)永远不会被执行到了。问题的原因是MethodChannel 并不知道底层的逻辑是否执行完毕。那怎样通知MethodChannel执行完了呢?需要调用如下代码:
如下代码调用任一一个即可
result.success();
result.error();
如果没有对应方法的实现可以调用如下代码:
result.notImplemented();
问题虽小,但是影响可能很大,一定要小心~
Flutter await代码带来的潜在并发
Flutter使用Dart语言进行开发。Dart语言具备非常友好的并发编程语法。例如 async/await。我们在享受并发语法带来便利的同时,也需要深刻理解代码背后的执行逻辑。只有这样,才能避免走入一些“深坑”。首先从最简单的逻辑来看:如果一个函数被标记为 async,意味着该函数会被异步执行,函数会返回一个 Future 对象。函数正常执行的到该函数的时候,并不会停下并等待函数的结果返回,而是直接运行下面的代码。如果想要程序停下,等待函数的执行结果,需要配合await关键字来实现。示例如下:
这里有一个非常有意思的问题,使用await 等待异步函数执行,到doJob2函数执行,这中间是不是仅仅执行了doAsyncJob函数内容?来看下面的例子
bool needReturn = false;
Future<void> doJob2() async {
needReturn = true;
}
Future<void> doJob() async {
if (needReturn) {
return;
}
print('needReturn position1 is $needReturn'); // needReturn == false ?
await doSomething();
print('needReturn position2 is $needReturn'); // needReturn == false ?
}
Future<void> doSomething() async {
print('doSomething~~~');
}
第一个问题 position1 位置的时候needReturn 是不是一定是false?
答案是yes, 因为needReturn == true 会在之前执行的时候,直接返回。要想执行到position1 ,needReturn一定是false;
第二个问题 position2 位置的时候needReturn 是不是一定是false?
答案是不一定!为什么不一定呢?doSomething函数中并没有设置needReturn为true。needReturn会被修改么?答案是有可能,原因是doJob2可能在其他控制流中被执行。看起来position2的上一句就是doSomething,但是在await 等待的时候,其他的并发函数也可能被执行,如果doJob2被执行,值就会发生了变化。结论:使用await 并发执行以后,记得一定要做变量的重新检查。因为这里虽然代码相邻,但是过程中可能执行大量其他并发函数,核心状态并不像看起来的那么可控。
Flutter FPS 高不代表一定流畅
流畅滚动是优异体验的核心保障。FPS(Frames Per Second)作为页面流畅度的核心度量指标,被广泛使用。FPS本质上度量的是每秒播放的帧数。下图直观对比不同帧率的显示效果。
Flutter开发页面,同样广泛的使用FPS来度量页面流畅度。但是Flutter一直有一个“细碎抖动”的问题,也就是页面整体是流畅的,但是在滚动的过程中有明显的细碎抖动,这对用户体验产生了伤害。在实际开发过程中,FPS这一指标对这类抖动问题的度量效率并不高。例如:前900ms刷了50帧,但是最后100ms刷了1帧,最后的FPS值是51,看起来也是一个不错的值。但是用户会在其中明显感知到卡顿。帧率的连贯性是很重要的,即便刷新只有30帧,但是如果一直是这个帧率,用户感知起来也是流畅的。但是如果一下子从50帧掉到30帧用户还是会感知明显的卡顿。所以流畅度的度量需要感知帧率的变化。那Flutter中怎么感知每一帧的变化呢?可以用如下方法获取每一帧的性能数据数据。
WidgetsBinding.instance.addTimingsCallback();
透过该方法,除了能获取每一帧的整体耗时,还可以细化到build和raster两个主要阶段的耗时。这样能更加深入的做性能问题的排查。数据结构体如下:
factory FrameTiming({
required int vsyncStart,
required int buildStart,
required int buildFinish,
required int rasterStart,
required int rasterFinish,
required int rasterFinishWallTime,
int frameNumber = -1,
})
那么在知道帧耗时的情况,怎么判定是一次卡顿呢?可以从如下两个维度来度量:
1. 帧耗时是之前N帧平均耗时的M倍(这里N和M可以根据实际情况调整,例如一般设置成3帧和2倍)
2. 帧耗时超过两帧电影帧耗时(电影帧单帧耗时:1000ms/24≈41.67ms,这是下线,帧耗时超过这个标准,用户能明显感知到卡顿)
同时我们也可以通过统计不同分位的帧耗时,更细致感知实际页面渲染情况。例如常见的90分位,99分位帧耗时。大家可以根据实际情况统计。
Flutter新渲染引擎impeller尝鲜
接着上面的问题,Flutter有一个 early-onset jank 的公开问题(问题详解可以参见引用【1】)。Flutter页面的抖动问题跟这个问题有着一定的关联。本质上impeller是Skia的一个替代方案。官方在Flutter3.0的版本中首次公开了Impeller的预览版本。同时在Flutter3.3版本中进行了大量完善。目前可以通过如下方式开启:
1. flutter run 添加 --enable-impeller
2. Native工程配置
在IOS工程的Info.plist文件中添加如下配置:
<key>FLTEnableImpeller</key>
<true/>
Android工程,在AndroidManifest.xml添加如下配置:
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="true" />
那impeller 效果如何呢?从我们初步的测试来看,有如下初步结论:注意目前impeller iOS的成熟度相比Android要高很多。我们只测试了iOS的场景
1. 从官方Gallery场景来看,优化效果显著,impeller的debug包就有了媲美之前release包的效果。Flutter的细碎抖动问题,在官方Gallery场景上基本解决。滚动流畅性有显著提升。
2. 由于官方Gallery比较简单,从闲鱼的实际benchmark来看,impeller目前在复杂场景下的性能未超过skia的实现。【测试版本 Flutter3.3.8 手机iPhone 13 Pro】主要原因是impeller目前阶段比较早,很多功能还有待完善,测试过程中也出现了大量渲染错误的问题。impeller距离生产中使用还需时日。
impeller | skia |
raster线程平均帧耗时 5.5ms | raster线程平均帧耗时 1.9ms |
impeller是Flutter根本上解决卡顿问题的重要尝试,虽然目前状态下还有很多的不完善,但是可以明显感受到impeller带来的显著变化,未来可期~
引用
【1】Flutter 新一代图形渲染器 Impeller
【2】https://github.com/flutter/flutter/wiki/Impeller
【3】https://docs.flutter.dev/development/tools/sdk/release-notes/release-notes-3.3.0