dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.21k stars 1.75k forks source link

BlazorWebView instances never eligible for garbage collection #17717

Open UnusualJustin opened 1 year ago

UnusualJustin commented 1 year ago

Description

Instances of the BlazorWebView control are never eligible for garbage collection regardless of how the control is created (Xaml or code behind). BlazorWebView does not implement IDisposable so I don't see a way to release any resources.

To demonstrate the issue, I've created an app with a single button that will open a new modal page that includes the demo Blazor page (and a back button). Every time the page Test button is clicked, a new page with a BlazorWebView is created, but regardless of how many times (or even calling GC.Collect directly), the number of WebView2 instances in Task Manager continues to grow.

Steps to Reproduce

  1. Run demo app
  2. Navigate between pages using Test / Back buttons
  3. Notice memory usage and WebView2 instances in TaskManager
  4. This can be observed on Android using edge://inspect (would need to enable dev tools in this project)

image

Link to public reproduction project repository

https://github.com/UnusualJustin/BlazorWebView-Memory-Leak

Version with bug

8.0.0-rc.1.9171 (and previous as far as I can tell)

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android, Windows, I was not able test on other platforms

Affected platform versions

No response

Did you find any workaround?

None so far.

Relevant log output

No response

XamlTest commented 1 year ago

Verified this on Visual Studio Enterprise 17.8.0 Preview 2.0(8.0.0-rc.1.9171). Can repro this on Windows 11 with below Project: MauiApp16.zip

Eilon commented 1 year ago

I've confirmed that this repros. My guess is that if you wait long enough, eventually enough stuff will get garbage collected, and everything will be cleaned up.

But, in the meantime, I did find a workaround that seems to work consistently in my testing:

  1. Find where in your code you want to manually end the BlazorWebView's internal webview control. This could be in some kind of Unloaded event on a page, or as part of some kind of app navigation gesture
  2. Call some APIs on the BlazorWebView to stop all components from running, and then call the WebView2 Close() method

For example, you could add this to a .NET MAUI page (e.g. a ContentPage):

            this.Unloaded += (s, e) =>
            {
                bwv.RootComponents.Clear(); // remove all components, meaning no more messages will be sent
#if WINDOWS
                ((Microsoft.UI.Xaml.Controls.WebView2)bwv.Handler?.PlatformView)?.Close(); // end the WebView2 control (this is a Windows-only thing) and cause the additional EXE processes to end
#endif
            };

But perhaps we can do some of this a bit better automatically in the future.

UnusualJustin commented 1 year ago

@Eilon , thank you for confirming this and offering a workaround.

I'll do some additional testing, but I believe the problem occurs on Android too. When I first discovered it, I was using the remote inspection tools in Edge and noticed the list of available web view instances continued to grow. I don't know of a similar way to investigate for iOS. If I learn anything more, I'll update this item.

For context, our app supports opening multiple tabs where each tab may contain a different document (rendered in a BlazorWebView). Other types of content can be hosted in tabs as well that isn't Blazor-based. Our current strategy is to use a converter to dynamically decide what ContentView to load based on the content being viewed. I realize it may be atypical to create / remove BlazorWebViews over the life of the app.

Eilon commented 1 year ago

After the #if WINDOWS you could do an #if ANDROID and possibly find the appropriate Android API call to do a similar thing, like maybe call .Dispose() on its PlatformView?