一个开发者工具包,用于检测、诊断和减轻 .NET MAUI 应用程序中的内存泄漏。
MAUI memory behavior is much better than it was when this project started, but real apps can still leak views, handlers, binding contexts, and platform resources.
MemoryToolkit.Maui V2 is a .NET 10 rebaseline of the original toolkit. MAUI 9/10 introduced automatic handler disconnection and a DisconnectHandlers() API, so V2 defaults to a less destructive teardown path while keeping the original leak detection and containment tools available for apps that need them.
MemoryToolkit.Maui offers three primary features to help manage this problem:
BindingContext, MAUI behaviors, Content, ItemsSource, item templates, gesture recognizers, formatted text spans, and Parent when you need stronger fault containment.If this project saves you time, money, or sanity, please consider sponsoring me here on GitHub :heart:
nuget install AdamE.MemoryToolkit.Maui
Update MauiProgram.cs. Note, this is only required for leak detection and global V2 defaults. TearDownBehavior does not require configuration.
…Get started detecting leaks at runtime by adding the LeakMonitorBehavior.Cascade attached property to your views.
If you're lucky enough to have no leaks, you should see info logs like:
If you're not so lucky, you'll see warnings like:
If you've configured the callback as demonstrated above, you'll also see a runtime alert:
Once leaks have been detected, you can ask MemoryToolkit.Maui to apply an explicit teardown step when a view appears to be done. The V2 default uses MAUI's built-in DisconnectHandlers() path. If you also need to break managed references such as BindingContext, MAUI behaviors, Content, ItemsSource, item templates, gesture recognizers, formatted text spans, and Parent, opt into Compartmentalize.
Note: When using both LeakMonitorBehavior and TearDownBehavior, make sure TearDownBehavior comes after LeakMonitorBehavior in the XAML.
While leak prevention & compartmentalization features are intended to be safe for production use, you might not want to use leak detection in release builds due to excessive GC.Collect() calls needed to get the GC to behave deterministically.
Warning: since the LeakMonitorBehavior works by walking the visual tree on Unload, it will not detect leaking subviews that may have been dynamically removed from the parent view. In these cases, consider adding another LeakMonitorBehavior.Cascade property to the subview.
Both LeakMonitorBehavior and TearDownBehavior offer an attached property Suppress that can be set to 'true' to exclude any view (and its subviews) from the effects of the behavior. This can be useful in cases where you're already aware of a leak and wish to suppress further warnings. Or perhaps you may not actually expect that view to be automatically monitored or torn down according to our definition of 'done with' (for example, for view caching).
In some cases, known leaks may be worked around by whacking the control into a safe state when we're done with it. For example, an SKLottieView from SkiaSharp once leaked as long as its IsAnimationEnabled property was set to True. The TearDownBehavior class offers a static Action property OnTearDown that is invoked before the toolkit disconnects each element's handler in Compartmentalize mode. The default DisconnectHandlers strategy delegates directly to MAUI's tree-wide extension method and does not run this per-element hook.
V2 offers three teardown strategies:
DetectOnly: do not tear down; useful for leak monitoring-only runs.DisconnectHandlers: call MAUI's built-in handler disconnection path. This is the V2 default.Compartmentalize: clear binding contexts, MAUI behaviors, parent/content references, item sources, item templates, gesture recognizers, formatted text spans, logical children, and resources before disconnecting handlers.MAUI 9+ already disconnects handlers automatically in common cases, such as back navigation, and exposes DisconnectHandlers() for explicit cleanup. MemoryToolkit.Maui keeps DisconnectHandlers as the default because it gives the toolkit a low-destruction cleanup action when its lifecycle inference says a view is done. Use Compartmentalize when you are validating leak propagation or need fault containment.
Compartmentalize clears the VisualElement.Behaviors collection. MemoryToolkit's own LeakMonitorBehavior and TearDownBehavior hooks use attached properties, so they are not stored in that collection.
Managed-reference clearing is best-effort: if a control throws while one reference is being cleared, the exception is logged and teardown continues with the remaining cleanup steps.
There are a few common-enough scenarios where you'll expect a NavigationPage to be unloaded only temporarily. For example, calling Browser.OpenAsync(..). In these cases, you can temporarily set the 'Suppress' properties on the NavigationPage itself, which will cause all behaviors within the page to be ignored. Here's an example handler method:
private void OnTapped(object? sender, TappedEventArgs e)
{
var navigationPage = Utilities.GetFirstSelfOrParentOfType(this);
if (navigationPage == null)
return;
LeakMonitorBehavior.SetSuppress(navigationPage, true);
TearDownBehavior.SetSuppress(navigationPage, true);
}Be sure to set the 'Suppress' properties back to false later. The view's Loaded event is probably a good place for that.
A common use of the ControlTemplate is to change the appearance of a control at run time. For example, https://github.com/roubachof/Sharpnado.TaskLoaderView uses different control templates to show different views based on some loading state (e.g. loading, loaded, error). Whenever ControlTemplates are being used in this way, it's a good idea to use the above attached properties on a per-template basis.
There are two core issues MemoryToolkit.Maui is designed around.
The V1/.NET 8-era failure mode was scary because small leaks could appear to retain much larger page graphs. Current MAUI is better: parent references are weak and handlers are disconnected automatically in common lifecycle cases, so a small leaked child is less likely to consume its whole host page.
That does not mean leak propagation is solved. A leaked control can still strongly retain its own BindingContext, Content, ItemsSource, item templates, resources, event subscriptions, commands, or platform objects. V2 keeps stronger graph clearing behind the opt-in Compartmentalize strategy for cases where you know a view is done and want to reduce the amount of state any remaining leak can keep alive.
Individual controls (e.g. ListView, Border, Entry, etc.) may be implemented in such a way that they need explicit handler cleanup to avoid memory leaks. This is particularly true on Apple platforms where cyclic references are not handled by the garbage collector. MAUI 9+ now disconnects handlers automatically in common cases and exposes DisconnectHandlers(), but apps can still need an intentional lifecycle signal when views are cached, moved, unloaded outside navigation, or need managed-reference containment.
MemoryToolkit.Maui's automatic behaviors are best-effort lifecycle inference. They run after a view unloads and make conservative guesses about when developers are usually 'done with' a view.
V2 currently treats a view as done in these cases:
Page was popped from an active NavigationPage.Page (e.g. a ControlTemplate was swapped out).NavigationPage that has itself unloaded and has not been suppressed.NavigationPage, remains unloaded after a short delay, is no longer in a navigation/modal stack, and is not still hosted by Shell.Automatic lifecycle inference skips NavigationPage containers themselves, because MAUI can keep navigation/modal stack state there while pages are temporarily unloaded. If you intentionally destroy a navigation container, call TearDown() directly.
It also skips views hosted by a NavigationPage while that navigation page still has active modal pages.
Out of the box, MemoryToolkit.Maui uses this definition to automatically apply leak monitoring and low-destruction handler disconnection. Compartmentalization is opt-in.
This is not comprehensive Shell, flyout, tab, or modal navigation tracking. It also cannot detect subviews that were dynamically removed before unload, and it should not be used as the only lifecycle signal for intentionally cached pages. In cases where this definition doesn't apply, MemoryToolkit.Maui still offers tools so developers can take direct control with Monitor() and TearDown().
TearDownBehavior.Cascade runs when MemoryToolkit.Maui believes a view is done. The default strategy is intentionally low-destruction, but Compartmentalize is much more invasive and should only be used when you understand what it clears.
By default, TearDownBehavior calls MAUI's built-in DisconnectHandlers() extension when a view appears to be done. This is the least destructive V2 strategy and does not clear managed references such as BindingContext, Content, or Parent.
When Strategy="Compartmentalize" is set, the behavior also clears binding contexts and removes references each view has to other views. It does this by setting certain properties to null (such as ItemsSource, item templates, Content, and Parent), clearing MAUI behaviors, gesture recognizers, and formatted text spans, and calling ClearLogicalChildren(). If this step fails to remove references to other objects, the leak can still spread.
In Compartmentalize mode, TearDownBehavior.OnTearDown runs before each element's handler is disconnected. Use this only for targeted control state cleanup that is safe to run when the view is done.
A sample MAUI project is included that demonstrates runtime leak detection and opt-in containment with a deliberately leaky control. This is a contrived demo, not proof that current MAUI turns every small leak into a full-page leak. Current MAUI is better than that; the sample gives the toolkit a predictable leak to find. The demonstration is meant to be run on iOS or Mac Catalyst.
The sample is a Shell app with a simple page that shows a scrollable list of 100 random photos from https://picsum.photos. Two buttons allow you to either push a new instance of the page on the navigation stack, or pop the current page. The current (managed) heap size is also displayed:
Right off the bat, the app consumes ~38 MB of managed memory. An empty MAUI app uses ~7-8MB (at least on iOS). The other 30 MB is artificial for the sake of demonstration. The CollectionView's ItemsSource property has been set to a collection of mocked-out 'view model' objects that each contain a 300KB byte array. Most view models probably won't be this big naturally, but it's definit
暂无开放 Issues,或尚未同步最近议题。