着色器编译时卡顿

如果你的移动应用程序上的动画只在首次运行时卡顿,很可能是着色器编译引起的。 Flutter 对着色器编译卡顿的长期解决方案是 Impeller,目前正处于预览阶段,在 iOS 上通过启用特定标志来使用(Android 正处于开发阶段,尚不可用)。

在继续下面的说明之前,请在 iOS 上尝试使用 Impeller,如果它没有解决你的问题,请在 GitHub issue 中告诉我们。 Impeller 的 Android 部分正在积极开发中,但还没有进入开发者预览阶段。

在我们努力让 Impeller 为生产做好准备的时候,你可以尝试将预编译的着色器与 iOS 应用绑定在一起来减轻着色器编译的卡顿。不幸的是,由于预编译的着色器是针对设备或特定 GPU 进行优化的,所以这种方法在 Android 上效果不佳。 Android 的硬件生态系统非常庞大,因此与应用程序绑定的特定 GPU 预编译着色器只能在一小部分设备上运行,而且很可能会加剧其他设备上的卡顿问题,甚至引发渲染错误。

另外,请注意,我们并不打算改进下面描述的创建预编译着色器。相反,为了真正解决这个问题,我们将主要精力集中在 Impeller 提供的更强大的解决方案上。

什么是着色器编译卡顿?

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

然而不幸的是,Skia 着色器生成和编译的过程与帧的工作是依次进行的。编译过程可能需要几百毫秒的时间,而对于 60 帧/秒 (frame-per-second) 的显示来说,一个流畅的帧必须在 16 毫秒内绘制完成。因此,编译过程可能导致数十帧被丢失,使帧数从 60 降到 6。这就是所谓的 编译卡顿 。编译完成之后,动画应该会变得流畅。

另一方面,Impeller 在我们构建 Flutter 引擎时已经生成并编译了所有必要的着色器。因此,在 Impeller 上运行的应用程序已经拥有了它们所需的所有着色器,并且这些着色器不会在动画中引起卡顿。

要获得更加确切的着色器编译卡顿存在的证据,你可以在 --trace-skia 开启时查看追踪文件中的 GrGLProgramBuilder::finalize。下面的截图展示了一个 timeline 追踪的样例。

A tracing screenshot verifying jank

如何定义「首次运行」?

在 iOS 上来说,「首次运行」意味着用户可能在每次打开应用后,在动画首次加载时都会出现卡顿。

如何使用 SkSL 预热

在 1.20 发布的时候,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. Build the app with SkSL warm-up using the following, as appropriate:

    在下面的命令中选择合适的构建带有 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. Test the newly built app.

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

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

使用这样的 集成测试,无论是代码发生改变或者 Flutter 更新了,你都可以轻松获得可靠的着色器缓存。这些测试也被用于验证开启着色器预热前后的性能变化上。更好的做法是,你可以把这些测试放进 CI(持续集成)系统上,这样就能在每次应用发布前自动生成并测试着色器缓存了。

就拿原始版本的 Flutter Gallery 举例。我们让 CI 系统在每次 Flutter commit 后都生成着色器缓存,并在 transitions_perf_test.dart 中验证性能。更多详细信息请查看 Flutter Gallery sksl 预热过渡性能验证,以及 Flutter Gallery sksl 预热过渡在 iOS_32 上的性能验证

在这种这种集成测试中,最差的帧光栅化时间是一个很好的指标来衡量着色器编译卡顿的严重性。例如,上述步骤减少了 Flutter gallery 应用的着色器编译卡顿,并减少了它在 Moto G4 手机上的最差的帧光栅化时间,从 ~90 ms 减少到 ~40 ms。在 iPhone 4s 上,它从 ~300 ms 减少到 ~80 ms。这种视觉差异如同本文开头所示一样。