Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Android Background Platform Channels #29147

Merged

Conversation

gaaclarke
Copy link
Member

@gaaclarke gaaclarke commented Oct 13, 2021

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

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See testing the engine for instructions on
    writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@google-cla google-cla bot added the cla: yes label Oct 13, 2021
@gaaclarke gaaclarke force-pushed the background-platform-channels-android branch 7 times, most recently from 222c2d2 to 3b19ead Compare October 13, 2021 20:23
@gaaclarke
Copy link
Member Author

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);
  }
}

@gaaclarke
Copy link
Member Author

@dnfield We have a functioning implementation for Android. Feel free to take an early look.

Outstanding work:

  1. Tests, this is going to be a doozy. We don't have end to end platform channel tests in the engine.
  2. Docstrings
  3. Probably going to rename PlatformMessageHandler to PlatformMessageHandlerAndroid
  4. Outstanding question about the API and the best way for users to get a TaskQueue (helper function versus factory method on BinaryMessenger).
  5. Breaking Change notice since we are adding a method to BinaryMessenger (very unlikely this affects anyone).
  6. (optimization) move the ui thread hop up from the completer to the PlatformMessageHandler to remove the mutex
  7. Turn my comments about what thread executes what into asserts, which requires plumbing task runners.

Copy link
Contributor

@dnfield dnfield left a 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) {
Copy link
Member

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

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Member Author

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.

@gaaclarke
Copy link
Member Author

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.

  1. We need to make sure the engine doesn't deconstruct when executing the reply on the background thread (maybe moving the reply to the platform thread which really isn't ideal)
  2. We need to make sure that calling into the PlatformMessageHandler isn't routed through the PlatformView since that could also be deleted on the platform thread while we are accessing the PlatformMessageHandler from him.

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();
Copy link
Contributor

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.

Copy link
Member Author

@gaaclarke gaaclarke Oct 15, 2021

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.

Copy link
Contributor

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
@gaaclarke gaaclarke force-pushed the background-platform-channels-android branch from 3992cc4 to 734f5d8 Compare October 19, 2021 18:40
@gaaclarke gaaclarke marked this pull request as ready for review October 19, 2021 18:49
@@ -98,6 +99,12 @@
@Keep
public class FlutterJNI {
private static final String TAG = "FlutterJNI";
// This serializes the invocation of platform message responses and the
Copy link
Member

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?

Copy link
Member Author

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.

@gaaclarke
Copy link
Member Author

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.

@gaaclarke gaaclarke requested a review from dnfield October 20, 2021 22:53
Comment on lines -67 to +84
void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler);
void setMessageHandler(
@NonNull String channel,
@Nullable BinaryMessageHandler handler,
@Nullable TaskQueue taskQueue);
Copy link
Member

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)

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah - got it

Copy link
Member Author

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. */
Copy link
Contributor

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.

Copy link
Member Author

@gaaclarke gaaclarke Oct 21, 2021

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).

Copy link
Contributor

@dnfield dnfield left a 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.

@gaaclarke gaaclarke added the waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land. label Oct 21, 2021
@fluttergithubbot fluttergithubbot merged commit 67c8f20 into flutter:master Oct 21, 2021
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Oct 22, 2021
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Oct 22, 2021
kylinchen pushed a commit to XianyuTech/engine that referenced this pull request Oct 22, 2021
dnfield added a commit that referenced this pull request Oct 26, 2021
gaaclarke pushed a commit that referenced this pull request Oct 26, 2021
gaaclarke added a commit to gaaclarke/engine that referenced this pull request Oct 26, 2021
This reverts commit 7ed91e14ccc1f5ffc4cb13d1d37e27d370c8f7cd.
gaaclarke added a commit that referenced this pull request Oct 27, 2021
* 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
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes platform-android waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants