blob: 147794823f95f95cb1ef75d9d40007023f8f006f [file] [log] [blame] [view]
Flutter combines [a Dart framework](https://github.com/flutter/flutter) with a high-performance [engine](https://github.com/flutter/engine).
The Flutter Engine is a portable runtime for high-quality cross-platform
applications. It implements Flutter's core libraries, including animation and
graphics, file and network I/O, accessibility support, plugin architecture, and
a Dart runtime and toolchain for developing, compiling, and running Flutter
applications.
## Anatomy of a Flutter app
The following diagram gives an overview of the pieces that make up a regular
Flutter app with one engine generated by `flutter create`. It shows where the
Flutter Engine sits in this stack, highlights API boundaries, and identifies the
repositories where the individual pieces live. The legend below clarifies some
of the terminology commonly used to describe the pieces of a Flutter app.
<img src="https://raw.githubusercontent.com/flutter/engine/main/docs/app_anatomy.svg?sanitize=true" alt="Flutter Architecture Diagram" width="40%"/>
#### Dart App
* Composes widgets into the desired UI.
* Implements business logic.
* Owned by app developer.
#### Framework ([source code](https://github.com/flutter/flutter/tree/main/packages/flutter/lib))
* Provides higher-level API to build high-quality apps (e.g. widgets,
hit-testing, gesture detection, accessibility, text input, etc.).
* Composites the app's widget tree into a scene.
#### Engine ([source code](https://github.com/flutter/engine/tree/main/shell/common))
* Responsible for rasterizing composited scenes.
* Provides low-level implementation of Flutter's core APIs (e.g. graphics, text
layout, Dart runtime, etc.).
* Exposes its functionality via its **dart:ui API** to the framework.
* Integrates with a specific platform via the Engine's **Embedder API**.
#### Embedder ([source code](https://github.com/flutter/engine/tree/main/shell/platform))
* Coordinates with the underlying operating system for access to services like
rendering surfaces, accessibility, and input.
* Manages the event loop.
* Exposes **platform-specific API** to integrate the Embedder into apps.
#### Runner
* Composes the pieces exposed by the platform-specific API of the Embedder into
an app package runnable on the target platform.
* Part of app template generated by `flutter create`, owned by app developer.
## Architecture overview
Flutter's engine takes core technologies, Skia, a 2D graphics rendering library,
and Dart, a VM for a garbage-collected object-oriented language, and hosts them
in a shell. Different platforms have different shells, for example we have
shells for
[Android](https://github.com/flutter/engine/tree/main/shell/platform/android)
and [iOS](https://github.com/flutter/engine/tree/main/shell/platform/darwin). We
also have an [embedder
API](https://github.com/flutter/engine/tree/main/shell/platform/embedder) which
allows Flutter's engine to be used as a library (see [Custom Flutter Engine
Embedders](../engine/Custom-Flutter-Engine-Embedders.md)).
The shells implement platform-specific code such as communicating with IMEs
(on-screen keyboards) and the system's application lifecycle events.
The Dart VM implements the normal Dart core libraries, plus an additional
library called `dart:ui` to provide low-level access to Skia features and the
shell. The shells can also communicate directly to Dart code via [Platform
Channels](https://flutter.io/platform-channels/) which bypass the engine.
![Flutter Architecture Diagram](https://raw.githubusercontent.com/flutter/engine/main/docs/flutter_overview.svg?sanitize=true)
## Threading
### Overview
The Flutter engine does not create or manage its own threads. Instead, it is the
responsibility of the embedder to create and manage threads (and their message
loops) for the Flutter engine. The embedder gives the Flutter engine task
runners for the threads it manages. In addition to the threads managed by the
embedder for the engine, the Dart VM also has its own thread pool. Neither the
Flutter engine nor the embedder have any access to the threads in this pool.
### Task Runner Configuration
The Flutter engine requires the embedder to give it references to 4 task
runners. The engine does not care if the references are to the same task runner,
or, if multiple task runners are serviced on the same thread. For optimum
performance, the embedder should create a dedicated thread per task runner.
Though the engine does not care about the threads the task runners are serviced
on, it does expect that the threading configuration remain stable for the entire
lifetime of the engine. That is, once the embedder decides to service a task
runner on a particular thread, it should execute tasks for that task runner only
on that one thread (till the engine is torn down).
The main task runners are:
* Platform Task Runner
* UI Task Runner
* Raster Task Runner
* IO Task Runner
### Platform Task Runner
This is the task runner for the thread the embedder considers as its main
thread. For example, this is typically the [Android Main
Thread](https://developer.android.com/guide/components/processes-and-threads.html)
or the [Main
Thread](https://developer.apple.com/documentation/foundation/nsthread/1412704-ismainthread?language=objc)
referenced by Foundation on Apple platforms.
Any significance assigned to the thread for this task runner is entirely
assigned by the embedder. The Flutter engine assigns no special meaning to this
thread. In fact, multiple Flutter engines can be launched with platform task
runners based on different threads. This is how the Flutter Content Handler in
Fuchsia works. A new Flutter engine is created in the process for each Flutter
application and a new platform thread is created for each engine.
Interacting with the Flutter engine in any way must happen on the platform
thread. Interacting with the engine on any other thread will trip assertions in
unoptimized builds and is not thread safe in release builds. There are numerous
components in the Flutter engine that are not thread safe. Once the Flutter
engine is set up and running, the embedder does not have to post tasks to any of
the task runners used to configure the engine as long as all accesses to the
embedder API are made on the platform thread.
In addition to being the thread on which the embedder interacts with the engine
after it is launched, this task runner also executes any pending platform
messages. This is handy because accessing most platform APIs is only safe on the
platform’s main thread. Plugins don’t have to rethread their calls to the main
thread. If plugins manage their own worker threads, it is their responsibility
to queue responses back onto the platform thread before they can be submitted
back to the engine for processing by Dart code. The rule of always interacting
with the engine on the platform thread holds here.
Even though blocking the platform thread for inordinate amounts of time will not
block the Flutter rendering pipeline, platforms do impose restrictions on
expensive operations on this thread. So it is advised that any expensive work in
response to platform messages be performed on separate worker threads (unrelated
to the four threads discussed above) before having the responses queued back on
the the platform thread for submission to the engine. Not doing so may result in
platform-specific watchdogs terminating the application. Embeddings such as
Android and iOS also uses the platform thread to pipe through user input events.
A blocked platform thread can also cause gestures to be dropped.
### UI Task Runner
The UI task runner is where the engine executes all Dart code for the root
isolate. The root isolate is a special isolate that has the necessary bindings
for Flutter to function. This isolate runs the application's main Dart code.
Bindings are set up on this isolate by the engine to schedule and submit frames.
For each frame that Flutter has to render:
* The root isolate has to tell the engine that a frame needs to be rendered.
* The engine will ask the platform that it should be notified on the next vsync.
* The platform waits for the next vsync.
* On vsync, the engine will wake up the Dart code and [perform the
following](https://api.flutter.dev/flutter/widgets/WidgetsBinding/drawFrame.html):
* Update animation interpolators.
* Rebuild the widgets in the application in a build phase.
* Lay out the newly constructed and widgets and paint them into a tree of
layers that are immediately submitted to the engine. Nothing is actually
rasterized here; only a description of what needs to be painted is
constructed as part of the paint phase.
* Construct or update a tree of nodes containing semantic information about
widgets on screen. This is used to update platform specific accessibility
components.
Apart from building frames for the engine to eventually render, the root isolate
also executes all responses for platform plugin messages, timers, microtasks and
asynchronous I/O (from sockets, file handles, etc.).
Since the UI thread constructs the layer tree that determines what the engine
will eventually paint onto the screen, it is the source of truth for everything
on the screen. Consequently, performing long synchronous operations on this
thread will cause jank in Flutter applications (a few milliseconds is enough to
miss the next frame!). Long operations can typically only be caused by Dart code
since the engine will not schedule any native code tasks on this task runner.
Because of this, this task runner (or thread) is typically referred to as the
Dart thread. It is possible for the embedder to post tasks onto this task
runner. This may cause jank in Flutter and embedders are advised not to do this
and instead assign a dedicated thread for this task runner.
If it is unavoidable for Dart code to perform expensive work, it is advised that
this code be moved into a separate [Dart
isolate](https://api.flutter.dev/flutter/dart-isolate/dart-isolate-library.html)
(e.g. using the
[`compute`](https://api.flutter.dev/flutter/foundation/compute-constant.html)
method). Dart code executing on a non-root isolate executes on a thread from a
Dart VM managed thread pool. This cannot cause jank in a Flutter application.
Terminating the root isolate will also terminate all isolates spawned by that
root isolate. Also, non-root isolates are incapable of scheduling frames and do
not have bindings that the Flutter framework depends on. Due to this, you cannot
interact with the Flutter framework in any meaningful way on the secondary
isolate. Use secondary isolates for tasks that require heavy computation.
### Raster Task Runner
The raster task runner executes tasks that need to access the rasterizer
(usually backed by GPU) on the device. The layer tree created by the Dart code
on the UI task runner is client-rendering-API agnostic. That is, the same layer
tree can be used to render a frame using OpenGL, Vulkan, software or really any
other backend configured for Skia. Components on the GPU task runner take the
layer tree and construct the appropriate draw commands. The raster task runner
components are also responsible for setting up all the GPU resources for a
particular frame. This includes talking to the platform to set up the
framebuffer, managing surface lifecycle, and ensuring that textures and buffers
for a particular frame are fully prepared.
Depending on how long it takes for the layer tree to be processed and the device
to finish displaying the frame, the various components of the raster task runner
may delay scheduling of further frames on the UI thread. Typically, the UI and
raster task runners are on different threads. In such cases, the raster thread
can be in the process of submitting a frame to the GPU while the UI thread is
already preparing the next frame. The pipelining mechanism makes sure that the
UI thread does not schedule too much work for the rasterizer.
Since the raster task runner components can introduce frame scheduling delays on
the UI thread, performing too much work on the raster thread will cause jank in
Flutter applications. Typically, there is no opportunity for the user to perform
custom tasks on this task runner because neither platform code nor Dart code can
access this task runner. However, it is still possible for the embedder to
schedule tasks on this thread. For this reason, it is recommended that embedders
provide a dedicated thread for the raster task runner per engine instance.
### IO Task Runner
All the task runners mentioned so far have pretty strong restrictions on the
kinds of operations that can be performed on this. Blocking the platform task
runner for an inordinate amount of time may trigger the platform's watchdog, and
blocking either the UI or raster task runners will cause jank in Flutter
applications. However, there are tasks necessary for the raster thread that
require doing some very expensive work. This expensive work is performed on the
IO task runner.
The main function of the IO task runner is reading compressed images from an
asset store and making sure these images are ready for rendering on the raster
task runner. To make sure a texture is ready for rendering, it first has to be
read as a blob of compressed data (typically PNG, JPEG, etc.) from an asset
store, decompressed into a GPU friendly format and uploaded to the GPU. These
operations are expensive and will cause jank if performed on the raster task
runner. Since only the raster task runner can access the GPU, the IO task runner
components set up a special context that is in the same sharegroup as the main
raster task runner context. This happens very early during engine setup and is
also the reason there is a single task runner for IO tasks. In reality, the
reading of the compressed bytes and decompression can happen on a thread pool.
The IO task runner is special because access to the context is only safe from a
specific thread. The only way to get a resource like
[`ui.Image`](https://docs.flutter.io/flutter/dart-ui/instantiateImageCodec.html)
is via an async call; this allows the framework to talk to the IO runner so that
it can asynchronously perform all the texture operations mentioned. The image
can then be immediately used in a frame without the raster thread having to do
expensive work.
There is no way for user code to access this thread either via Dart or native
plugins. Even the embedder is free to schedule tasks on this thread that are
fairly expensive. This won’t cause jank in Flutter applications but may delay
having the futures images and other resources be resolved in a timely manner.
Even so, it is recommended that custom embedders set up a dedicated thread for
this task runner.
### Flutter Engine Groups
There are two different ways to create Flutter engines which has ramifications
on how threading is managed across multiple Flutter engines. Flutter engines can
be instantiated directly or through the FlutterEngineGroup APIs
[[ios](https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Headers/FlutterEngineGroup.h),
[android](https://api.flutter.dev/javadoc/io/flutter/embedding/engine/FlutterEngineGroup.html)].
If an engine is instantiated directly, it gets its own engine threads and [dart isolate
group](https://dart.dev/language/concurrency#performance-and-isolate-groups).
However, all engines instantiated with a FlutterEngineGroup share the same
threads across the group. Each engine has their own root isolate but share the
same UI task runner, Dart isolate group and source code.
As a result engines in a FlutterEngineGroup will have a smaller memory
footprint, faster startup latency, and better CPU core usage with shared threads
and a shared thread pool for isolates.
### Current Platform Specific Threading Configurations
As mentioned, the engine can support multiple threading configurations, the
configurations currently used by the supported platforms are:
#### iOS
A dedicated thread is created for the UI, raster and IO task runners per engine
instance. All engine instances share the same platform thread and task runner.
#### Android
A dedicated thread is created for the UI, raster and IO task runners per engine
instance. All engine instances share the same platform thread and task runner.
#### Fuchsia
A dedicated thread is created for the UI, raster, IO and Platform task runners
per engine instance.
#### macOS
A dedicated thread is created for the UI, raster and IO task runners per engine
instance. All engine instances share the same platform thread and task runner.
#### Windows
A dedicated thread is created for the UI, raster and IO task runners per engine
instance. All engine instances share the same platform thread and task runner.
#### Linux
A dedicated thread is created for the UI and IO task runners per engine
instance. The platform thread and the raster thread are the same thread. All
engine instances share the same platform thread and task runner.
#### Flutter Tester (used by `flutter test`)
The same main thread is used for the UI, raster, IO and Platform task runners
for the single instance engine supported in the process.
## Text rendering
Our text rendering stack is as follows:
* A minikin derivative we call libtxt (font selection, bidi, line breaking).
* HarfBuzz (glyph selection, shaping).
* Skia (rendering/GPU back-end), which uses FreeType for font rendering on
Android and Fuchsia, and CoreGraphics for font rendering on iOS.