控制加载顺序,优化性能与内存

This page describes the breakdown of the steps involvedto show a Flutter UI. Knowing this, you can make better, informed decisionsabout when to pre-warm the Flutter engine, which operations are possibleat which stage, and the latency and memory costs of those operations.

Loading Flutter

Android and iOS apps (the two supported platforms for integrating into existingapps), full Flutter apps, and add-to-app patterns have a similar sequence ofconceptual loading steps when displaying the Flutter UI.

Finding the Flutter resources

Flutter’s engine runtime and your application’s compiled Dart code are bothbundled as shared libraries on Android and iOS. The first step of loadingFlutter is to find those resources in your .apk/.ipa/.app (along withother Flutter assets such as images, fonts, and JIT code if applicable).

This happens when you construct a FlutterEngine for the first time on bothAndroidand iOS APIs.

Loading the Flutter library

After it’s found, the engine’s shared libraries are memory loaded once per process.

On Android, this also happens when the FlutterEngineis constructed because the JNI connectors need to reference the Flutter C++library. On iOS, this happens when the FlutterEngineis first run, such as by running runWithEntrypoint:FlutterEngine(im)runWithEntrypoint:).

Starting the Dart VM

The Dart runtime is responsible for managing Dart memory and concurrency foryour Dart code. In JIT mode, it’s additionally responsible for compilingthe Dart source code into machine code during runtime.

A single Dart runtime exists per application session on Android and iOS.

A one-time Dart VM start is done when constructing the FlutterEnginefor the first time on Android and when running a Dart entrypointFlutterEngine(im)runWithEntrypoint:)for the first time on iOS.

At this point, your Dart code’s snapshotis also loaded into memory from your application’s files.

This is a generic process that also occurs if you used the Dart SDKdirectly, without the Flutter engine.

The Dart VM never shuts down after it’s started.

Creating and running a Dart Isolate

After the Dart runtime is initialized, the Flutter engine’s usage of the Dartruntime is the next step.

This is done by starting a Dart Isolatein the Dart runtime. The isolate is Dart’s container for memory and threads. Anumber of auxiliary threadson the host platform are also created at this point to support the isolate, suchas a thread for offloading GPU handling and another for image decoding.

One isolate exists per FlutterEngine instance, and multiple isolatescan be hosted by the same Dart VM.

On Android, this happens when you call DartExecutor.executeDartEntrypoint()on a FlutterEngine instance.

On iOS, this happens when you call runWithEntrypoint:FlutterEngine(im)runWithEntrypoint:)on a FlutterEngine.

At this point, your Dart code’s selected entrypoint (the main() function ofyour Dart library’s main.dart file, by default) is executed. If you called theFlutter function runApp() in yourmain() function, then your Flutter app or your library’s widget tree is also createdand built. If you need to prevent certain functionalities from executingin your Flutter code, then the AppLifecycleState.detached enum value indicatesthat the FlutterEngine isn’t attached to any UI components such as aFlutterViewController on iOS or a FlutterActivity on Android.

Attaching a UI to the Flutter engine

A standard, full Flutter app moves to reach this state as soon as the app islaunched.

In an add-to-app scenario, this happens when you attach a FlutterEngineto a UI component such as by calling startActivity())with an Intentbuilt using FlutterActivity.withCachedEngine()on Android. Or, by presenting a FlutterViewControllerinitialized by using initWithEngine: nibName: bundle:FlutterViewController(im)initWithEngine:nibName:bundle:)on iOS.

This is also the case if a Flutter UI component was launched withoutpre-warming a FlutterEngine such as with FlutterActivity.createDefaultIntent()on Android or with FlutterViewController initWithProject: nibName: bundle:FlutterViewController(im)initWithProject:nibName:bundle:)on iOS. An implicit FlutterEngine is created in these cases.

Behind the scene, both platform’s UI components provide theFlutterEngine with a rendering surface such as a Surfaceon Android or a CAEAGLLayeror CAMetalLayeron iOS.

At this point, the Layertree generated by your Flutter program, per frame, is converted intoOpenGL (or Vulkan or Metal) GPU instructions.

Memory and latency

Showing a Flutter UI has a non-trivial latency cost. This cost can be lessened by starting the Flutter engine ahead of time.

The most relevant choice for add-to-app scenarios is for you to decidewhen to pre-load a FlutterEngine (that is, to load the Flutter library,start the Dart VM, and run entrypoint in an isolate), and what the memory and latencycost is of that pre-warm. You also need to know how the pre-warm affects the memory and latency cost ofrendering a first Flutter frame when the UI component is subsequently attachedto that FlutterEngine.

As of Flutter v1.10.3, and testing on a low-end 2015 class device in release-AOTmode, pre-warming the FlutterEngine costs:

  • 42 MB and 1530 ms to prewarm on Android. 330 ms of it is a blocking call onthe main thread.
  • 22 MB and 860 ms to prewarm on iOS. 260 ms of it is a blocking call on themain thread.

A Flutter UI can be attached during the pre-warm. The remaining time is joined to the time-to-first-frame latency.

Memory-wise, a cost sample (variable, depending on the use case) could be:

  • ~4 MB OS’s memory usage for creating pthreads.
  • ~10 MB GPU driver memory.
  • ~1 MB for Dart runtime-managed memory.
  • ~5 MB for Dart-loaded font maps.

Latency-wise, a cost sample (variable, depending on the use case) could be:

  • ~20 ms to collect the Flutter assets from the application package.
  • ~15 ms to dlopen the Flutter engine library.
  • ~200 ms to create the Dart VM and load the AOT snapshot.
  • ~200 ms to load Flutter-dependent fonts and assets.
  • ~400 ms to run the entrypoint, create the first widget tree, and compile the neededGPU shader programs.

The FlutterEngine should be pre-warmed late enough to delay thememory consumption needed but early enough to avoid combining theFlutter engine start-up time with the first frame latency of showing Flutter.

The exact timing depends on the app’s structure and heuristics. An example wouldbe to load the Flutter engine in the screen before the screen is drawn by Flutter.

Given an engine pre-warm, the first frame cost on UI attach is:

  • 320 ms on Android and an additional 12 MB (highly dependent on the screen’sphysical pixel size).
  • 200 ms on iOS and an additional 16 MB (highly dependent on the screen’s physicalpixel size).

Memory-wise, the cost is primarily the graphical memory buffer used forrendering and is dependent on the screen size.

Latency-wise, the cost is primarily waiting for the OS callback to provideFlutter with a rendering surface and compiling the remaining shader programsthat are not pre-emptively predictable. This is a one-time cost.

When the Flutter UI component is released, the UI-related memory is freed.This doesn’t affect the Flutter state, which lives in the FlutterEngine(unless the FlutterEngine is also released).