-
Notifications
You must be signed in to change notification settings - Fork 6k
Android Background Platform Channels #29147
Android Background Platform Channels #29147
Conversation
222c2d2
to
3b19ead
Compare
This now works, tested manually with: public class AndroidPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
@Override
public void
onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
BinaryMessenger.TaskQueue taskQueue =
flutterPluginBinding.getBinaryMessenger().makeBackgroundTaskQueue();
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(),
"android_plugin", StandardMethodCodec.INSTANCE,
taskQueue);
channel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
} |
@dnfield We have a functioning implementation for Android. Feel free to take an early look. Outstanding work:
|
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 starting with the core engine class - havne't looked super closely at the other stuff just yet.
@@ -858,14 +858,23 @@ public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformM | |||
this.platformMessageHandler = platformMessageHandler; | |||
} | |||
|
|||
private native void nativeCleanupMessageData(long messageData); | |||
|
|||
public void cleanupMessageData(long messageData) { |
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.
Make this a static method
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 we're avoiding that to make it easier to mock this out in testing, right?
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.
Although I also wonder if this shoul dbe on a separate class from FlutterJNI
. That might be deferred to a separate PR, but we'd have to make sure this gets called from the right thread, and the right thread might not be the platform thread, in which case other methods on flutterJNI won't work.
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 just following the established pattern. I know in java it's frowned upon to have static functions. Is there any concern here? This call can happen on any thread, at this point we know we have exclusive control over the message data because of the refactor I did previously moving messages to std::unique_ptr.
shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java
Outdated
Show resolved
Hide resolved
shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java
Show resolved
Hide resolved
Thanks for taking an early look, I'll look through everything closer this afternoon. I was OOO and still have a weird schedule because my family is sick. I know there is some work to do to make this threadsafe.
I have a few outstanding issues I have to circle back to then come back to this. |
// detach FlutterJNI on the platform thread while a background thread invokes | ||
// a message response. Typically accessing the shell holder happens on the | ||
// platform thread and doesn't require locking. | ||
private ReentrantReadWriteLock shellHolderLock = new ReentrantReadWriteLock(); |
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.
Rather than this, could we just have a different Java object that the UI thread works with?
We could avoid the lock on this side completely if we used API similar to FlutterEnginePostDartObject
in the embedder API.
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.
That may be possible. So this is protecting the engine from being deleted on the platform thread while a reply is being invoked in the engine from some arbitrary thread, which will eventually post to the ui thread. We could percolate up the hop to the ui thread, but even if we were executing here on the ui thread we'd have to have a mutex to make sure the platform thread doesn't change the shellHolder while the ui thread is accessing it.
I think in practice that might be ok to not have the mutex since the ui thread should be shutdown as part of deleting the engine on the platform thread. We'd have to plumb through a way to dispatch to the ui thread from java though and that approach relies on logic that exists somewhere else to protect us, instead of just being able to read through the code and knowing it's safe. It's harder to reason about and prove it's correct.
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's not clear to me that reading this lets us know its safe (you still have to assume some things about the platform/UI thread lifecycles), and it's a little confusing to have all but one method on this class be called exclusively from the platform thread.
It's probably fine to at least start with the mutex and see if we can get rid of it later, but I think I'd still feel more comfortable with this being a different Java object, even if only to make sure that the caller(s) of this method don't get confused about which other methods they can call and only find out at runtime that they're wrong.
part 1: moved dispatch to embedder
PlatformMessageHandlerAndroid This eliminates the potential race condition where we are calling into a platform view when it is being deallocated (That may not be possible in practice but this is safer).
attaching, deatching and invoking the reply with locks.
2) remove it's ownership from android_shell_holder, now just shell and the platform_view have a reference
3992cc4
to
734f5d8
Compare
shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java
Outdated
Show resolved
Hide resolved
@@ -98,6 +99,12 @@ | |||
@Keep | |||
public class FlutterJNI { | |||
private static final String TAG = "FlutterJNI"; | |||
// This serializes the invocation of platform message responses and the |
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.
Why does the shellHolderLock need to be reentrant?
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.
That's the only concrete implementation of ReadWriteLock in the jdk.
shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java
Outdated
Show resolved
Hide resolved
Okay, @dnfield. I think you'll like this change. Now the TaskQueue definition looks like this: public interface TaskQueue {} There is no way that a user can make their own TaskQueue, and when they get a TaskQueue they can't execute anything on it. This is the most locked down we can make it. |
void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler); | ||
void setMessageHandler( | ||
@NonNull String channel, | ||
@Nullable BinaryMessageHandler handler, | ||
@Nullable TaskQueue taskQueue); |
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 is a breaking change, right? It is possible to keep the old method as an overload, by analogy with EventChannel constructors? (here and elsewhere)
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 change is unavoidable because BinaryMessenger is an interface, a hypothetical implementor of the BinaryMessenger would have to implement the new method or accommodate the new parameter, same difference.
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.
See https://flutter.dev/docs/resources/compatibility
The criteria is whether this breaks a test, including contributed 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.
(Or rather, especially contributed tests, including tests in Google's internal monorepo).
IOW, this can probably break some people, but if they didn't contribute tests for it, we don't really know, and so we'll just make the change.
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.
Turns out it breaks flutter/plugins tests, which should probably qualify it as breaking.
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.
+1 for using default implementation. Isn't every plugin broken by this change?
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.
(We'd still need default for makeBackgroundTaskQueue
).
Only unit tests are breaking, because they're expecting method signatures that are no longer valid.
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.
And in particular, only unit tests that mock the binary messenger itself.
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.
ah - got it
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.
There is one legitimate breakage in production code for an internal customer. They have a class that wraps up a BinaryMessenger, but doesn't implement the BinaryMessenger interface. Splitting up setMessageHandler
and setTaskQueue
would avoid the problem. I still think the breaking change is better since it leads to cleaner bookkeeping instead of creating a more complicated bookkeeping to accommodate very rare usage, where the fix is trivial. Setting the handler and the thread it executes on should be an atomic choice.
@@ -26,6 +26,18 @@ | |||
* @see EventChannel , which supports communication using event streams. | |||
*/ | |||
public interface BinaryMessenger { | |||
/** An abstraction over dispatching a Runnable to be executed on some thread. */ |
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.
Consider documenting a bit more about how/where this is used.
nit: instead of an abstraction
, maybe something like An opaque dispatching interface for servicing plugin method calls
or something.
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, I don't want to specifically say it's opaque. That might not be true in the future. (edit, almost done, waiting for the local checks fyi).
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.
LGTM with nit about doc on TaskQueue
.
This reverts commit 7ed91e14ccc1f5ffc4cb13d1d37e27d370c8f7cd.
* Revert "Revert "Android Background Platform Channels (#29147)"" This reverts commit 7ed91e14ccc1f5ffc4cb13d1d37e27d370c8f7cd. * Made this PR less of a breaking change by keeping setMessageHandler's signature the same
issue flutter/flutter#91635 (fixed for android)
google issue: b/203443395
Everything has unit tests. There aren't integration tests since the only possible test harness would be scenario app which we are removing. All existing platform channels go through this code so broken existing behavior is covered with existing integration tests and performance tests like platform_channels_benchmarks
In a separate PR I plan on expanding the benchmarks tests to cover background task queues too (the the flutter repo).
Pre-launch Checklist
writing and running engine tests.
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.