着色器编译卡顿

如果移动应用上的动画看起来很卡顿,但仅在首次运行时出现此问题,这可能是由于着色器编译造成的。Flutter 解决着色器编译卡顿的长期解决方案是 Impeller,它在 iOS 的稳定版本中提供,并在 Android 上以标志的形式提供预览。

在我们致力于让 Impeller 完全准备好投入生产的同时,你可以通过将预编译着色器与 iOS 应用捆绑在一起来缓解着色器编译卡顿。遗憾的是,由于预编译着色器是设备或 GPU 特定的,因此此方法在 Android 上效果不佳。Android 硬件生态系统非常庞大,因此与应用程序捆绑在一起的 GPU 特定的预编译着色器仅适用于一小部分设备,并且可能会加剧其他设备上的卡顿,甚至造成渲染错误。

此外,请注意,我们不打算对下面描述的用于创建预编译着色器的开发者体验进行改进。相反,我们将精力集中在 Impeller 提供的更稳健的解决方案上。

什么是着色器编译卡顿?

着色器是在 GPU(图形处理单元)上运行的一段代码。当 Flutter 用于渲染的 Skia 图形后端首次看到新的绘图命令序列时,它有时会为该命令序列生成并编译一个自定义 GPU 着色器。这允许该序列和潜在的类似序列尽可能快地渲染。

遗憾的是,Skia 的着色器生成和编译与帧工作负载同时发生。编译可能需要花费数百毫秒,而流畅的帧需要在 16 毫秒内绘制,才能实现 60 fps(每秒帧数)的显示。因此,编译可能会导致错过数十帧,并将 fps 从 60 降至 6。这就是编译卡顿。编译完成后,动画应流畅。

另一方面,Impeller 在我们构建 Flutter Engine 时生成并编译所有必要的着色器。因此,在 Impeller 上运行的应用已经拥有所需的所有着色器,并且可以在不给动画引入卡顿的情况下使用着色器。

着色器编译卡顿存在的明确证据是在启用 --trace-skia 的情况下在跟踪中设置 GrGLProgramBuilder::finalize。以下屏幕截图显示了一个时间线跟踪示例。

A tracing screenshot verifying jank

“首次运行”是什么意思?

在 iOS 上,“首次运行”意味着用户在每次从头打开应用时,动画首次出现时可能会看到卡顿。

如何使用 SkSL 预热

Flutter 为应用开发者提供了命令行工具,用于收集最终用户可能需要的着色器,格式为 SkSL(Skia 着色器语言)。然后,SkSL 着色器可以打包到应用中,并在最终用户首次打开应用时进行预热(预编译),从而减少以后动画中的编译卡顿。使用以下说明收集和打包 SkSL 着色器

  1. 使用 --cache-sksl 运行应用以捕获 SkSL 中的着色器

    flutter run --profile --cache-sksl
    

    如果之前已经运行过没有 --cache-sksl 的相同应用,则可能需要 --purge-persistent-cache 标志

    flutter run --profile --cache-sksl --purge-persistent-cache
    

    此标志会移除可能干扰 SkSL 着色器捕获的较旧的非 SkSL 着色器缓存。它还会清除 SkSL 着色器,因此在首次 --cache-sksl 运行时使用它。

  2. 使用应用触发尽可能多的动画;尤其是那些有编译卡顿的动画。

  3. flutter run 的命令行中按 M,将捕获的 SkSL 着色器写入名为 flutter_01.sksl.json 之类文件的某个文件中。为获得最佳结果,在实际 iOS 设备上捕获 SkSL 着色器。在模拟器上捕获的着色器不太可能在实际硬件上正常工作。

  4. 使用以下方法构建带有 SkSL 预热的应用,视情况而定

    flutter build ios --bundle-sksl-path flutter_01.sksl.json
    

    如果它是为像 test_driver/app.dart 这样的驱动器测试构建的,请确保也指定 --target=test_driver/app.dart(例如,flutter build ios --bundle-sksl-path flutter_01.sksl.json --target=test_driver/app.dart)。

  5. 测试新构建的应用。

或者,你可以编写一些集成测试,使用一个命令自动执行前三个步骤。例如

flutter drive --profile --cache-sksl --write-sksl-on-exit flutter_01.sksl.json -t test_driver/app.dart

使用此类 集成测试,你可以轻松可靠地获取新的 SkSL,当应用代码更改时,或当 Flutter 升级时。此类测试还可以用于验证 SkSL 预热前后性能的变化。更好的是,你可以将这些测试放入 CI(持续集成)系统中,以便在应用的生命周期内自动生成和测试 SkSL。

Flutter Gallery 的原始版本为例。CI 系统被设置为针对每次 Flutter 提交生成 SkSL,并在 transitions_perf_test.dart 测试中验证性能。有关更多详细信息,请查看 flutter_gallery_sksl_warmup__transition_perfflutter_gallery_sksl_warmup__transition_perf_e2e_ios32 任务。

最差帧光栅化时间是此类集成测试中一个有用的指标,用于指示着色器编译卡顿的严重性。例如,上述步骤减少了 Flutter Gallery 的着色器编译卡顿,并将其在 Moto G4 上的最差帧光栅化时间从约 90 毫秒加快到约 40 毫秒。在 iPhone 4s 上,它从约 300 毫秒减少到约 80 毫秒。这导致了本文开头所说明的视觉差异。