-
Notifications
You must be signed in to change notification settings - Fork 6k
FlutterEngineGroup for Android #23675
Conversation
956f086 to
a207e91
Compare
xster
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some read-along narrative. Hopefully helps with review a bit.
|
|
||
| std::unique_ptr<Shell> Shell::Spawn( | ||
| Settings settings, | ||
| RunConfiguration run_configuration, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was changed from Settings to RunConfiguration since the spawning mechanism can't consume most of the Settings data anyway (since we're spawing off of a previous Shell which was already built using existing Settings data).
| ASSERT_TRUE(configuration.IsValid()); | ||
| configuration.SetEntrypoint("fixturesAreFunctionalMain"); | ||
|
|
||
| auto second_configuration = RunConfiguration::InferFromSettings(settings); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tweaked the test slightly to make sure it can get 2 different entrypoints.
| static size_t thread_host_count = 1; | ||
| auto thread_label = std::to_string(thread_host_count++); | ||
|
|
||
| thread_host_ = std::make_shared<ThreadHost>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also shared Threads in this PR
| shell, // delegate | ||
| shell.GetTaskRunners(), // task runners | ||
| jni_facade, // JNI interop | ||
| android_context // Android context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also shared the GPU context in this PR
| shell_->NotifyLowMemoryWarning(); | ||
| } | ||
|
|
||
| std::optional<RunConfiguration> AndroidShellHolder::BuildRunConfiguration( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved this from the jni_impl to here since it's a lot of business logic and it would be nice to keep the jni as a simple pipe
|
|
||
| // ----- End Engine FlutterTextUtils Methods ---- | ||
|
|
||
| @Nullable private Long nativePlatformViewId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This thing was a misnomer. The receiving side file is named platform view but is really a Shell (which owns the platform view). Renamed to reduce confusion.
| @Nullable String entrypointFunctionName, @Nullable String pathToEntrypointFunction) { | ||
| ensureRunningOnMainThread(); | ||
| ensureAttachedToNative(); | ||
| FlutterJNI spawnedJNI = new FlutterJNI(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This builds everything backwards. Instead of creating a partially built wrapper and filling in a corresponding native instance in there, this builds the native instance first, then builds everything in Java around it.
| Log.v(TAG, "Executing Dart entrypoint: " + dartEntrypoint); | ||
|
|
||
| flutterJNI.runBundleAndSnapshotFromLibrary( | ||
| dartEntrypoint.pathToBundle, dartEntrypoint.dartEntrypointFunctionName, null, assetManager); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The null here was just a feature mysteriously and annoyingly missing on Android for some reason flutter/flutter#72630.
| fml::jni::JavaStringToString(env, jBundlePath)) // apk asset dir | ||
| ); | ||
|
|
||
| std::unique_ptr<IsolateConfiguration> isolate_configuration; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this has to happen in 2 places now, so I moved it into the android shell holder as explained above.
| threadHostType}; | ||
| } | ||
|
|
||
| static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSString* libraryURI) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this new indirection since we're no longer filling in the Settings in the Spawn to partially consume it anymore as explained above.
gaaclarke
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow monster review, awesome progress. I glossed over the golden image test because my brain was starting to sizzle.
|
|
||
| // Pull out the new PlatformViewAndroid from the new Shell to feed to it to | ||
| // the new AndroidShellHolder. | ||
| fml::WeakPtr<PlatformViewAndroid> weak_platform_view; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's keeping this alive that makes a weak pointer the right choice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shell (which we have a unique_ptr to a few lines down) owns a unique_ptr to a PlatformView which we then read out into this line to feed into the AndroidShellHolder which owns a unique_ptr to the Shell which owns this PlatformView.
Adding some more comments.
| asset_manager_ = asset_manager; | ||
| auto config = BuildRunConfiguration(asset_manager, entrypoint, libraryUrl); | ||
| if (!config) { | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FML_LOG instead of failing silently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does on line 280 which is the only way to end up here.
| // Shell ID into the new FlutterJNI instance. | ||
| spawnedJNI.nativeShellId = | ||
| nativeSpawn(spawnedJNI, nativeShellId, entrypointFunctionName, pathToEntrypointFunction); | ||
| Preconditions.checkState( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name is kind of silly, in the case it's a post-condition =)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya, the name is a bit silly. That's just what Java/Guava calls these things. cs/Preconditions.checkState
shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
Outdated
Show resolved
Hide resolved
| * @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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/checkState/checkIsTrue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto, cs/Preconditions.checkState
| if (spawned_shell_holder->IsValid()) { | ||
| return reinterpret_cast<jlong>(spawned_shell_holder.release()); | ||
| } else { | ||
| return 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be more appropriate to throw a java exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, but it's it should be consistent with the mainline path nativeAttach. It's a pretty big breaking change to make it conform there. Instead, the current pattern is defensive coding on the Java side. Doing that as well here.
shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java
Outdated
Show resolved
Hide resolved
| public FlutterEngine createAndRunEngine(@Nullable DartEntrypoint dartEntrypoint) { | ||
| FlutterEngine engine = null; | ||
| if (activeEngines.size() == 0) { | ||
| engine = createEngine(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for doing this here instead of merging it into the block below that calls executeDartEntrypoint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh ya, a good place to add a comment. The engine needs to be created first to call the FlutterLoader if needed. This lets the default DartEntrypoint creator know how to find the assets.
| @Nullable String[] dartVmArgs, | ||
| boolean automaticallyRegisterPlugins, | ||
| boolean waitForRestorationData) { | ||
| this.context = context; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to hold a reference to the context instead of having callers pass the context to spawn()?
Holding the context here creates a risk that a FlutterEngine may keep the context (such as an Activity) alive longer than intended, resulting in resource leaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya that's a really good point. On the one hand, if we keep it the same:
- Theoretically, the users should pass the application context to make this point moot, but it's basically unenforceable.
- It's very hard to audit all the places this Context this ends up at. And if we create 2 engines that share a whole bunch of things but not the same Context, it's not clear yet if there might be side effects where components built with 2 different Context might interact in unpredictable ways.
On the other hand:
- Risking leaking an Activity after detaching an engine is severe.
Let me look at this again. I'm leaning towards doing it your way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended removing the Context ivar from the engine and the engine group. Thanks for pointing out!
| * <p>This method should only be called once across all FlutterJNI instances. | ||
| */ | ||
| public void loadLibrary() { | ||
| if (loadLibraryCalled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is not necessary here - System.loadLibrary is safe to call twice for the same library.
(These methods are effectively static - IIRC they were declared as instance methods just so they could be mocked in tests)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya, though herein lies the cognitive ambiguity since it's not clear what methods the user is expected to call again on a second JNI and which ones are pointless no-ops or redundant. This doesn't really do anything except add docs and messages to point out that certain "static" functions don't need to be called again on a new JNI.
| @NonNull String engineCachesPath, | ||
| long initTimeMillis) { | ||
| if (initCalled) { | ||
| Log.w(TAG, "FlutterJNI.init called more than once"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, nativeInit is not safe to call more than once. This should throw an exception if called again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed log to exception
shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
Outdated
Show resolved
Hide resolved
| // 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 nativeShellId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd vote for renaming this to nativeShellHolderId.
This is a pointer to an AndroidShellHolder, which in turn holds references to the Shell and the PlatformView.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. The Shell actually holds the real unique_ptr to the PlatformView and the AndroidShellHolder holds the unique_ptr to the Shell, so it's 1 layer of indirection. But I'm all for being as technically exact as possible.
| // AOT. | ||
| static jlong SpawnJNI(JNIEnv* env, | ||
| jobject jcaller, | ||
| jobject flutterJNI, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this to spawnedJNI. The flutterJNI name is a bit confusing because jcaller is also an instance of FlutterJNI.
Alternatively this method could use JNI APIs to construct the spawned FlutterJNI instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm gonna call it newFlutterJNI. It's not technically "spawned" since nothing spawned it from an existing "pool". The Java side just constructed a new one and gave it to C++ for a callback. Adding some code comments too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively this method could use JNI APIs to construct the spawned FlutterJNI instance.
I didn't understand this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently nativeSpawn takes an argument containing an empty FlutterJNI representing the newly spawned engine. nativeSpawn constructs the peer AndroidShellHolder and returns a pointer to it, which the Java side then stores in nativeShellHolderId.
The nativeSpawn C++ implementation could instead construct the new FlutterJNI instance, fill in its nativeShellHolderId, and return it. That might make for a cleaner interface. But it also means more complex JNI code. So given that this is a private interface, it's probably more trouble than it's worth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean do something like https://stackoverflow.com/questions/10130819/use-jni-to-create-populate-and-return-a-java-class-instance right? Takes a bit of gymnastics but it is semantically clearer. Lower prio but it's interesting, I'll try it. The FlutterJNI construction signature is unlikely to change at this point too so we'll probably not expose Java refactors into entangled C++ changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
man that was really verbose, since the Java Long needs to be instantiated separately. But done now.
416326e to
0c734d7
Compare
|
Thanks for your quick thorough review! |
|
I'll try to run all the devicelab tests locally before submitting |
|
Ran all the Android devicelab tests locally. We should be ok. |
This comment has been minimized.
This comment has been minimized.
…y linux can match. Try to pull it from CI
4326bcc to
f402440
Compare
gaaclarke
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I think this is good, LGTM. It's pretty big. I just had a few nits and comments, nothing blocking.
|
|
||
| @override | ||
| void onDrawFrame() { | ||
| // Just draw once since the content never changes. | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this removal intentional? Since it doesn't call the super implementation this empty method might be on purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is intentional (since the super syncs a second frame then signals to take a screenshot). The no-op here was somewhat intentional too (just to write some comments on it). I don't think it'll do anything on iOS. It'll send a 'take_screenshot' channel message that'll be dropped on the floor.
| } | ||
|
|
||
| jfieldID shellHolderId = | ||
| env->GetFieldID(jniClass, "nativeShellHolderId", "Ljava/lang/Long;"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a huge deal, but this would be faster if we cached the FieldIds and MethodIds. Maybe it can be a follow up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aha, good point. Done
| /// | ||
| PlatformViewAndroid(PlatformView::Delegate& delegate, | ||
| flutter::TaskRunners task_runners, | ||
| std::shared_ptr<PlatformViewAndroidJNI> jni_facade, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Should pass these by const reference. The copy constructor for std::shared_ptr is pretty heavy, it includes mutex locking and stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done here and in android_shell_holder
|
|
||
| PlatformViewAndroid::~PlatformViewAndroid() = default; | ||
|
|
||
| void PlatformViewAndroid::InitSurface() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This is kind of a weak abstraction. It doesn't just init the surface and obfuscates its side effects =) This would be slightly better:
surface_factory_ = MakeSurfaceFactory(android_context_);
android_surface_ = MakeSurface(surface_factory_);The benefit being that the side effects are visible at the call site; the dependencies are explicit as input parameters, which also reduces the powers of the abstraction; the abstraction names more closely match what they actually do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just another point, i've often seen people use void(void) methods for initialization and they often end up creating temporal bugs (where one method assumes an instance variable has already been initialized) which can't happen with pure functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sg, done
| MethodChannel channel = | ||
| new MethodChannel(getFlutterEngine().getDartExecutor(), "driver", JSONMethodCodec.INSTANCE); | ||
| Map<String, Object> test = new HashMap<>(2); | ||
| test.put("name", launchIntent.getStringExtra("scenario")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comes through as an integer, rather than a string, and causes the test to fail on firebase unfortunately - although we're not picking up the failure :\
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm - I can see this was just moved from other code - I guess FTL changed or this code just never worked.
Description
The PR creates a second FlutterEngine creation path where resources are shared from the first FlutterEngine to reduce startup and memory cost.
Related Issues
Fixes flutter/flutter#72018
Fixes flutter/flutter#72528
Fixes flutter/flutter#72630
Fixes flutter/flutter#73599
Tests
I added the following tests:
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.Reviewer Checklist
Breaking Change
Did any tests fail when you ran them? Please read handling breaking changes.