This is the fourth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

The article series Flutter is serialized

  • An In-depth analysis of the Engineering Structure and Application Layer Compilation source code of Flutter Android
  • The Nature of the Flutter Command: An In-depth source analysis of the Flutter Tools mechanism
  • RunApp of Flutter and Source Analysis of the Three Tree Birth Process
  • Source Code Analysis of the Activity/Fragment process on the Android Side Flutter
  • Source Analysis of Flutter Android FlutterInjector and Dependent Process
  • Source Code Analysis of Flutter Android FlutterEngine Java Related Processes

background

FlutterEngine, FlutterEngineGroup, FlutterEngineCache and other related classes are often used in the Java layer of the Flutter. This paper is an anatomical analysis of them. Since the version of Flutter 2 made a major adjustment in this area, our analysis takes version 2.2.3 as an example.

FlutterEngine correlation analysis

FlutterEngine is a stand-alone container for the Flutter runtime environment that allows you to run Dart code in Android applications. Dart code in FlutterEngine can be executed in the background, or Dart UI effects can be rendered to the screen using the accompanying FlutterRenderer and Dart code, rendering can start and stop, This allows FlutterEngine to move from UI interactions to data-only capabilities, and then back to UI interactions.

Executing Dart or Flutter code with FlutterEngine requires first getting a DartExecutor reference through FlutterEngine, Then call DartExecutor executeDartEntrypoint (DartExecutor. DartEntrypoint) perform the Dart code, The same FlutterEngine instance of DartExecutor executeDartEntrypoint (DartExecutor. DartEntrypoint) method can only be called once, be sure to keep in mind.

