diff --git a/.gitignore b/.gitignore index d46d282df64d9..dfae6ab8dc57b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .*.sw? .DS_Store .ccls-cache +.cache .classpath .clangd/ .cproject diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 02a871110fbb2..5f784be07af1e 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -748,6 +748,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Trans FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java diff --git a/runtime/isolate_configuration.h b/runtime/isolate_configuration.h index 1fc3efef51323..f1a018704d6c0 100644 --- a/runtime/isolate_configuration.h +++ b/runtime/isolate_configuration.h @@ -54,7 +54,7 @@ class IsolateConfiguration { /// using the legacy settings fields that specify /// the asset by name instead of a mappings /// callback. - /// @param[in] io_worker An optional IO worker. Specify `nullptr` is a + /// @param[in] io_worker An optional IO worker. Specify `nullptr` if a /// worker should not be used or one is not /// available. /// diff --git a/shell/common/shell.cc b/shell/common/shell.cc index c135a76c6c3a3..fe3e687f1333d 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -475,12 +475,12 @@ Shell::~Shell() { } std::unique_ptr Shell::Spawn( - Settings settings, + RunConfiguration run_configuration, const CreateCallback& on_create_platform_view, const CreateCallback& on_create_rasterizer) const { FML_DCHECK(task_runners_.IsValid()); std::unique_ptr result(Shell::Create( - task_runners_, PlatformData{}, settings, + task_runners_, PlatformData{}, GetSettings(), vm_->GetVMData()->GetIsolateSnapshot(), on_create_platform_view, on_create_rasterizer, vm_, [engine = this->engine_.get()]( @@ -498,10 +498,8 @@ std::unique_ptr Shell::Spawn( /*settings=*/settings, /*animator=*/std::move(animator)); })); - RunConfiguration configuration = - RunConfiguration::InferFromSettings(settings); result->shared_resource_context_ = io_manager_->GetSharedResourceContext(); - result->RunEngine(std::move(configuration)); + result->RunEngine(std::move(run_configuration)); return result; } diff --git a/shell/common/shell.h b/shell/common/shell.h index c0ae17a929148..3749d4727bca7 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -219,9 +219,19 @@ class Shell final : public PlatformView::Delegate, /// and a smaller memory footprint than an Shell created with a /// Create function. /// + /// The new Shell is returned in a running state so RunEngine + /// shouldn't be called again on the Shell. Once running, the + /// second Shell is mostly independent from the original Shell + /// and the original Shell doesn't need to keep running for the + /// spawned Shell to keep functioning. + /// @param[in] run_configuration A RunConfiguration used to run the Isolate + /// associated with this new Shell. It doesn't have to be the same + /// configuration as the current Shell but it needs to be in the + /// same snapshot or AOT. + /// /// @see http://flutter.dev/go/multiple-engines std::unique_ptr Spawn( - Settings settings, + RunConfiguration run_configuration, const CreateCallback& on_create_platform_view, const CreateCallback& on_create_rasterizer) const; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index f5adef7425ed5..aca67acd5c081 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -2438,39 +2438,61 @@ TEST_F(ShellTest, Spawn) { ASSERT_TRUE(configuration.IsValid()); configuration.SetEntrypoint("fixturesAreFunctionalMain"); + auto second_configuration = RunConfiguration::InferFromSettings(settings); + ASSERT_TRUE(second_configuration.IsValid()); + second_configuration.SetEntrypoint("testCanLaunchSecondaryIsolate"); + fml::AutoResetWaitableEvent main_latch; + std::string last_entry_point; + // Fulfill native function for the first Shell's entrypoint. AddNativeCallback( - "SayHiFromFixturesAreFunctionalMain", - CREATE_NATIVE_ENTRY([&main_latch](auto args) { main_latch.Signal(); })); + "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) { + last_entry_point = shell->GetEngine()->GetLastEntrypoint(); + main_latch.Signal(); + })); + // Fulfill native function for the second Shell's entrypoint. + AddNativeCallback( + // The Dart native function names aren't very consistent but this is just + // the native function name of the second vm entrypoint in the fixture. + "NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) {})); RunEngine(shell.get(), std::move(configuration)); main_latch.Wait(); ASSERT_TRUE(DartVMRef::IsInstanceRunning()); + // Check first Shell ran the first entrypoint. + ASSERT_EQ("fixturesAreFunctionalMain", last_entry_point); - PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [this, - &spawner = shell, - settings]() { - MockPlatformViewDelegate platform_view_delegate; - auto spawn = spawner->Spawn( - settings, - [&platform_view_delegate](Shell& shell) { - auto result = std::make_unique( - platform_view_delegate, shell.GetTaskRunners()); - ON_CALL(*result, CreateRenderingSurface()) - .WillByDefault(::testing::Invoke( - [] { return std::make_unique(); })); - return result; - }, - [](Shell& shell) { return std::make_unique(shell); }); - ASSERT_NE(nullptr, spawn.get()); - ASSERT_TRUE(ValidateShell(spawn.get())); - - PostSync(spawner->GetTaskRunners().GetIOTaskRunner(), [&spawner, &spawn] { - ASSERT_EQ(spawner->GetIOManager()->GetResourceContext().get(), - spawn->GetIOManager()->GetResourceContext().get()); - }); - DestroyShell(std::move(spawn)); - }); + PostSync( + shell->GetTaskRunners().GetPlatformTaskRunner(), + [this, &spawner = shell, &second_configuration]() { + MockPlatformViewDelegate platform_view_delegate; + auto spawn = spawner->Spawn( + std::move(second_configuration), + [&platform_view_delegate](Shell& shell) { + auto result = std::make_unique( + platform_view_delegate, shell.GetTaskRunners()); + ON_CALL(*result, CreateRenderingSurface()) + .WillByDefault(::testing::Invoke( + [] { return std::make_unique(); })); + return result; + }, + [](Shell& shell) { return std::make_unique(shell); }); + ASSERT_NE(nullptr, spawn.get()); + ASSERT_TRUE(ValidateShell(spawn.get())); + + PostSync(spawner->GetTaskRunners().GetUITaskRunner(), [&spawn] { + // Check second shell ran the second entrypoint. + ASSERT_EQ("testCanLaunchSecondaryIsolate", + spawn->GetEngine()->GetLastEntrypoint()); + }); + + PostSync( + spawner->GetTaskRunners().GetIOTaskRunner(), [&spawner, &spawn] { + ASSERT_EQ(spawner->GetIOManager()->GetResourceContext().get(), + spawn->GetIOManager()->GetResourceContext().get()); + }); + DestroyShell(std::move(spawn)); + }); DestroyShell(std::move(shell)); ASSERT_FALSE(DartVMRef::IsInstanceRunning()); diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 0a2189a8e3afd..69ab05fc049b5 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -151,6 +151,7 @@ android_java_sources = [ "io/flutter/embedding/engine/FlutterEngine.java", "io/flutter/embedding/engine/FlutterEngineCache.java", "io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java", + "io/flutter/embedding/engine/FlutterEngineGroup.java", "io/flutter/embedding/engine/FlutterJNI.java", "io/flutter/embedding/engine/FlutterOverlaySurface.java", "io/flutter/embedding/engine/FlutterShellArgs.java", @@ -465,6 +466,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/android/RobolectricFlutterActivity.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", "test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java", + "test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java", "test/io/flutter/embedding/engine/FlutterEngineTest.java", "test/io/flutter/embedding/engine/FlutterJNITest.java", "test/io/flutter/embedding/engine/FlutterShellArgsTest.java", diff --git a/shell/platform/android/android_shell_holder.cc b/shell/platform/android/android_shell_holder.cc index 00aed2b25b876..e83980f4bf4f5 100644 --- a/shell/platform/android/android_shell_holder.cc +++ b/shell/platform/android/android_shell_holder.cc @@ -9,15 +9,21 @@ #include #include #include +#include +#include #include #include #include +#include "flutter/fml/logging.h" #include "flutter/fml/make_copyable.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/platform/android/jni_util.h" #include "flutter/shell/common/rasterizer.h" +#include "flutter/shell/common/run_configuration.h" +#include "flutter/shell/common/thread_host.h" +#include "flutter/shell/platform/android/context/android_context.h" #include "flutter/shell/platform/android/platform_view_android.h" namespace flutter { @@ -33,15 +39,16 @@ AndroidShellHolder::AndroidShellHolder( std::shared_ptr jni_facade, bool is_background_view) : settings_(std::move(settings)), jni_facade_(jni_facade) { - static size_t shell_count = 1; - auto thread_label = std::to_string(shell_count++); + static size_t thread_host_count = 1; + auto thread_label = std::to_string(thread_host_count++); + thread_host_ = std::make_shared(); if (is_background_view) { - thread_host_ = {thread_label, ThreadHost::Type::UI}; + *thread_host_ = {thread_label, ThreadHost::Type::UI}; } else { - thread_host_ = {thread_label, ThreadHost::Type::UI | - ThreadHost::Type::RASTER | - ThreadHost::Type::IO}; + *thread_host_ = {thread_label, ThreadHost::Type::UI | + ThreadHost::Type::RASTER | + ThreadHost::Type::IO}; } fml::WeakPtr weak_platform_view; @@ -64,8 +71,8 @@ AndroidShellHolder::AndroidShellHolder( ); } weak_platform_view = platform_view_android->GetWeakPtr(); - shell.OnDisplayUpdates(DisplayUpdateType::kStartup, - {Display(jni_facade->GetDisplayRefreshRate())}); + auto display = Display(jni_facade->GetDisplayRefreshRate()); + shell.OnDisplayUpdates(DisplayUpdateType::kStartup, {display}); return platform_view_android; }; @@ -82,14 +89,14 @@ AndroidShellHolder::AndroidShellHolder( fml::RefPtr platform_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); if (is_background_view) { - auto single_task_runner = thread_host_.ui_thread->GetTaskRunner(); + auto single_task_runner = thread_host_->ui_thread->GetTaskRunner(); raster_runner = single_task_runner; ui_runner = single_task_runner; io_runner = single_task_runner; } else { - raster_runner = thread_host_.raster_thread->GetTaskRunner(); - ui_runner = thread_host_.ui_thread->GetTaskRunner(); - io_runner = thread_host_.io_thread->GetTaskRunner(); + raster_runner = thread_host_->raster_thread->GetTaskRunner(); + ui_runner = thread_host_->ui_thread->GetTaskRunner(); + io_runner = thread_host_->io_thread->GetTaskRunner(); } flutter::TaskRunners task_runners(thread_label, // label @@ -129,9 +136,28 @@ AndroidShellHolder::AndroidShellHolder( is_valid_ = shell_ != nullptr; } +AndroidShellHolder::AndroidShellHolder( + const Settings& settings, + const std::shared_ptr& jni_facade, + const std::shared_ptr& thread_host, + std::unique_ptr shell, + const fml::WeakPtr& platform_view) + : settings_(std::move(settings)), + jni_facade_(jni_facade), + platform_view_(platform_view), + thread_host_(thread_host), + shell_(std::move(shell)) { + FML_DCHECK(jni_facade); + FML_DCHECK(shell_); + FML_DCHECK(shell_->IsSetup()); + FML_DCHECK(platform_view_); + FML_DCHECK(thread_host_); + is_valid_ = shell_ != nullptr; +} + AndroidShellHolder::~AndroidShellHolder() { shell_.reset(); - thread_host_.Reset(); + thread_host_->Reset(); } bool AndroidShellHolder::IsValid() const { @@ -142,12 +168,82 @@ const flutter::Settings& AndroidShellHolder::GetSettings() const { return settings_; } -void AndroidShellHolder::Launch(RunConfiguration config) { +std::unique_ptr AndroidShellHolder::Spawn( + std::shared_ptr jni_facade, + const std::string& entrypoint, + const std::string& libraryUrl) const { + FML_DCHECK(shell_ && shell_->IsSetup()) + << "A new Shell can only be spawned " + "if the current Shell is properly constructed"; + + // Pull out the new PlatformViewAndroid from the new Shell to feed to it to + // the new AndroidShellHolder. + // + // It's a weak pointer because it's owned by the Shell (which we're also) + // making below. And the AndroidShellHolder then owns the Shell. + fml::WeakPtr weak_platform_view; + + // Take out the old AndroidContext to reuse inside the PlatformViewAndroid + // of the new Shell. + PlatformViewAndroid* android_platform_view = platform_view_.get(); + // There's some indirection with platform_view_ being a weak pointer but + // we just checked that the shell_ exists above and a valid shell is the + // owner of the platform view so this weak pointer always exists. + FML_DCHECK(android_platform_view); + std::shared_ptr android_context = + android_platform_view->GetAndroidContext(); + FML_DCHECK(android_context); + + // This is a synchronous call, so the captures don't have race checks. + Shell::CreateCallback on_create_platform_view = + [&jni_facade, android_context, &weak_platform_view](Shell& shell) { + std::unique_ptr platform_view_android; + platform_view_android = std::make_unique( + shell, // delegate + shell.GetTaskRunners(), // task runners + jni_facade, // JNI interop + android_context // Android context + ); + weak_platform_view = platform_view_android->GetWeakPtr(); + auto display = Display(jni_facade->GetDisplayRefreshRate()); + shell.OnDisplayUpdates(DisplayUpdateType::kStartup, {display}); + return platform_view_android; + }; + + Shell::CreateCallback on_create_rasterizer = [](Shell& shell) { + return std::make_unique(shell); + }; + + // TODO(xster): could be worth tracing this to investigate whether + // the IsolateConfiguration could be cached somewhere. + auto config = BuildRunConfiguration(asset_manager_, entrypoint, libraryUrl); + if (!config) { + // If the RunConfiguration was null, the kernel blob wasn't readable. + // Fail the whole thing. + return nullptr; + } + + std::unique_ptr shell = shell_->Spawn( + std::move(config.value()), on_create_platform_view, on_create_rasterizer); + + return std::unique_ptr( + new AndroidShellHolder(GetSettings(), jni_facade, thread_host_, + std::move(shell), weak_platform_view)); +} + +void AndroidShellHolder::Launch(std::shared_ptr asset_manager, + const std::string& entrypoint, + const std::string& libraryUrl) { if (!IsValid()) { return; } - shell_->RunEngine(std::move(config)); + asset_manager_ = asset_manager; + auto config = BuildRunConfiguration(asset_manager, entrypoint, libraryUrl); + if (!config) { + return; + } + shell_->RunEngine(std::move(config.value())); } Rasterizer::Screenshot AndroidShellHolder::Screenshot( @@ -168,4 +264,37 @@ void AndroidShellHolder::NotifyLowMemoryWarning() { FML_DCHECK(shell_); shell_->NotifyLowMemoryWarning(); } + +std::optional AndroidShellHolder::BuildRunConfiguration( + std::shared_ptr asset_manager, + const std::string& entrypoint, + const std::string& libraryUrl) const { + std::unique_ptr isolate_configuration; + if (flutter::DartVM::IsRunningPrecompiledCode()) { + isolate_configuration = IsolateConfiguration::CreateForAppSnapshot(); + } else { + std::unique_ptr kernel_blob = + fml::FileMapping::CreateReadOnly( + GetSettings().application_kernel_asset); + if (!kernel_blob) { + FML_DLOG(ERROR) << "Unable to load the kernel blob asset."; + return std::nullopt; + } + isolate_configuration = + IsolateConfiguration::CreateForKernel(std::move(kernel_blob)); + } + + RunConfiguration config(std::move(isolate_configuration), + std::move(asset_manager)); + + { + if ((entrypoint.size() > 0) && (libraryUrl.size() > 0)) { + config.SetEntrypointAndLibrary(std::move(entrypoint), + std::move(libraryUrl)); + } else if (entrypoint.size() > 0) { + config.SetEntrypoint(std::move(entrypoint)); + } + } + return config; +} } // namespace flutter diff --git a/shell/platform/android/android_shell_holder.h b/shell/platform/android/android_shell_holder.h index d888893f77c27..39f0274fd49b5 100644 --- a/shell/platform/android/android_shell_holder.h +++ b/shell/platform/android/android_shell_holder.h @@ -7,6 +7,7 @@ #include +#include "flutter/assets/asset_manager.h" #include "flutter/fml/macros.h" #include "flutter/fml/unique_fd.h" #include "flutter/lib/ui/window/viewport_metrics.h" @@ -19,6 +20,23 @@ namespace flutter { +//---------------------------------------------------------------------------- +/// @brief This is the Android owner of the core engine Shell. +/// +/// @details This is the top orchestrator class on the C++ side for the +/// Android embedding. It corresponds to a FlutterEngine on the +/// Java side. This class is in C++ because the Shell is in +/// C++ and an Android orchestrator needs to exist to +/// compose it with other Android specific C++ components such as +/// the PlatformViewAndroid. This composition of many-to-one +/// C++ components would be difficult to do through JNI whereas +/// a FlutterEngine and AndroidShellHolder has a 1:1 relationship. +/// +/// Technically, the FlutterJNI class owns this AndroidShellHolder +/// class instance, but the FlutterJNI class is meant to be mostly +/// static and has minimal state to perform the C++ pointer <-> +/// Java class instance translation. +/// class AndroidShellHolder { public: AndroidShellHolder(flutter::Settings settings, @@ -29,7 +47,43 @@ class AndroidShellHolder { bool IsValid() const; - void Launch(RunConfiguration configuration); + //---------------------------------------------------------------------------- + /// @brief This is a factory for a derived AndroidShellHolder from an + /// existing AndroidShellHolder. + /// + /// @details Creates one Shell from another Shell where the created + /// Shell takes the opportunity to share any internal components + /// it can. This results is a Shell that has a smaller startup + /// time cost and a smaller memory footprint than an Shell created + /// with a Create function. + /// + /// The new Shell is returned in a new AndroidShellHolder + /// instance. + /// + /// The new Shell's flutter::Settings cannot be changed from that + /// of the initial Shell. The RunConfiguration subcomponent can + /// be changed however in the spawned Shell to run a different + /// entrypoint than the existing shell. + /// + /// Since the AndroidShellHolder both binds downwards to a Shell + /// and also upwards to JNI callbacks that the PlatformViewAndroid + /// makes, the JNI instance holding this AndroidShellHolder should + /// be created first to supply the jni_facade callback. + /// + /// @param[in] jni_facade this argument should be the JNI callback facade of + /// a new JNI instance meant to hold this AndroidShellHolder. + /// + /// @returns A new AndroidShellHolder containing a new Shell. Returns + /// nullptr when a new Shell can't be created. + /// + std::unique_ptr Spawn( + std::shared_ptr jni_facade, + const std::string& entrypoint, + const std::string& libraryUrl) const; + + void Launch(std::shared_ptr asset_manager, + const std::string& entrypoint, + const std::string& libraryUrl); const flutter::Settings& GetSettings() const; @@ -46,12 +100,33 @@ class AndroidShellHolder { const flutter::Settings settings_; const std::shared_ptr jni_facade_; fml::WeakPtr platform_view_; - ThreadHost thread_host_; + std::shared_ptr thread_host_; std::unique_ptr shell_; bool is_valid_ = false; uint64_t next_pointer_flow_id_ = 0; - + std::shared_ptr asset_manager_; + + //---------------------------------------------------------------------------- + /// @brief Constructor with its components injected. + /// + /// @details This is similar to the standard constructor, except its + /// members were constructed elsewhere and injected. + /// + /// All injected components must be non-null and valid. + /// + /// Used when constructing the Shell from the inside out when + /// spawning from an existing Shell. + /// + AndroidShellHolder(const flutter::Settings& settings, + const std::shared_ptr& jni_facade, + const std::shared_ptr& thread_host, + std::unique_ptr shell, + const fml::WeakPtr& platform_view); static void ThreadDestructCallback(void* value); + std::optional BuildRunConfiguration( + std::shared_ptr asset_manager, + const std::string& entrypoint, + const std::string& libraryUrl) const; FML_DISALLOW_COPY_AND_ASSIGN(AndroidShellHolder); }; diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 16007fa1e23c8..52de50afd0db0 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -12,6 +12,7 @@ import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint; import io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.PluginRegistry; @@ -114,6 +115,12 @@ public void onPreEngineRestart() { platformViewsController.onPreEngineRestart(); restorationChannel.clearData(); } + + @Override + public void onEngineWillDestroy() { + // This inner implementation doesn't do anything since FlutterEngine sent this + // notification in the first place. It's meant for external listeners. + } }; /** @@ -304,15 +311,23 @@ public FlutterEngine( if (flutterLoader == null) { flutterLoader = FlutterInjector.instance().flutterLoader(); } - flutterLoader.startInitialization(context.getApplicationContext()); - flutterLoader.ensureInitializationComplete(context, dartVmArgs); + + if (!flutterJNI.isAttached()) { + flutterLoader.startInitialization(context.getApplicationContext()); + flutterLoader.ensureInitializationComplete(context, dartVmArgs); + } flutterJNI.addEngineLifecycleListener(engineLifecycleListener); flutterJNI.setPlatformViewsController(platformViewsController); flutterJNI.setLocalizationPlugin(localizationPlugin); flutterJNI.setDeferredComponentManager(FlutterInjector.instance().deferredComponentManager()); - attachToJni(); + // It should typically be a fresh, unattached JNI. But on a spawned engine, the JNI instance + // is already attached to a native shell. In that case, the Java FlutterEngine is created around + // an existing shell. + if (!flutterJNI.isAttached()) { + attachToJni(); + } // TODO(mattcarroll): FlutterRenderer is temporally coupled to attach(). Remove that coupling if // possible. @@ -344,6 +359,36 @@ private boolean isAttachedToJni() { return flutterJNI.isAttached(); } + /** + * Create a second {@link FlutterEngine} based on this current one by sharing as much resources + * together as possible to minimize startup latency and memory cost. + * + * @param context is a Context used to create the {@link FlutterEngine}. Could be the same Context + * as the current engine or a different one. Generally, only an application Context is needed + * for the {@link FlutterEngine} and its dependencies. + * @param dartEntrypoint specifies the {@link DartEntrypoint} the new engine should run. It + * doesn't need to be the same entrypoint as the current engine but must be built in the same + * AOT or snapshot. + * @return a new {@link FlutterEngine}. + */ + @NonNull + /*package*/ FlutterEngine spawn( + @NonNull Context context, @NonNull DartEntrypoint dartEntrypoint) { + if (!isAttachedToJni()) { + throw new IllegalStateException( + "Spawn can only be called on a fully constructed FlutterEngine"); + } + + 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. + } + /** * Registers all plugins that an app lists in its pubspec.yaml. * @@ -382,6 +427,9 @@ private void registerPlugins() { */ public void destroy() { Log.v(TAG, "Destroying."); + for (EngineLifecycleListener listener : engineLifecycleListeners) { + listener.onEngineWillDestroy(); + } // The order that these things are destroyed is important. pluginRegistry.destroy(); platformViewsController.onDetachedFromJNI(); @@ -567,5 +615,11 @@ public ContentProviderControlSurface getContentProviderControlSurface() { public interface EngineLifecycleListener { /** Lifecycle callback invoked before a hot restart of the Flutter engine. */ void onPreEngineRestart(); + /** + * Lifecycle callback invoked before the Flutter engine is destroyed. + * + *

For the duration of the call, the Flutter engine is still valid. + */ + void onEngineWillDestroy(); } } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java new file mode 100644 index 0000000000000..0e9d76350d00f --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint; +import java.util.ArrayList; +import java.util.List; + +/** + * This class is experimental. Please do not ship production code using it. + * + *

Represents a collection of {@link io.flutter.embedding.engine.FlutterEngine}s who share + * resources to allow them to be created faster and with less memory than calling the {@link + * io.flutter.embedding.engine.FlutterEngine}'s constructor multiple times. + * + *

When creating or recreating the first {@link io.flutter.embedding.engine.FlutterEngine} in the + * FlutterEngineGroup, the behavior is the same as creating a {@link + * io.flutter.embedding.engine.FlutterEngine} via its constructor. When subsequent {@link + * io.flutter.embedding.engine.FlutterEngine}s are created, resources from an existing living {@link + * io.flutter.embedding.engine.FlutterEngine} is re-used. + * + *

Deleting a FlutterEngineGroup doesn't invalidate its existing {@link + * io.flutter.embedding.engine.FlutterEngine}s, but it eliminates the possibility to create more + * {@link io.flutter.embedding.engine.FlutterEngine}s in that group. + */ +public class FlutterEngineGroup { + + /* package */ @VisibleForTesting final List activeEngines = new ArrayList<>(); + + /** + * Creates a {@link io.flutter.embedding.engine.FlutterEngine} in this group and run its {@link + * io.flutter.embedding.engine.dart.DartExecutor} with a default entrypoint of the "main" function + * in the "lib/main.dart" file. + * + *

If no prior {@link io.flutter.embedding.engine.FlutterEngine} were created in this group, + * the initialization cost will be slightly higher than subsequent engines. The very first {@link + * io.flutter.embedding.engine.FlutterEngine} created per program, regardless of + * FlutterEngineGroup, also incurs the Dart VM creation time. + * + *

Subsequent engine creations will share resources with existing engines. However, if all + * existing engines were {@link io.flutter.embedding.engine.FlutterEngine#destroy()}ed, the next + * engine created will recreate its dependencies. + */ + public FlutterEngine createAndRunDefaultEngine(@NonNull Context context) { + return createAndRunEngine(context, null); + } + + /** + * Creates a {@link io.flutter.embedding.engine.FlutterEngine} in this group and run its {@link + * io.flutter.embedding.engine.dart.DartExecutor} with the specified {@link DartEntrypoint}. + * + *

If no prior {@link io.flutter.embedding.engine.FlutterEngine} were created in this group, + * the initialization cost will be slightly higher than subsequent engines. The very first {@link + * io.flutter.embedding.engine.FlutterEngine} created per program, regardless of + * FlutterEngineGroup, also incurs the Dart VM creation time. + * + *

Subsequent engine creations will share resources with existing engines. However, if all + * existing engines were {@link io.flutter.embedding.engine.FlutterEngine#destroy()}ed, the next + * engine created will recreate its dependencies. + */ + public FlutterEngine createAndRunEngine( + @NonNull Context context, @Nullable DartEntrypoint dartEntrypoint) { + FlutterEngine engine = null; + // This is done up here because an engine needs to be created first in order to be able to use + // DartEntrypoint.createDefault. The engine creation initializes the FlutterLoader so + // DartEntrypoint known where to find the assets for the AOT or kernel code. + if (activeEngines.size() == 0) { + engine = createEngine(context); + } + + if (dartEntrypoint == null) { + dartEntrypoint = DartEntrypoint.createDefault(); + } + + if (activeEngines.size() == 0) { + engine.getDartExecutor().executeDartEntrypoint(dartEntrypoint); + } else { + engine = activeEngines.get(0).spawn(context, dartEntrypoint); + } + + activeEngines.add(engine); + + final FlutterEngine engineToCleanUpOnDestroy = engine; + engine.addEngineLifecycleListener( + new FlutterEngine.EngineLifecycleListener() { + + @Override + public void onPreEngineRestart() { + // No-op. Not interested. + } + + @Override + public void onEngineWillDestroy() { + activeEngines.remove(engineToCleanUpOnDestroy); + } + }); + return engine; + } + + @VisibleForTesting + /* package */ FlutterEngine createEngine(Context context) { + return new FlutterEngine(context); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index e122e2528a936..01797d6768fb7 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -28,6 +28,7 @@ import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.localization.LocalizationPlugin; import io.flutter.plugin.platform.PlatformViewsController; +import io.flutter.util.Preconditions; import io.flutter.view.AccessibilityBridge; import io.flutter.view.FlutterCallbackInformation; import java.nio.ByteBuffer; @@ -104,24 +105,42 @@ public class FlutterJNI { * *

This must be called before any other native methods, and can be overridden by tests to avoid * loading native libraries. + * + *

This method should only be called once across all FlutterJNI instances. */ public void loadLibrary() { + if (FlutterJNI.loadLibraryCalled) { + Log.w(TAG, "FlutterJNI.loadLibrary called more than once"); + } + System.loadLibrary("flutter"); + FlutterJNI.loadLibraryCalled = true; } + private static boolean loadLibraryCalled = false; + /** * Prefetch the default font manager provided by SkFontMgr::RefDefault() which is a process-wide * singleton owned by Skia. Note that, the first call to SkFontMgr::RefDefault() will take * noticeable time, but later calls will return a reference to the preexisting font manager. + * + *

This method should only be called once across all FlutterJNI instances. */ public void prefetchDefaultFontManager() { + if (FlutterJNI.prefetchDefaultFontManagerCalled) { + Log.w(TAG, "FlutterJNI.prefetchDefaultFontManager called more than once"); + } + FlutterJNI.nativePrefetchDefaultFontManager(); + FlutterJNI.prefetchDefaultFontManagerCalled = true; } + private static boolean prefetchDefaultFontManagerCalled = false; + /** * Perform one time initialization of the Dart VM and Flutter engine. * - *

This method must be called only once. + *

This method must be called only once. Calling more than once will cause an exception. * * @param context The application context. * @param args Arguments to the Dart VM/Flutter engine. @@ -137,9 +156,16 @@ public void init( @NonNull String appStoragePath, @NonNull String engineCachesPath, long initTimeMillis) { + if (FlutterJNI.initCalled) { + Log.w(TAG, "FlutterJNI.init called more than once"); + } + FlutterJNI.nativeInit( context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis); + FlutterJNI.initCalled = true; } + + private static boolean initCalled = false; // END methods related to FlutterLoader @Nullable private static AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate; @@ -167,21 +193,37 @@ public static native void nativeInit( private native boolean nativeGetIsSoftwareRenderingEnabled(); @UiThread - // TODO(mattcarroll): add javadocs + /** + * Checks launch settings for whether software rendering is requested. + * + *

The value is the same per program. + */ public boolean getIsSoftwareRenderingEnabled() { return nativeGetIsSoftwareRenderingEnabled(); } @Nullable - // TODO(mattcarroll): add javadocs + /** + * Observatory URI for the VM instance. + * + *

Its value is set by the native engine once {@link #init(Context, String[], String, String, + * String, long)} is run. + */ public static String getObservatoryUri() { return observatoryUri; } public static void setRefreshRateFPS(float refreshRateFPS) { + if (FlutterJNI.setRefreshRateFPSCalled) { + Log.w(TAG, "FlutterJNI.setRefreshRateFPS called more than once"); + } + FlutterJNI.refreshRateFPS = refreshRateFPS; + FlutterJNI.setRefreshRateFPSCalled = true; } + private static boolean setRefreshRateFPSCalled = false; + // TODO(mattcarroll): add javadocs public static void setAsyncWaitForVsyncDelegate(@Nullable AsyncWaitForVsyncDelegate delegate) { asyncWaitForVsyncDelegate = delegate; @@ -220,7 +262,10 @@ public static native void nativeOnVsync( // ----- End Engine FlutterTextUtils Methods ---- - @Nullable private Long nativePlatformViewId; + // Below represents the stateful part of the FlutterJNI instances that aren't static per program. + // Conceptually, it represents a native shell instance. + + @Nullable private Long nativeShellHolderId; @Nullable private AccessibilityDelegate accessibilityDelegate; @Nullable private PlatformMessageHandler platformMessageHandler; @Nullable private LocalizationPlugin localizationPlugin; @@ -249,7 +294,7 @@ public FlutterJNI() { * a Java Native Interface (JNI). */ public boolean isAttached() { - return nativePlatformViewId != null; + return nativeShellHolderId != null; } /** @@ -262,7 +307,7 @@ public boolean isAttached() { public void attachToNative(boolean isBackgroundView) { ensureRunningOnMainThread(); ensureNotAttachedToNative(); - nativePlatformViewId = performNativeAttach(this, isBackgroundView); + nativeShellHolderId = performNativeAttach(this, isBackgroundView); } @VisibleForTesting @@ -272,6 +317,40 @@ public long performNativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgr private native long nativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgroundView); + /** + * Spawns a new FlutterJNI instance from the current instance. + * + *

This creates another native shell from the current shell. This causes the 2 shells to re-use + * some of the shared resources, reducing the total memory consumption versus creating a new + * FlutterJNI by calling its standard constructor. + * + *

This can only be called once the current FlutterJNI instance is attached by calling {@link + * #attachToNative(boolean)}. + * + *

Static methods that should be only called once such as {@link #init(Context, String[], + * String, String, String, long)} or {@link #setRefreshRateFPS(float)} shouldn't be called again + * on the spawned FlutterJNI instance. + */ + @UiThread + @NonNull + public FlutterJNI spawn( + @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction) { + ensureRunningOnMainThread(); + ensureAttachedToNative(); + FlutterJNI spawnedJNI = + nativeSpawn(nativeShellHolderId, entrypointFunctionName, pathToEntrypointFunction); + Preconditions.checkState( + spawnedJNI.nativeShellHolderId != null && spawnedJNI.nativeShellHolderId > 0, + "Failed to spawn new JNI connected shell from existing shell."); + + return spawnedJNI; + } + + private native FlutterJNI nativeSpawn( + long nativeSpawningShellId, + @Nullable String entrypointFunctionName, + @Nullable String pathToEntrypointFunction); + /** * Detaches this {@code FlutterJNI} instance from Flutter's native engine, which precludes any * further communication between Android code and Flutter's platform agnostic engine. @@ -279,7 +358,8 @@ public long performNativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgr *

This method must not be invoked if {@code FlutterJNI} is not already attached to native. * *

Invoking this method will result in the release of all native-side resources that were setup - * during {@link #attachToNative(boolean)}, or accumulated thereafter. + * during {@link #attachToNative(boolean)} or {@link #spawn(String, String)}, or accumulated + * thereafter. * *

It is permissable to re-attach this instance to native after detaching it from native. */ @@ -287,21 +367,21 @@ public long performNativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgr public void detachFromNativeAndReleaseResources() { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeDestroy(nativePlatformViewId); - nativePlatformViewId = null; + nativeDestroy(nativeShellHolderId); + nativeShellHolderId = null; } - private native void nativeDestroy(long nativePlatformViewId); + private native void nativeDestroy(long nativeShellHolderId); private void ensureNotAttachedToNative() { - if (nativePlatformViewId != null) { + if (nativeShellHolderId != null) { throw new RuntimeException( "Cannot execute operation because FlutterJNI is attached to native."); } } private void ensureAttachedToNative() { - if (nativePlatformViewId == null) { + if (nativeShellHolderId == null) { throw new RuntimeException( "Cannot execute operation because FlutterJNI is not attached to native."); } @@ -364,10 +444,10 @@ void onRenderingStopped() { public void onSurfaceCreated(@NonNull Surface surface) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeSurfaceCreated(nativePlatformViewId, surface); + nativeSurfaceCreated(nativeShellHolderId, surface); } - private native void nativeSurfaceCreated(long nativePlatformViewId, @NonNull Surface surface); + private native void nativeSurfaceCreated(long nativeShellHolderId, @NonNull Surface surface); /** * In hybrid composition, call this method when the {@link Surface} has changed. @@ -380,11 +460,11 @@ public void onSurfaceCreated(@NonNull Surface surface) { public void onSurfaceWindowChanged(@NonNull Surface surface) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeSurfaceWindowChanged(nativePlatformViewId, surface); + nativeSurfaceWindowChanged(nativeShellHolderId, surface); } private native void nativeSurfaceWindowChanged( - long nativePlatformViewId, @NonNull Surface surface); + long nativeShellHolderId, @NonNull Surface surface); /** * Call this method when the {@link Surface} changes that was previously registered with {@link @@ -397,10 +477,10 @@ private native void nativeSurfaceWindowChanged( public void onSurfaceChanged(int width, int height) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeSurfaceChanged(nativePlatformViewId, width, height); + nativeSurfaceChanged(nativeShellHolderId, width, height); } - private native void nativeSurfaceChanged(long nativePlatformViewId, int width, int height); + private native void nativeSurfaceChanged(long nativeShellHolderId, int width, int height); /** * Call this method when the {@link Surface} is destroyed that was previously registered with @@ -414,10 +494,10 @@ public void onSurfaceDestroyed() { ensureRunningOnMainThread(); ensureAttachedToNative(); onRenderingStopped(); - nativeSurfaceDestroyed(nativePlatformViewId); + nativeSurfaceDestroyed(nativeShellHolderId); } - private native void nativeSurfaceDestroyed(long nativePlatformViewId); + private native void nativeSurfaceDestroyed(long nativeShellHolderId); /** * Call this method to notify Flutter of the current device viewport metrics that are applies to @@ -446,7 +526,7 @@ public void setViewportMetrics( ensureRunningOnMainThread(); ensureAttachedToNative(); nativeSetViewportMetrics( - nativePlatformViewId, + nativeShellHolderId, devicePixelRatio, physicalWidth, physicalHeight, @@ -465,7 +545,7 @@ public void setViewportMetrics( } private native void nativeSetViewportMetrics( - long nativePlatformViewId, + long nativeShellHolderId, float devicePixelRatio, int physicalWidth, int physicalHeight, @@ -489,11 +569,11 @@ private native void nativeSetViewportMetrics( public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeDispatchPointerDataPacket(nativePlatformViewId, buffer, position); + nativeDispatchPointerDataPacket(nativeShellHolderId, buffer, position); } private native void nativeDispatchPointerDataPacket( - long nativePlatformViewId, @NonNull ByteBuffer buffer, int position); + long nativeShellHolderId, @NonNull ByteBuffer buffer, int position); // ------ End Touch Interaction Support --- @UiThread @@ -590,11 +670,11 @@ public void dispatchSemanticsAction( int id, int action, @Nullable ByteBuffer args, int argsPosition) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeDispatchSemanticsAction(nativePlatformViewId, id, action, args, argsPosition); + nativeDispatchSemanticsAction(nativeShellHolderId, id, action, args, argsPosition); } private native void nativeDispatchSemanticsAction( - long nativePlatformViewId, int id, int action, @Nullable ByteBuffer args, int argsPosition); + long nativeShellHolderId, int id, int action, @Nullable ByteBuffer args, int argsPosition); /** * Instructs Flutter to enable/disable its semantics tree, which is used by Flutter to support @@ -604,10 +684,10 @@ private native void nativeDispatchSemanticsAction( public void setSemanticsEnabled(boolean enabled) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeSetSemanticsEnabled(nativePlatformViewId, enabled); + nativeSetSemanticsEnabled(nativeShellHolderId, enabled); } - private native void nativeSetSemanticsEnabled(long nativePlatformViewId, boolean enabled); + private native void nativeSetSemanticsEnabled(long nativeShellHolderId, boolean enabled); // TODO(mattcarroll): figure out what flags are supported and add javadoc about when/why/where to // use this. @@ -615,10 +695,10 @@ public void setSemanticsEnabled(boolean enabled) { public void setAccessibilityFeatures(int flags) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeSetAccessibilityFeatures(nativePlatformViewId, flags); + nativeSetAccessibilityFeatures(nativeShellHolderId, flags); } - private native void nativeSetAccessibilityFeatures(long nativePlatformViewId, int flags); + private native void nativeSetAccessibilityFeatures(long nativeShellHolderId, int flags); // ------ End Accessibility Support ---- // ------ Start Texture Registration Support ----- @@ -630,11 +710,11 @@ public void setAccessibilityFeatures(int flags) { public void registerTexture(long textureId, @NonNull SurfaceTextureWrapper textureWrapper) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeRegisterTexture(nativePlatformViewId, textureId, textureWrapper); + nativeRegisterTexture(nativeShellHolderId, textureId, textureWrapper); } private native void nativeRegisterTexture( - long nativePlatformViewId, long textureId, @NonNull SurfaceTextureWrapper textureWrapper); + long nativeShellHolderId, long textureId, @NonNull SurfaceTextureWrapper textureWrapper); /** * Call this method to inform Flutter that a texture previously registered with {@link @@ -647,10 +727,10 @@ private native void nativeRegisterTexture( public void markTextureFrameAvailable(long textureId) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeMarkTextureFrameAvailable(nativePlatformViewId, textureId); + nativeMarkTextureFrameAvailable(nativeShellHolderId, textureId); } - private native void nativeMarkTextureFrameAvailable(long nativePlatformViewId, long textureId); + private native void nativeMarkTextureFrameAvailable(long nativeShellHolderId, long textureId); /** * Unregisters a texture that was registered with {@link #registerTexture(long, SurfaceTexture)}. @@ -659,10 +739,10 @@ public void markTextureFrameAvailable(long textureId) { public void unregisterTexture(long textureId) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeUnregisterTexture(nativePlatformViewId, textureId); + nativeUnregisterTexture(nativeShellHolderId, textureId); } - private native void nativeUnregisterTexture(long nativePlatformViewId, long textureId); + private native void nativeUnregisterTexture(long nativeShellHolderId, long textureId); // ------ Start Texture Registration Support ----- // ------ Start Dart Execution Support ------- @@ -681,7 +761,7 @@ public void runBundleAndSnapshotFromLibrary( ensureRunningOnMainThread(); ensureAttachedToNative(); nativeRunBundleAndSnapshotFromLibrary( - nativePlatformViewId, + nativeShellHolderId, bundlePath, entrypointFunctionName, pathToEntrypointFunction, @@ -689,7 +769,7 @@ public void runBundleAndSnapshotFromLibrary( } private native void nativeRunBundleAndSnapshotFromLibrary( - long nativePlatformViewId, + long nativeShellHolderId, @NonNull String bundlePath, @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction, @@ -760,7 +840,7 @@ private void handlePlatformMessageResponse(int replyId, byte[] reply) { public void dispatchEmptyPlatformMessage(@NonNull String channel, int responseId) { ensureRunningOnMainThread(); if (isAttached()) { - nativeDispatchEmptyPlatformMessage(nativePlatformViewId, channel, responseId); + nativeDispatchEmptyPlatformMessage(nativeShellHolderId, channel, responseId); } else { Log.w( TAG, @@ -773,7 +853,7 @@ public void dispatchEmptyPlatformMessage(@NonNull String channel, int responseId // Send an empty platform message to Dart. private native void nativeDispatchEmptyPlatformMessage( - long nativePlatformViewId, @NonNull String channel, int responseId); + long nativeShellHolderId, @NonNull String channel, int responseId); /** Sends a reply {@code message} from Android to Flutter over the given {@code channel}. */ @UiThread @@ -781,7 +861,7 @@ public void dispatchPlatformMessage( @NonNull String channel, @Nullable ByteBuffer message, int position, int responseId) { ensureRunningOnMainThread(); if (isAttached()) { - nativeDispatchPlatformMessage(nativePlatformViewId, channel, message, position, responseId); + nativeDispatchPlatformMessage(nativeShellHolderId, channel, message, position, responseId); } else { Log.w( TAG, @@ -794,7 +874,7 @@ public void dispatchPlatformMessage( // Send a data-carrying platform message to Dart. private native void nativeDispatchPlatformMessage( - long nativePlatformViewId, + long nativeShellHolderId, @NonNull String channel, @Nullable ByteBuffer message, int position, @@ -805,7 +885,7 @@ private native void nativeDispatchPlatformMessage( public void invokePlatformMessageEmptyResponseCallback(int responseId) { ensureRunningOnMainThread(); if (isAttached()) { - nativeInvokePlatformMessageEmptyResponseCallback(nativePlatformViewId, responseId); + nativeInvokePlatformMessageEmptyResponseCallback(nativeShellHolderId, responseId); } else { Log.w( TAG, @@ -816,7 +896,7 @@ public void invokePlatformMessageEmptyResponseCallback(int responseId) { // Send an empty response to a platform message received from Dart. private native void nativeInvokePlatformMessageEmptyResponseCallback( - long nativePlatformViewId, int responseId); + long nativeShellHolderId, int responseId); // TODO(mattcarroll): differentiate between channel responses and platform responses. @UiThread @@ -825,7 +905,7 @@ public void invokePlatformMessageResponseCallback( ensureRunningOnMainThread(); if (isAttached()) { nativeInvokePlatformMessageResponseCallback( - nativePlatformViewId, responseId, message, position); + nativeShellHolderId, responseId, message, position); } else { Log.w( TAG, @@ -836,7 +916,7 @@ public void invokePlatformMessageResponseCallback( // Send a data-carrying response to a platform message received from Dart. private native void nativeInvokePlatformMessageResponseCallback( - long nativePlatformViewId, int responseId, @Nullable ByteBuffer message, int position); + long nativeShellHolderId, int responseId, @Nullable ByteBuffer message, int position); // ------- End Platform Message Support ---- // ----- Start Engine Lifecycle Support ---- @@ -1040,11 +1120,11 @@ public void requestDartDeferredLibrary(int loadingUnitId) { public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeLoadDartDeferredLibrary(nativePlatformViewId, loadingUnitId, searchPaths); + nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, searchPaths); } private native void nativeLoadDartDeferredLibrary( - long nativePlatformViewId, int loadingUnitId, @NonNull String[] searchPaths); + long nativeShellHolderId, int loadingUnitId, @NonNull String[] searchPaths); /** * Adds the specified AssetManager as an APKAssetResolver in the Flutter Engine's AssetManager. @@ -1061,11 +1141,11 @@ public void updateJavaAssetManager( @NonNull AssetManager assetManager, @NonNull String assetBundlePath) { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeUpdateJavaAssetManager(nativePlatformViewId, assetManager, assetBundlePath); + nativeUpdateJavaAssetManager(nativeShellHolderId, assetManager, assetBundlePath); } private native void nativeUpdateJavaAssetManager( - long nativePlatformViewId, + long nativeShellHolderId, @NonNull AssetManager assetManager, @NonNull String assetBundlePath); @@ -1121,11 +1201,11 @@ public void onDisplayPlatformView( public Bitmap getBitmap() { ensureRunningOnMainThread(); ensureAttachedToNative(); - return nativeGetBitmap(nativePlatformViewId); + return nativeGetBitmap(nativeShellHolderId); } // TODO(mattcarroll): determine if this is nonull or nullable - private native Bitmap nativeGetBitmap(long nativePlatformViewId); + private native Bitmap nativeGetBitmap(long nativeShellHolderId); /** * Notifies the Dart VM of a low memory event, or that the application is in a state such that now @@ -1138,10 +1218,10 @@ public Bitmap getBitmap() { public void notifyLowMemoryWarning() { ensureRunningOnMainThread(); ensureAttachedToNative(); - nativeNotifyLowMemoryWarning(nativePlatformViewId); + nativeNotifyLowMemoryWarning(nativeShellHolderId); } - private native void nativeNotifyLowMemoryWarning(long nativePlatformViewId); + private native void nativeNotifyLowMemoryWarning(long nativeShellHolderId); private void ensureRunningOnMainThread() { if (Looper.myLooper() != mainLooper) { diff --git a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java index 3d6cb8416e21d..ccb8c3b7ebcea 100644 --- a/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java +++ b/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java @@ -121,7 +121,10 @@ public void executeDartEntrypoint(@NonNull DartEntrypoint dartEntrypoint) { Log.v(TAG, "Executing Dart entrypoint: " + dartEntrypoint); flutterJNI.runBundleAndSnapshotFromLibrary( - dartEntrypoint.pathToBundle, dartEntrypoint.dartEntrypointFunctionName, null, assetManager); + dartEntrypoint.pathToBundle, + dartEntrypoint.dartEntrypointFunctionName, + dartEntrypoint.dartEntrypointLibrary, + assetManager); isApplicationRunning = true; } @@ -269,12 +272,25 @@ public static DartEntrypoint createDefault() { /** The path within the AssetManager where the app will look for assets. */ @NonNull public final String pathToBundle; + /** The library or file location that contains the Dart entrypoint function. */ + @Nullable public final String dartEntrypointLibrary; + /** The name of a Dart function to execute. */ @NonNull public final String dartEntrypointFunctionName; public DartEntrypoint( @NonNull String pathToBundle, @NonNull String dartEntrypointFunctionName) { this.pathToBundle = pathToBundle; + dartEntrypointLibrary = null; + this.dartEntrypointFunctionName = dartEntrypointFunctionName; + } + + public DartEntrypoint( + @NonNull String pathToBundle, + @NonNull String dartEntrypointLibrary, + @NonNull String dartEntrypointFunctionName) { + this.pathToBundle = pathToBundle; + this.dartEntrypointLibrary = dartEntrypointLibrary; this.dartEntrypointFunctionName = dartEntrypointFunctionName; } diff --git a/shell/platform/android/io/flutter/util/Preconditions.java b/shell/platform/android/io/flutter/util/Preconditions.java index 3a089c1b1ea80..83c58f202fd4d 100644 --- a/shell/platform/android/io/flutter/util/Preconditions.java +++ b/shell/platform/android/io/flutter/util/Preconditions.java @@ -4,6 +4,8 @@ package io.flutter.util; +import androidx.annotation.Nullable; + /** * Static convenience methods that help a method or constructor check whether it was invoked * correctly (that is, whether its preconditions were met). @@ -24,4 +26,30 @@ public static T checkNotNull(T reference) { } return reference; } + + /** + * Ensures the truth of an expression involving the state of the calling instance. + * + * @param expression a boolean expression that must be checked to be true + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState(boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling instance. + * + * @param expression a boolean expression that must be checked to be true + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState(boolean expression, @Nullable Object errorMessage) { + if (!expression) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + } } diff --git a/shell/platform/android/io/flutter/view/FlutterNativeView.java b/shell/platform/android/io/flutter/view/FlutterNativeView.java index 2d5ee8c40ad56..2b7afda518c1f 100644 --- a/shell/platform/android/io/flutter/view/FlutterNativeView.java +++ b/shell/platform/android/io/flutter/view/FlutterNativeView.java @@ -165,5 +165,10 @@ public void onPreEngineRestart() { } mPluginRegistry.onPreEngineRestart(); } + + public void onEngineWillDestroy() { + // The old embedding doesn't actually have a FlutterEngine. It interacts with the JNI + // directly. + } } } diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 68b2b8fdb1a3f..b580b4049bc05 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -63,27 +63,32 @@ PlatformViewAndroid::PlatformViewAndroid( platform_view_android_delegate_(jni_facade) { if (use_software_rendering) { android_context_ = - std::make_unique(AndroidRenderingAPI::kSoftware); + std::make_shared(AndroidRenderingAPI::kSoftware); } else { #if SHELL_ENABLE_VULKAN android_context_ = - std::make_unique(AndroidRenderingAPI::kVulkan); -#else // SHELL_ENABLE_VULKAN + std::make_shared(AndroidRenderingAPI::kVulkan); +#else // SHELL_ENABLE_VULKAN android_context_ = std::make_unique( AndroidRenderingAPI::kOpenGLES, fml::MakeRefCounted()); #endif // SHELL_ENABLE_VULKAN } - FML_CHECK(android_context_ && android_context_->IsValid()) - << "Could not create an Android context."; - - surface_factory_ = std::make_shared( - *android_context_, jni_facade); + surface_factory_ = MakeSurfaceFactory(*android_context_, *jni_facade_); + android_surface_ = MakeSurface(surface_factory_); +} - android_surface_ = surface_factory_->CreateSurface(); - FML_CHECK(android_surface_ && android_surface_->IsValid()) - << "Could not create an OpenGL, Vulkan or Software surface to setup " - "rendering."; +PlatformViewAndroid::PlatformViewAndroid( + PlatformView::Delegate& delegate, + flutter::TaskRunners task_runners, + const std::shared_ptr& jni_facade, + const std::shared_ptr& android_context) + : PlatformView(delegate, std::move(task_runners)), + jni_facade_(jni_facade), + android_context_(android_context), + platform_view_android_delegate_(jni_facade) { + surface_factory_ = MakeSurfaceFactory(*android_context_, *jni_facade_); + android_surface_ = MakeSurface(surface_factory_); } PlatformViewAndroid::PlatformViewAndroid( @@ -96,6 +101,27 @@ PlatformViewAndroid::PlatformViewAndroid( PlatformViewAndroid::~PlatformViewAndroid() = default; +std::shared_ptr +PlatformViewAndroid::MakeSurfaceFactory( + const AndroidContext& android_context, + const PlatformViewAndroidJNI& jni_facade) { + FML_CHECK(android_context.IsValid()) + << "Could not create surface from invalid Android context."; + + return std::make_shared(android_context, + jni_facade_); +} + +std::unique_ptr PlatformViewAndroid::MakeSurface( + const std::shared_ptr& surface_factory) { + auto surface = surface_factory->CreateSurface(); + + FML_CHECK(surface && surface->IsValid()) + << "Could not create an OpenGL, Vulkan or Software surface to setup " + "rendering."; + return surface; +} + void PlatformViewAndroid::NotifyCreated( fml::RefPtr native_window) { if (android_surface_) { diff --git a/shell/platform/android/platform_view_android.h b/shell/platform/android/platform_view_android.h index 49e3c15c13929..2bdbb2c02a958 100644 --- a/shell/platform/android/platform_view_android.h +++ b/shell/platform/android/platform_view_android.h @@ -53,6 +53,17 @@ class PlatformViewAndroid final : public PlatformView { std::shared_ptr jni_facade, bool use_software_rendering); + //---------------------------------------------------------------------------- + /// @brief Creates a new PlatformViewAndroid but using an existing + /// Android GPU context to create new surfaces. This maximizes + /// resource sharing between 2 PlatformViewAndroids of 2 Shells. + /// + PlatformViewAndroid( + PlatformView::Delegate& delegate, + flutter::TaskRunners task_runners, + const std::shared_ptr& jni_facade, + const std::shared_ptr& android_context); + ~PlatformViewAndroid() override; void NotifyCreated(fml::RefPtr native_window); @@ -108,9 +119,13 @@ class PlatformViewAndroid final : public PlatformView { std::unique_ptr updated_asset_resolver, AssetResolver::AssetResolverType type) override; + const std::shared_ptr& GetAndroidContext() { + return android_context_; + } + private: const std::shared_ptr jni_facade_; - std::unique_ptr android_context_; + std::shared_ptr android_context_; std::shared_ptr surface_factory_; PlatformViewAndroidDelegate platform_view_android_delegate_; @@ -155,6 +170,13 @@ class PlatformViewAndroid final : public PlatformView { // |PlatformView| void RequestDartDeferredLibrary(intptr_t loading_unit_id) override; + std::shared_ptr MakeSurfaceFactory( + const AndroidContext& android_context, + const PlatformViewAndroidJNI& jni_facade); + + std::unique_ptr MakeSurface( + const std::shared_ptr& surface_factory); + void InstallFirstFrameCallback(); void FireFirstFrameCallback(); diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 1b1efa0d2efe9..54fd5e1ec21f4 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -59,6 +60,8 @@ static fml::jni::ScopedJavaGlobalRef* g_flutter_jni_class = nullptr; static fml::jni::ScopedJavaGlobalRef* g_texture_wrapper_class = nullptr; +static fml::jni::ScopedJavaGlobalRef* g_java_long_class = nullptr; + // Called By Native static jmethodID g_flutter_callback_info_constructor = nullptr; @@ -74,6 +77,12 @@ jobject CreateFlutterCallbackInformation( env->NewStringUTF(callbackLibraryPath.c_str())); } +static jfieldID g_jni_shell_holder_field = nullptr; + +static jmethodID g_jni_constructor = nullptr; + +static jmethodID g_long_constructor = nullptr; + static jmethodID g_handle_platform_message_method = nullptr; static jmethodID g_handle_platform_message_response_method = nullptr; @@ -145,6 +154,55 @@ static void DestroyJNI(JNIEnv* env, jobject jcaller, jlong shell_holder) { delete ANDROID_SHELL_HOLDER; } +// Signature is similar to RunBundleAndSnapshotFromLibrary but it can't change +// the bundle path or asset manager since we can only spawn with the same +// AOT. +// +// The shell_holder instance must be a pointer address to the current +// AndroidShellHolder whose Shell will be used to spawn a new Shell. +// +// This creates a Java Long that points to the newly created +// AndroidShellHolder's raw pointer, connects that Long to a newly created +// FlutterJNI instance, then returns the FlutterJNI instance. +static jobject SpawnJNI(JNIEnv* env, + jobject jcaller, + jlong shell_holder, + jstring jEntrypoint, + jstring jLibraryUrl) { + jobject jni = env->NewObject(g_flutter_jni_class->obj(), g_jni_constructor); + if (jni == nullptr) { + FML_LOG(ERROR) << "Could not create a FlutterJNI instance"; + return nullptr; + } + + fml::jni::JavaObjectWeakGlobalRef java_jni(env, jni); + std::shared_ptr jni_facade = + std::make_shared(java_jni); + + auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint); + auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl); + + auto spawned_shell_holder = + ANDROID_SHELL_HOLDER->Spawn(jni_facade, entrypoint, libraryUrl); + + if (spawned_shell_holder == nullptr || !spawned_shell_holder->IsValid()) { + FML_LOG(ERROR) << "Could not spawn Shell"; + return nullptr; + } + + jobject javaLong = env->CallStaticObjectMethod( + g_java_long_class->obj(), g_long_constructor, + reinterpret_cast(spawned_shell_holder.release())); + if (javaLong == nullptr) { + FML_LOG(ERROR) << "Could not create a Long instance"; + return nullptr; + } + + env->SetObjectField(jni, g_jni_shell_holder_field, javaLong); + + return jni; +} + static void SurfaceCreated(JNIEnv* env, jobject jcaller, jlong shell_holder, @@ -200,37 +258,10 @@ static void RunBundleAndSnapshotFromLibrary(JNIEnv* env, fml::jni::JavaStringToString(env, jBundlePath)) // apk asset dir ); - std::unique_ptr isolate_configuration; - if (flutter::DartVM::IsRunningPrecompiledCode()) { - isolate_configuration = IsolateConfiguration::CreateForAppSnapshot(); - } else { - std::unique_ptr kernel_blob = - fml::FileMapping::CreateReadOnly( - ANDROID_SHELL_HOLDER->GetSettings().application_kernel_asset); - if (!kernel_blob) { - FML_DLOG(ERROR) << "Unable to load the kernel blob asset."; - return; - } - isolate_configuration = - IsolateConfiguration::CreateForKernel(std::move(kernel_blob)); - } - - RunConfiguration config(std::move(isolate_configuration), - std::move(asset_manager)); - - { - auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint); - auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl); - - if ((entrypoint.size() > 0) && (libraryUrl.size() > 0)) { - config.SetEntrypointAndLibrary(std::move(entrypoint), - std::move(libraryUrl)); - } else if (entrypoint.size() > 0) { - config.SetEntrypoint(std::move(entrypoint)); - } - } + auto entrypoint = fml::jni::JavaStringToString(env, jEntrypoint); + auto libraryUrl = fml::jni::JavaStringToString(env, jLibraryUrl); - ANDROID_SHELL_HOLDER->Launch(std::move(config)); + ANDROID_SHELL_HOLDER->Launch(asset_manager, entrypoint, libraryUrl); } static jobject LookupCallbackInformation(JNIEnv* env, @@ -599,6 +630,12 @@ bool RegisterApi(JNIEnv* env) { .signature = "(J)V", .fnPtr = reinterpret_cast(&DestroyJNI), }, + { + .name = "nativeSpawn", + .signature = "(JLjava/lang/String;Ljava/lang/String;)Lio/flutter/" + "embedding/engine/FlutterJNI;", + .fnPtr = reinterpret_cast(&SpawnJNI), + }, { .name = "nativeRunBundleAndSnapshotFromLibrary", .signature = "(JLjava/lang/String;Ljava/lang/String;" @@ -766,6 +803,29 @@ bool RegisterApi(JNIEnv* env) { return false; } + g_jni_shell_holder_field = env->GetFieldID( + g_flutter_jni_class->obj(), "nativeShellHolderId", "Ljava/lang/Long;"); + + if (g_jni_shell_holder_field == nullptr) { + FML_LOG(ERROR) << "Could not locate FlutterJNI's nativeShellHolderId field"; + return false; + } + + g_jni_constructor = + env->GetMethodID(g_flutter_jni_class->obj(), "", "()V"); + + if (g_jni_constructor == nullptr) { + FML_LOG(ERROR) << "Could not locate FlutterJNI's constructor"; + return false; + } + + g_long_constructor = env->GetStaticMethodID(g_java_long_class->obj(), + "valueOf", "(J)Ljava/lang/Long;"); + if (g_long_constructor == nullptr) { + FML_LOG(ERROR) << "Could not locate Long's constructor"; + return false; + } + g_handle_platform_message_method = env->GetMethodID(g_flutter_jni_class->obj(), "handlePlatformMessage", "(Ljava/lang/String;[BI)V"); @@ -1017,6 +1077,13 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { return false; } + g_java_long_class = new fml::jni::ScopedJavaGlobalRef( + env, env->FindClass("java/lang/Long")); + if (g_java_long_class->is_null()) { + FML_LOG(ERROR) << "Could not locate java.lang.Long class"; + return false; + } + return RegisterApi(env); } diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 5ea424c1413cb..b4adff91c685e 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -13,6 +13,7 @@ import io.flutter.embedding.android.FlutterViewTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; import io.flutter.embedding.engine.FlutterEngineConnectionRegistryTest; +import io.flutter.embedding.engine.FlutterEngineGroupComponentTest; import io.flutter.embedding.engine.FlutterJNITest; import io.flutter.embedding.engine.LocalizationPluginTest; import io.flutter.embedding.engine.RenderingComponentTest; @@ -59,6 +60,7 @@ FlutterAndroidComponentTest.class, FlutterEngineCacheTest.class, FlutterEngineConnectionRegistryTest.class, + FlutterEngineGroupComponentTest.class, FlutterEngineTest.class, FlutterFragmentActivityTest.class, FlutterFragmentTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java new file mode 100644 index 0000000000000..8e709d86c43c1 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.plugins.GeneratedPluginRegistrant; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +// It's a component test because it tests both FlutterEngineGroup and FlutterEngine. +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterEngineGroupComponentTest { + @Mock FlutterJNI flutterJNI; + FlutterEngineGroup engineGroupUnderTest; + FlutterEngine firstEngineUnderTest; + boolean jniAttached; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + jniAttached = false; + when(flutterJNI.isAttached()).thenAnswer(invocation -> jniAttached); + doAnswer(invocation -> jniAttached = true).when(flutterJNI).attachToNative(false); + GeneratedPluginRegistrant.clearRegisteredEngines(); + + firstEngineUnderTest = + spy( + new FlutterEngine( + RuntimeEnvironment.application, + mock(FlutterLoader.class), + flutterJNI, + /*dartVmArgs=*/ new String[] {}, + /*automaticallyRegisterPlugins=*/ false)); + when(firstEngineUnderTest.getDartExecutor()).thenReturn(mock(DartExecutor.class)); + engineGroupUnderTest = + new FlutterEngineGroup() { + @Override + FlutterEngine createEngine(Context context) { + return firstEngineUnderTest; + } + }; + } + + @After + public void tearDown() { + GeneratedPluginRegistrant.clearRegisteredEngines(); + engineGroupUnderTest = null; + firstEngineUnderTest = null; + } + + @Test + public void listensToEngineDestruction() { + FlutterEngine firstEngine = + engineGroupUnderTest.createAndRunEngine( + RuntimeEnvironment.application, mock(DartEntrypoint.class)); + assertEquals(1, engineGroupUnderTest.activeEngines.size()); + + firstEngine.destroy(); + assertEquals(0, engineGroupUnderTest.activeEngines.size()); + } + + @Test + public void canRecreateEngines() { + FlutterEngine firstEngine = + engineGroupUnderTest.createAndRunEngine( + RuntimeEnvironment.application, mock(DartEntrypoint.class)); + assertEquals(1, engineGroupUnderTest.activeEngines.size()); + + firstEngine.destroy(); + assertEquals(0, engineGroupUnderTest.activeEngines.size()); + + FlutterEngine secondEngine = + engineGroupUnderTest.createAndRunEngine( + RuntimeEnvironment.application, mock(DartEntrypoint.class)); + assertEquals(1, engineGroupUnderTest.activeEngines.size()); + // They happen to be equal in our test since we mocked it to be so. + assertEquals(firstEngine, secondEngine); + } + + @Test + public void canSpawnMoreEngines() { + FlutterEngine firstEngine = + engineGroupUnderTest.createAndRunEngine( + RuntimeEnvironment.application, mock(DartEntrypoint.class)); + assertEquals(1, engineGroupUnderTest.activeEngines.size()); + + doReturn(mock(FlutterEngine.class)) + .when(firstEngine) + .spawn(any(Context.class), any(DartEntrypoint.class)); + + FlutterEngine secondEngine = + engineGroupUnderTest.createAndRunEngine( + RuntimeEnvironment.application, mock(DartEntrypoint.class)); + assertEquals(2, engineGroupUnderTest.activeEngines.size()); + + firstEngine.destroy(); + assertEquals(1, engineGroupUnderTest.activeEngines.size()); + + // Now the second spawned engine is the only one left and it will be called to spawn the next + // engine in the chain. + when(secondEngine.spawn(any(Context.class), any(DartEntrypoint.class))) + .thenReturn(mock(FlutterEngine.class)); + + FlutterEngine thirdEngine = + engineGroupUnderTest.createAndRunEngine( + RuntimeEnvironment.application, mock(DartEntrypoint.class)); + assertEquals(2, engineGroupUnderTest.activeEngines.size()); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java index be9057d97ed8e..d57ab939048b2 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java @@ -6,7 +6,9 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -15,6 +17,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import io.flutter.FlutterInjector; import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngine.EngineLifecycleListener; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.plugin.platform.PlatformViewsController; @@ -27,6 +30,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -35,11 +40,23 @@ @RunWith(RobolectricTestRunner.class) public class FlutterEngineTest { @Mock FlutterJNI flutterJNI; + boolean jniAttached; @Before public void setUp() { MockitoAnnotations.initMocks(this); - when(flutterJNI.isAttached()).thenReturn(true); + jniAttached = false; + when(flutterJNI.isAttached()).thenAnswer(invocation -> jniAttached); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + jniAttached = true; + return null; + } + }) + .when(flutterJNI) + .attachToNative(false); GeneratedPluginRegistrant.clearRegisteredEngines(); } @@ -108,9 +125,6 @@ public void itNotifiesPlatformViewsControllerWhenDevHotRestart() { @Test public void itNotifiesPlatformViewsControllerAboutJNILifecycle() { - FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); - when(mockFlutterJNI.isAttached()).thenReturn(true); - PlatformViewsController platformViewsController = mock(PlatformViewsController.class); // Execute behavior under test. @@ -118,7 +132,7 @@ public void itNotifiesPlatformViewsControllerAboutJNILifecycle() { new FlutterEngine( RuntimeEnvironment.application, mock(FlutterLoader.class), - mockFlutterJNI, + flutterJNI, platformViewsController, /*dartVmArgs=*/ new String[] {}, /*automaticallyRegisterPlugins=*/ false); @@ -178,4 +192,44 @@ public void itCanUseFlutterLoaderInjectionViaFlutterInjector() throws NameNotFou verify(mockFlutterLoader, times(1)).startInitialization(any()); verify(mockFlutterLoader, times(1)).ensureInitializationComplete(any(), any()); } + + @Test + public void itNotifiesListenersForDestruction() throws NameNotFoundException { + Context context = mock(Context.class); + Context packageContext = mock(Context.class); + + when(context.createPackageContext(any(), anyInt())).thenReturn(packageContext); + + FlutterEngine engineUnderTest = + new FlutterEngine( + context, + mock(FlutterLoader.class), + flutterJNI, + /*dartVmArgs=*/ new String[] {}, + /*automaticallyRegisterPlugins=*/ false); + + EngineLifecycleListener listener = mock(EngineLifecycleListener.class); + engineUnderTest.addEngineLifecycleListener(listener); + engineUnderTest.destroy(); + verify(listener, times(1)).onEngineWillDestroy(); + } + + @Test + public void itDoesNotAttachAgainWhenBuiltWithAnAttachedJNI() throws NameNotFoundException { + Context context = mock(Context.class); + Context packageContext = mock(Context.class); + + when(context.createPackageContext(any(), anyInt())).thenReturn(packageContext); + when(flutterJNI.isAttached()).thenReturn(true); + + FlutterEngine engineUnderTest = + new FlutterEngine( + context, + mock(FlutterLoader.class), + flutterJNI, + /*dartVmArgs=*/ new String[] {}, + /*automaticallyRegisterPlugins=*/ false); + + verify(flutterJNI, never()).attachToNative(false); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java index 138e36c766f0c..7c2fb5b8069be 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java @@ -5,6 +5,7 @@ package test.io.flutter.embedding.engine; import static junit.framework.TestCase.assertEquals; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -24,6 +25,8 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class PluginComponentTest { + boolean jniAttached; + @Before public void setUp() { FlutterInjector.reset(); @@ -36,7 +39,9 @@ public void pluginsCanAccessFlutterAssetPaths() { FlutterInjector.setInstance( new FlutterInjector.Builder().setFlutterLoader(new FlutterLoader(mockFlutterJNI)).build()); FlutterJNI flutterJNI = mock(FlutterJNI.class); - when(flutterJNI.isAttached()).thenReturn(true); + jniAttached = false; + when(flutterJNI.isAttached()).thenAnswer(invocation -> jniAttached); + doAnswer(invocation -> jniAttached = true).when(flutterJNI).attachToNative(false); FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index ccad2bb7f7582..859f77d405120 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -968,8 +968,8 @@ - (FlutterEngine*)spawnWithEntrypoint:(/*nullable*/ NSString*)entrypoint project:_dartProject.get() allowHeadlessExecution:_allowHeadlessExecution]; - flutter::Settings settings = _shell->GetSettings(); - SetEntryPoint(&settings, entrypoint, libraryURI); + flutter::RunConfiguration configuration = + [_dartProject.get() runConfigurationForEntrypoint:entrypoint libraryOrNil:libraryURI]; fml::WeakPtr platform_view = _shell->GetPlatformView(); FML_DCHECK(platform_view); @@ -992,7 +992,7 @@ - (FlutterEngine*)spawnWithEntrypoint:(/*nullable*/ NSString*)entrypoint [](flutter::Shell& shell) { return std::make_unique(shell); }; std::unique_ptr shell = - _shell->Spawn(std::move(settings), on_create_platform_view, on_create_rasterizer); + _shell->Spawn(std::move(configuration), on_create_platform_view, on_create_rasterizer); result->_threadHost = _threadHost; result->_profiler = _profiler; diff --git a/shell/platform/darwin/ios/platform_view_ios.h b/shell/platform/darwin/ios/platform_view_ios.h index 46cb3e027c448..2425fea5bae9a 100644 --- a/shell/platform/darwin/ios/platform_view_ios.h +++ b/shell/platform/darwin/ios/platform_view_ios.h @@ -93,7 +93,7 @@ class PlatformViewIOS final : public PlatformView { // |PlatformView| void SetSemanticsEnabled(bool enabled) override; - /** Acessor for the `IOSContext` associated with the platform view. */ + /** Accessor for the `IOSContext` associated with the platform view. */ const std::shared_ptr& GetIosContext() { return ios_context_; } private: diff --git a/testing/scenario_app/android/.gitignore b/testing/scenario_app/android/.gitignore index b32f6f298eae0..88b214e75574c 100644 --- a/testing/scenario_app/android/.gitignore +++ b/testing/scenario_app/android/.gitignore @@ -12,3 +12,4 @@ /build /captures .externalNativeBuild +.cache diff --git a/testing/scenario_app/android/android/gradle-home/.cache/daemon/5.6.4/registry.bin b/testing/scenario_app/android/android/gradle-home/.cache/daemon/5.6.4/registry.bin deleted file mode 100644 index 0f3905597f5bd..0000000000000 Binary files a/testing/scenario_app/android/android/gradle-home/.cache/daemon/5.6.4/registry.bin and /dev/null differ diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/SpawnEngineTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/SpawnEngineTests.java new file mode 100644 index 0000000000000..96f91337a03fb --- /dev/null +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/SpawnEngineTests.java @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.scenariosui; + +import android.content.Intent; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; +import dev.flutter.scenarios.SpawnedEngineActivity; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SpawnEngineTests { + Intent intent; + + @Rule + public ActivityTestRule activityRule = + new ActivityTestRule<>( + SpawnedEngineActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + + @Before + public void setUp() { + intent = new Intent(Intent.ACTION_MAIN); + } + + @Test + public void testSpawnedEngine() throws Exception { + intent.putExtra("scenario", "spawn_engine_works"); + ScreenshotUtil.capture(activityRule.launchActivity(intent)); + } +} diff --git a/testing/scenario_app/android/app/src/main/AndroidManifest.xml b/testing/scenario_app/android/app/src/main/AndroidManifest.xml index d5a088de8c355..642450cd3bc27 100644 --- a/testing/scenario_app/android/app/src/main/AndroidManifest.xml +++ b/testing/scenario_app/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,23 @@ + + + + + + + + + + + notifyFlutterRendered()); + + return secondEngine; + } +} diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java new file mode 100644 index 0000000000000..ce0a89e5c39d1 --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java @@ -0,0 +1,152 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.scenarios; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import io.flutter.Log; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryCodec; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodChannel; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class TestActivity extends TestableFlutterActivity { + static final String TAG = "Scenarios"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent launchIntent = getIntent(); + if ("com.google.intent.action.TEST_LOOP".equals(launchIntent.getAction())) { + if (Build.VERSION.SDK_INT > 22) { + requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + } + // Run for one minute, get the timeline data, write it, and finish. + final Uri logFileUri = launchIntent.getData(); + new Handler() + .postDelayed( + new Runnable() { + @Override + public void run() { + writeTimelineData(logFileUri); + + testFlutterLoaderCallbackWhenInitializedTwice(); + } + }, + 20000); + } else { + testFlutterLoaderCallbackWhenInitializedTwice(); + } + } + + @Override + @NonNull + public FlutterShellArgs getFlutterShellArgs() { + FlutterShellArgs args = FlutterShellArgs.fromIntent(getIntent()); + args.add(FlutterShellArgs.ARG_TRACE_STARTUP); + args.add(FlutterShellArgs.ARG_ENABLE_DART_PROFILING); + args.add(FlutterShellArgs.ARG_VERBOSE_LOGGING); + return args; + } + + @Override + public void onFlutterUiDisplayed() { + final Intent launchIntent = getIntent(); + if (!launchIntent.hasExtra("scenario")) { + return; + } + MethodChannel channel = + new MethodChannel(getFlutterEngine().getDartExecutor(), "driver", JSONMethodCodec.INSTANCE); + Map test = new HashMap<>(2); + test.put("name", launchIntent.getStringExtra("scenario")); + test.put("use_android_view", launchIntent.getBooleanExtra("use_android_view", false)); + channel.invokeMethod("set_scenario", test); + } + + private void writeTimelineData(Uri logFile) { + if (logFile == null) { + throw new IllegalArgumentException(); + } + if (getFlutterEngine() == null) { + Log.e(TAG, "Could not write timeline data - no engine."); + return; + } + final BasicMessageChannel channel = + new BasicMessageChannel<>( + getFlutterEngine().getDartExecutor(), "write_timeline", BinaryCodec.INSTANCE); + channel.send( + null, + (ByteBuffer reply) -> { + try { + final FileDescriptor fd = + getContentResolver().openAssetFileDescriptor(logFile, "w").getFileDescriptor(); + final FileOutputStream outputStream = new FileOutputStream(fd); + outputStream.write(reply.array()); + outputStream.close(); + } catch (IOException ex) { + Log.e(TAG, "Could not write timeline file: " + ex.toString()); + } + finish(); + }); + } + + /** + * This method verifies that {@link FlutterLoader#ensureInitializationCompleteAsync(Context, + * String[], Handler, Runnable)} invokes its callback when called after initialization. + */ + private void testFlutterLoaderCallbackWhenInitializedTwice() { + FlutterLoader flutterLoader = new FlutterLoader(); + + // Flutter is probably already loaded in this app based on + // code that ran before this method. Nonetheless, invoke the + // blocking initialization here to ensure it's initialized. + flutterLoader.startInitialization(getApplicationContext()); + flutterLoader.ensureInitializationComplete(getApplication(), new String[] {}); + + // Now that Flutter is loaded, invoke ensureInitializationCompleteAsync with + // a callback and verify that the callback is invoked. + Handler mainHandler = new Handler(Looper.getMainLooper()); + + final AtomicBoolean didInvokeCallback = new AtomicBoolean(false); + + flutterLoader.ensureInitializationCompleteAsync( + getApplication(), + new String[] {}, + mainHandler, + new Runnable() { + @Override + public void run() { + didInvokeCallback.set(true); + } + }); + + mainHandler.post( + new Runnable() { + @Override + public void run() { + if (!didInvokeCallback.get()) { + throw new RuntimeException( + "Failed test: FlutterLoader#ensureInitializationCompleteAsync() did not invoke its callback."); + } + } + }); + } +} diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java index 1fd0609df3bc1..abd3d19ff3f86 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java @@ -4,70 +4,11 @@ package dev.flutter.scenarios; -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; -import io.flutter.Log; import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.FlutterShellArgs; -import io.flutter.embedding.engine.loader.FlutterLoader; -import io.flutter.plugin.common.BasicMessageChannel; -import io.flutter.plugin.common.BinaryCodec; -import io.flutter.plugin.common.JSONMethodCodec; -import io.flutter.plugin.common.MethodChannel; -import java.io.FileDescriptor; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -public class TextPlatformViewActivity extends TestableFlutterActivity { +public class TextPlatformViewActivity extends TestActivity { static final String TAG = "Scenarios"; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Intent launchIntent = getIntent(); - if ("com.google.intent.action.TEST_LOOP".equals(launchIntent.getAction())) { - if (Build.VERSION.SDK_INT > 22) { - requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); - } - // Run for one minute, get the timeline data, write it, and finish. - final Uri logFileUri = launchIntent.getData(); - new Handler() - .postDelayed( - new Runnable() { - @Override - public void run() { - writeTimelineData(logFileUri); - - testFlutterLoaderCallbackWhenInitializedTwice(); - } - }, - 20000); - } else { - testFlutterLoaderCallbackWhenInitializedTwice(); - } - } - - @Override - @NonNull - public FlutterShellArgs getFlutterShellArgs() { - FlutterShellArgs args = FlutterShellArgs.fromIntent(getIntent()); - args.add(FlutterShellArgs.ARG_TRACE_STARTUP); - args.add(FlutterShellArgs.ARG_ENABLE_DART_PROFILING); - args.add(FlutterShellArgs.ARG_VERBOSE_LOGGING); - return args; - } - @Override public void configureFlutterEngine(FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); @@ -76,87 +17,4 @@ public void configureFlutterEngine(FlutterEngine flutterEngine) { .getRegistry() .registerViewFactory("scenarios/textPlatformView", new TextPlatformViewFactory()); } - - @Override - public void onFlutterUiDisplayed() { - final Intent launchIntent = getIntent(); - if (!launchIntent.hasExtra("scenario")) { - return; - } - MethodChannel channel = - new MethodChannel(getFlutterEngine().getDartExecutor(), "driver", JSONMethodCodec.INSTANCE); - Map test = new HashMap<>(2); - test.put("name", launchIntent.getStringExtra("scenario")); - test.put("use_android_view", launchIntent.getBooleanExtra("use_android_view", false)); - channel.invokeMethod("set_scenario", test); - } - - private void writeTimelineData(Uri logFile) { - if (logFile == null) { - throw new IllegalArgumentException(); - } - if (getFlutterEngine() == null) { - Log.e(TAG, "Could not write timeline data - no engine."); - return; - } - final BasicMessageChannel channel = - new BasicMessageChannel<>( - getFlutterEngine().getDartExecutor(), "write_timeline", BinaryCodec.INSTANCE); - channel.send( - null, - (ByteBuffer reply) -> { - try { - final FileDescriptor fd = - getContentResolver().openAssetFileDescriptor(logFile, "w").getFileDescriptor(); - final FileOutputStream outputStream = new FileOutputStream(fd); - outputStream.write(reply.array()); - outputStream.close(); - } catch (IOException ex) { - Log.e(TAG, "Could not write timeline file: " + ex.toString()); - } - finish(); - }); - } - - /** - * This method verifies that {@link FlutterLoader#ensureInitializationCompleteAsync(Context, - * String[], Handler, Runnable)} invokes its callback when called after initialization. - */ - private void testFlutterLoaderCallbackWhenInitializedTwice() { - FlutterLoader flutterLoader = new FlutterLoader(); - - // Flutter is probably already loaded in this app based on - // code that ran before this method. Nonetheless, invoke the - // blocking initialization here to ensure it's initialized. - flutterLoader.startInitialization(getApplicationContext()); - flutterLoader.ensureInitializationComplete(getApplication(), new String[] {}); - - // Now that Flutter is loaded, invoke ensureInitializationCompleteAsync with - // a callback and verify that the callback is invoked. - Handler mainHandler = new Handler(Looper.getMainLooper()); - - final AtomicBoolean didInvokeCallback = new AtomicBoolean(false); - - flutterLoader.ensureInitializationCompleteAsync( - getApplication(), - new String[] {}, - mainHandler, - new Runnable() { - @Override - public void run() { - didInvokeCallback.set(true); - } - }); - - mainHandler.post( - new Runnable() { - @Override - public void run() { - if (!didInvokeCallback.get()) { - throw new RuntimeException( - "Failed test: FlutterLoader#ensureInitializationCompleteAsync() did not invoke its callback."); - } - } - }); - } } diff --git a/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.SpawnEngineTests__testSpawnedEngine.png b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.SpawnEngineTests__testSpawnedEngine.png new file mode 100644 index 0000000000000..8771e1487403f Binary files /dev/null and b/testing/scenario_app/android/reports/screenshots/dev.flutter.scenariosui.SpawnEngineTests__testSpawnedEngine.png differ diff --git a/testing/scenario_app/lib/src/bogus_font_text.dart b/testing/scenario_app/lib/src/bogus_font_text.dart index e3338234c665d..bbde7cda99ed1 100644 --- a/testing/scenario_app/lib/src/bogus_font_text.dart +++ b/testing/scenario_app/lib/src/bogus_font_text.dart @@ -56,9 +56,4 @@ class BogusFontText extends Scenario { }, ); } - - @override - void onDrawFrame() { - // Just draw once since the content never changes. - } } diff --git a/tools/android_lint/project.xml b/tools/android_lint/project.xml index f557fb58e130c..29b512015262b 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -25,6 +25,9 @@ + + + @@ -36,7 +39,6 @@ -