To render the Flutter contents onto the screen, you need to call FlutterEngine’s getRenderer() method to get a FlutterRenderer reference, Then attach the FlutterRenderer instance to a RenderSurface (such as the default FlutterView, That is, one of its internal FlutterSurfaceView, FlutterTextureView, and FlutterImageView (see previous articles).

When the first FlutterEngine instance is created in each process of the App, the Flutter engine’s native library is loaded and the Dart VM (VM lifecycle follow process) is started. Other FlutterEngines in the same process will then run on the same VM instance. But when running DartExecutor, you will have your own Dart Isolate. Each Isolate is an independent Dart environment. They cannot communicate with each other unless the Isolate port is used. [See official documents]

Therefore, for a multi-process and multi-Flutterengine app, the relationship between FlutterEngine and DartExecutor, Dart VM and Isolate is roughly as follows:Here is a snippet of FlutterEngine’s core source code:

public class FlutterEngine {
  //Flutter C/C++ interacts with the Platform Java layer interface definition.
  @NonNull private final FlutterJNI flutterJNI;
  // To render the Flutter Dart UI to the screen, the renderer will attach to the RenderSurface.
  @NonNull private final FlutterRenderer renderer;
  // The Dart actuator.
  @NonNull private final DartExecutor dartExecutor;
  // To manage android components and the Flutter plugins plugin.
  @NonNull private final FlutterEngineConnectionRegistry pluginRegistry;
  // a localized Android implementation plugin.
  @NonNull private final LocalizationPlugin localizationPlugin;

  // A bunch of system channels.
  @NonNull private final AccessibilityChannel accessibilityChannel;
  @NonNull private final DeferredComponentChannel deferredComponentChannel;
  @NonNull private final KeyEventChannel keyEventChannel;
  @NonNull private final LifecycleChannel lifecycleChannel;
  @NonNull private final LocalizationChannel localizationChannel;
  @NonNull private final MouseCursorChannel mouseCursorChannel;
  @NonNull private final NavigationChannel navigationChannel;
  @NonNull private final RestorationChannel restorationChannel;
  @NonNull private final PlatformChannel platformChannel;
  @NonNull private final SettingsChannel settingsChannel;
  @NonNull private final SystemChannel systemChannel;
  @NonNull private final TextInputChannel textInputChannel;

  // Platform Views.
  @NonNull private final PlatformViewsController platformViewsController;
  // Engine Lifecycle.
  @NonNull private final Set<EngineLifecycleListener> engineLifecycleListeners = new HashSet<>();
  / /...

  // The full-argument constructor, where all the construction ends up
  public FlutterEngine(
      @NonNull Context context,
      @Nullable FlutterLoader flutterLoader,
      @NonNull FlutterJNI flutterJNI,
      @NonNull PlatformViewsController platformViewsController,
      @Nullable String[] dartVmArgs,
      boolean automaticallyRegisterPlugins,
      boolean waitForRestorationData) {
    / /...
    // Create a DartExecutor and pass in flutterJNI and Android assetManager instances.
    this.dartExecutor = new DartExecutor(flutterJNI, assetManager);
    this.dartExecutor.onAttachedToJNI();
    / /...
    Each channel instantiation is analyzed separately below.
    accessibilityChannel = new AccessibilityChannel(dartExecutor, flutterJNI);
    deferredComponentChannel = new DeferredComponentChannel(dartExecutor);
    keyEventChannel = new KeyEventChannel(dartExecutor);
    lifecycleChannel = new LifecycleChannel(dartExecutor);
    localizationChannel = new LocalizationChannel(dartExecutor);
    mouseCursorChannel = new MouseCursorChannel(dartExecutor);
    navigationChannel = new NavigationChannel(dartExecutor);
    platformChannel = new PlatformChannel(dartExecutor);
    restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
    settingsChannel = new SettingsChannel(dartExecutor);
    systemChannel = new SystemChannel(dartExecutor);
    textInputChannel = new TextInputChannel(dartExecutor);
    / /...
    // Plug-in instantiation.
    this.localizationPlugin = new LocalizationPlugin(context, localizationChannel);

    this.flutterJNI = flutterJNI;
    if (flutterLoader == null) {
      flutterLoader = FlutterInjector.instance().flutterLoader();
    }
    / /...

    this.pluginRegistry =
        new FlutterEngineConnectionRegistry(context.getApplicationContext(), this, flutterLoader);
	// The plugins are automatically registered by default, and can be configured through the manifest file, etc.
    if(automaticallyRegisterPlugins && flutterLoader.automaticallyRegisterPlugins()) { registerPlugins(); }}/ /...

  // Register all the dependent Flutter plugins in pubspec. Yaml at the root of the project.
  / / Flutter tool generates a GeneratedPluginRegistrant class.
  private void registerPlugins(a) {
    try{ Class<? > generatedPluginRegistrant = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant");
      Method registrationMethod = generatedPluginRegistrant.getDeclaredMethod("registerWith", FlutterEngine.class);
      registrationMethod.invoke(null.this);
    } catch (Exception e) {
      Log.w(TAG, "Tried to automatically register plugins with FlutterEngine ("
              + this + ") but could not find and invoke the GeneratedPluginRegistrant."); }}/ /... Get method that omits a bunch of attribute members
}
Copy the code

The above code snippet is actually quite straightforward, mainly responsible for the various plug-in, Channel, rendering environment, run environment instantiation preparation. Let’s focus on the registerPlugins() method above, He internal reflection calls the IO. Flutter. Plugins. RegisterWith GeneratedPluginRegistrant class (this) method passing in the current FlutterEngine instance.

You might ask, well, thisio.flutter.plugins.GeneratedPluginRegistrantWhere do classes come from? It’s actually at the root of the Project Flutterpubspec.yamlFile dependencies that have the Flutter Plugin will be executedflutter pub getSuch as Flutter tools command to automatically generate a class called GeneratedPluginRegistrant, containing the dependent Flutter plugins add related code. Let’s use a demo to illustrate this, as shown belowpubspec.yamlThe webview_flutter Plugin is added to the webview_flutter Plugin. The effect of running pub get is as follows:As you can see, it is called when you construct an instantiation of FlutterEngineregisterPlugins()Method,registerPlugins()Methods are automatically generated by reflection callsio.flutter.plugins.GeneratedPluginRegistrantOf the classregisterWith(this)Method to pass in the current FlutterEngine instance. whileio.flutter.plugins.GeneratedPluginRegistrantOf the classregisterWith(this)The main method is to put us inpubspec.yamlThe Flutter Plugin dependencies in the file are appented to the Plugins collection.

Let’s first look at the flutterEngine. GetPlugins ().add(XXX) method:

class FlutterEngineConnectionRegistry
    implements PluginRegistry.ActivityControlSurface.ServiceControlSurface.BroadcastReceiverControlSurface.ContentProviderControlSurface {
  / /...
  @Override
  public void add(@NonNull FlutterPlugin plugin) {
    / /...
    plugins.put(plugin.getClass(), plugin);
    plugin.onAttachedToEngine(pluginBinding);

    // For ActivityAware plugins, add the plugin to our set of ActivityAware
    // plugins, and if this engine is currently attached to an Activity,
    // notify the ActivityAware plugin that it is now attached to an Activity.
    if (plugin instanceof ActivityAware) {
      ActivityAware activityAware = (ActivityAware) plugin;
      activityAwarePlugins.put(plugin.getClass(), activityAware);

      if(isAttachedToActivity()) { activityAware.onAttachedToActivity(activityPluginBinding); }}// For ServiceAware plugins, add the plugin to our set of ServiceAware
    // plugins, and if this engine is currently attached to a Service,
    // notify the ServiceAware plugin that it is now attached to a Service.
    if (plugin instanceof ServiceAware) {
      ServiceAware serviceAware = (ServiceAware) plugin;
      serviceAwarePlugins.put(plugin.getClass(), serviceAware);

      if(isAttachedToService()) { serviceAware.onAttachedToService(servicePluginBinding); }}// For BroadcastReceiverAware plugins, add the plugin to our set of BroadcastReceiverAware
    // plugins, and if this engine is currently attached to a BroadcastReceiver,
    // notify the BroadcastReceiverAware plugin that it is now attached to a BroadcastReceiver.
    if (plugin instanceof BroadcastReceiverAware) {
      BroadcastReceiverAware broadcastReceiverAware = (BroadcastReceiverAware) plugin;
      broadcastReceiverAwarePlugins.put(plugin.getClass(), broadcastReceiverAware);

      if(isAttachedToBroadcastReceiver()) { broadcastReceiverAware.onAttachedToBroadcastReceiver(broadcastReceiverPluginBinding); }}// For ContentProviderAware plugins, add the plugin to our set of ContentProviderAware
    // plugins, and if this engine is currently attached to a ContentProvider,
    // notify the ContentProviderAware plugin that it is now attached to a ContentProvider.
    if (plugin instanceof ContentProviderAware) {
      ContentProviderAware contentProviderAware = (ContentProviderAware) plugin;
      contentProviderAwarePlugins.put(plugin.getClass(), contentProviderAware);

      if(isAttachedToContentProvider()) { contentProviderAware.onAttachedToContentProvider(contentProviderPluginBinding); }}}/ /...
}
Copy the code

As you can see, the add method of FlutterEngineConnectionRegistry don’t can understand we need to do too much explanation, mainly is to add a FlutterPlugin instance, It then calls a bunch of life-cycle like methods of the FlutterPlugin interface convention, such as onAttachedToEngine, and depending on the specific type of plug-in (Android platform component type, Activity, Service, Broadcast, ContentProvider) so that the developer of the Flutter Plugin can apply their own platform logic to these timing methods. For example, the webview_flutter Plugin is implemented as follows:

public class WebViewFlutterPlugin implements FlutterPlugin {
  private FlutterCookieManager flutterCookieManager;
  / /...
  / / in the add FlutterEngineConnectionRegistry trigger calls, instantiation BinaryMessenger and FlutterCookieManager.
  @Override
  public void onAttachedToEngine(FlutterPluginBinding binding) {
    BinaryMessenger messenger = binding.getBinaryMessenger();
    binding.getPlatformViewRegistry().registerViewFactory("plugins.flutter.io/webview".new WebViewFactory(messenger, /*containerView=*/ null));
    flutterCookieManager = new FlutterCookieManager(messenger);
  }
  
  Trigger calls, remove the / / FlutterEngineConnectionRegistry remove plug-ins.
  @Override
  public void onDetachedFromEngine(FlutterPluginBinding binding) {
    if (flutterCookieManager == null) {
      return;
    }
    flutterCookieManager.dispose();
    flutterCookieManager = null; }}Copy the code

Understand each compiled under the Flutter when Android App automatically generated GeneratedPluginRegistrant is what happened. Also know why ask GeneratedPluginRegistrant class needs to keep live in confusion listing reasons. The overall process is roughly as follows:The various instantiation channels in the FlutterEngine constructor will not be expanded here and will be resolved in a separate section.

FlutterEngineCache correlation analysis

FlutterEngineCache is a simple process singleton in which the FlutterEngine instances are cached by Map and the code is not much to analyze.

public class FlutterEngineCache {
  private static FlutterEngineCache instance;
  // Singleton mode
  public static FlutterEngineCache getInstance(a) {
    if (instance == null) {
      instance = new FlutterEngineCache();
    }
    return instance;
  }
  // Cache a collection of FlutterEngine instances based on key
  private final Map<String, FlutterEngine> cachedEngines = new HashMap<>();

  // Check whether the FlutterEngine instance with the specified ID is included
  public boolean contains(@NonNull String engineId) {
    return cachedEngines.containsKey(engineId);
  }

  // Get the FlutterEngine instance with the specified ID
  public FlutterEngine get(@NonNull String engineId) {
    return cachedEngines.get(engineId);
  }

  // Cache the FlutterEngine instance with the specified ID
  public void put(@NonNull String engineId, @Nullable FlutterEngine engine) {
    if(engine ! =null) {
      cachedEngines.put(engineId, engine);
    } else{ cachedEngines.remove(engineId); }}// Delete the FlutterEngine instance with the specified ID
  public void remove(@NonNull String engineId) {
    put(engineId, null);
  }

  // Clear the entire cache
  public void clear(a) { cachedEngines.clear(); }}Copy the code

FlutterActivity support and cache FlutterEngine used together, can pass FlutterActivity. WithCachedEngine (String) to build a FlutterActivity Intent, The Intent is configured to use the existing cache FlutterEngine. Using cache FlutterEngine, the FlutterEngine should have been executed Dart code, that is to say the Dart entry point and the initial route have been defined, so CachedEngineIntentBuilder does not provide these configuration properties. FlutterEngineCache is recommended as this will warm up the engine and reduce the white screen or wait time when the Flutter page starts, as stated by the official.

FlutterEngineGroup correlation analysis

Those of you who have been exposed to Flutter very early know that in the mixed development of Flutter, One of the most criticized problems with the multiple Flutter pages (FlutterActivity) pattern is that it generates multiple Instances of FlutterEngine and each FlutterEngine instance is very memory intensive. Therefore, the folk Flutter Boost scheme similar to the salt fish Flutter was developed. The single FlutterEngine scheme was adopted (scenes such as split screen were not compatible) and the memory of the whole single-process App was shared under the same Isolate. Later, due to the outcry from the community, the authorities also made efforts to release the experimental FlutterEngineGroup, the official solution of multiple Flutterengines in Flutter 2.0, that is, each page is a FlutterEngine. Or a page contains multiple FlutterEngines. Each FlutterEngine corresponds to an Isolate and does not share memory. Officially, the FlutterEngine memory generated from the FlutterEngineGroup will only increase by 180K because it shares common resources (such as GPU context, font metrics, snapshots of isolated threads, etc.) to speed up first rendering, reduce latency, and reduce memory usage. However, as of the 2.2 release of Flutter, FlutterEngineGroup is still an experimental feature and is not recommended for use in formal projects. See the official multiple-Flutters documentation. Let’s look at the core source code:

public class FlutterEngineGroup {
  final List<FlutterEngine> activeEngines = new ArrayList<>();
  / /...

  // Create a FlutterEngine using the FlutterEngineGroup instance. The first FlutterEngine is not different from the other ones.
  public FlutterEngine createAndRunEngine(
      @NonNull Context context, @Nullable DartEntrypoint dartEntrypoint) {
    FlutterEngine engine = null;
    / /...
    if (activeEngines.size() == 0) {
      // The first FlutterEngine created from the FlutterEngineGroup is the same as a normal FlutterEngine.
      engine = createEngine(context);
      engine.getDartExecutor().executeDartEntrypoint(dartEntrypoint);
    } else {
      // The first FlutterEngine created from the FlutterEngineGroup is not the first FlutterEngine. Spawn based on the first one.
      engine = activeEngines.get(0).spawn(context, dartEntrypoint);
    }

    activeEngines.add(engine);
	/ /...
    return engine;
  }

  FlutterEngine createEngine(Context context) {
    return newFlutterEngine(context); }}Copy the code

As you can see, the difference between FlutterEngine and normal FlutterEngine is that the createAndRunEngine method for the FlutterEngineGroup is created differently because, The createAndRunEngine method creates the first FlutterEngine instance as normal. When creating the second FlutterEngine instance, the spawn(Context, dartEntrypoint) method of the first FlutterEngine instance is used. So let’s look at the Spawn method of FlutterEngine, as follows:

FlutterEngine spawn(@NonNull Context context, @NonNull DartEntrypoint dartEntrypoint) {
  / /...
  FlutterJNI newFlutterJNI =
      flutterJNI.spawn(
          dartEntrypoint.dartEntrypointFunctionName, dartEntrypoint.dartEntrypointLibrary);
  return new FlutterEngine(
      context, // Context.
      null.// FlutterLoader. A null value passed here causes the constructor to get it from the
      // FlutterInjector.
      newFlutterJNI); // FlutterJNI.
}
Copy the code

It is clear that the Spawn method is implemented in the C/C ++ layer of FlutterEngine, which we will not follow up on, as you can see from his comments, This approach to creating a second FlutterEngine based on the current FlutterEngine minimalizes startup latency and memory costs by sharing as many resources as possible, which is the official solution. For the demo, see add_to_app/multiple_flutters on github samples.

conclusion

It can be seen that the essence of FlutterEngine, FlutterEngineCache and FlutterEngineGroup are all FlutterEngine, and what is more is the mechanism of swapping space for time and sharing space. Other official FlutterEngineGroup will be released as soon as possible.