Skip to content

Commit

Permalink
[ios/android] fix memory leak in ImageButton (#18602)
Browse files Browse the repository at this point in the history
* [ios] fix memory leak in `ImageButton`

Context: #18365

Adding a parameter to the test:

    [Theory("Handler Does Not Leak")]
    [InlineData(typeof(ImageButton))]
    public async Task HandlerDoesNotLeak(Type type)

Shows a memory leak in `ImageButton`, caused by the cycle

* `ImageButtonHandler` ->
* `UIButton` events like `TouchUpInside` ->
* `ImageButtonHandler`

I could solve this problem by creating a `ImageButtonProxy` class -- the
same pattern I've used in other PRs to avoid cycles. This makes an
intermediate type to handle the events and breaks the cycle.

Still thinking if the analyzer could have caught this, issue filed at:

jonathanpeppers/memory-analyzers#12

* [android] fix memory leak in `ImageButton`

Context: a270ebd
Context: 1bbe79d
Context: material-components/material-components-android#2063

Reviewing a GC dump of the device tests, I noticed a `System.Action`
keeping the `ImageButton` alive:

    Microsoft.Maui.Controls.ImageButton
        System.Action
            Java.Lang.Thread.RunnableImplementor

So next, I looked for `System.Action` and found the path on the
`ReferencedTypes` tab:

    System.Action
        Microsoft.Maui.Platform.ImageButtonExtensions.[]c__DisplayClass4_0
            Google.Android.Material.ImageView.ShapeableImageView
            Microsoft.Maui.Controls.ImageButton

Which led me to the code:

    public static async void UpdatePadding(this ShapeableImageView platformButton, IImageButton imageButton)
    {
        platformButton.SetContentPadding(imageButton);
        platformButton.Post(() =>
        {
            platformButton.SetContentPadding(imageButton);
        });
        platformButton.SetContentPadding(imageButton);
    }

?!? Why is this code calling `SetContentPadding` three times?

Reviewing the commit history:

* a270ebd
* 1bbe79d
* material-components/material-components-android#2063

I could comment out the code and the leak is solved, but I found I could
also change the code to use `await Task.Yield()` for the same result.
  • Loading branch information
jonathanpeppers authored Nov 10, 2023
1 parent 432f5cc commit fabfc55
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 28 deletions.
2 changes: 2 additions & 0 deletions src/Controls/tests/DeviceTests/Memory/MemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ void SetupBuilder()
handlers.AddHandler<Polyline, PolylineHandler>();
handlers.AddHandler<IContentView, ContentViewHandler>();
handlers.AddHandler<Image, ImageHandler>();
handlers.AddHandler<ImageButton, ImageButtonHandler>();
handlers.AddHandler<IndicatorView, IndicatorViewHandler>();
handlers.AddHandler<RefreshView, RefreshViewHandler>();
handlers.AddHandler<IScrollView, ScrollViewHandler>();
Expand All @@ -60,6 +61,7 @@ void SetupBuilder()
[InlineData(typeof(Editor))]
[InlineData(typeof(GraphicsView))]
[InlineData(typeof(Image))]
[InlineData(typeof(ImageButton))]
[InlineData(typeof(IndicatorView))]
[InlineData(typeof(Label))]
[InlineData(typeof(Picker))]
Expand Down
68 changes: 46 additions & 22 deletions src/Core/src/Handlers/ImageButton/ImageButtonHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Microsoft.Maui.Handlers
{
public partial class ImageButtonHandler : ViewHandler<IImageButton, UIButton>
{
readonly ImageButtonProxy _proxy = new();

protected override UIButton CreatePlatformView()
{
var platformView = new UIButton(UIButtonType.System)
Expand All @@ -17,18 +19,14 @@ protected override UIButton CreatePlatformView()

protected override void ConnectHandler(UIButton platformView)
{
platformView.TouchUpInside += OnButtonTouchUpInside;
platformView.TouchUpOutside += OnButtonTouchUpOutside;
platformView.TouchDown += OnButtonTouchDown;
_proxy.Connect(VirtualView, platformView);

base.ConnectHandler(platformView);
}

protected override void DisconnectHandler(UIButton platformView)
{
platformView.TouchUpInside -= OnButtonTouchUpInside;
platformView.TouchUpOutside -= OnButtonTouchUpOutside;
platformView.TouchDown -= OnButtonTouchDown;
_proxy.Disconnect(platformView);

base.DisconnectHandler(platformView);

Expand All @@ -55,22 +53,6 @@ public static void MapPadding(IImageButtonHandler handler, IImageButton imageBut
(handler.PlatformView as UIButton)?.UpdatePadding(imageButton);
}

void OnButtonTouchUpInside(object? sender, EventArgs e)
{
VirtualView?.Released();
VirtualView?.Clicked();
}

void OnButtonTouchUpOutside(object? sender, EventArgs e)
{
VirtualView?.Released();
}

void OnButtonTouchDown(object? sender, EventArgs e)
{
VirtualView?.Pressed();
}

partial class ImageButtonImageSourcePartSetter
{
public override void SetImageSource(UIImage? platformImage)
Expand All @@ -85,5 +67,47 @@ public override void SetImageSource(UIImage? platformImage)
button.VerticalAlignment = UIControlContentVerticalAlignment.Fill;
}
}

class ImageButtonProxy
{
WeakReference<IImageButton>? _virtualView;

IImageButton? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null;

public void Connect(IImageButton virtualView, UIButton platformView)
{
_virtualView = new(virtualView);

platformView.TouchUpInside += OnButtonTouchUpInside;
platformView.TouchUpOutside += OnButtonTouchUpOutside;
platformView.TouchDown += OnButtonTouchDown;
}

public void Disconnect(UIButton platformView)
{
platformView.TouchUpInside -= OnButtonTouchUpInside;
platformView.TouchUpOutside -= OnButtonTouchUpOutside;
platformView.TouchDown -= OnButtonTouchDown;
}

void OnButtonTouchUpInside(object? sender, EventArgs e)
{
if (VirtualView is IImageButton imageButton)
{
imageButton.Released();
imageButton.Clicked();
}
}

void OnButtonTouchUpOutside(object? sender, EventArgs e)
{
VirtualView?.Released();
}

void OnButtonTouchDown(object? sender, EventArgs e)
{
VirtualView?.Pressed();
}
}
}
}
12 changes: 6 additions & 6 deletions src/Core/src/Platform/Android/ImageButtonExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Android.Graphics.Drawables;
using System.Threading.Tasks;
using Android.Graphics.Drawables;
using Android.Widget;
using Google.Android.Material.ImageView;
using Google.Android.Material.Shape;
Expand Down Expand Up @@ -41,13 +42,12 @@ public static void UpdateCornerRadius(this ShapeableImageView platformButton, IB
.Build();
}

public static void UpdatePadding(this ShapeableImageView platformButton, IImageButton imageButton)
public static async void UpdatePadding(this ShapeableImageView platformButton, IImageButton imageButton)
{
platformButton.SetContentPadding(imageButton);
platformButton.Post(() =>
{
platformButton.SetContentPadding(imageButton);
});

// see: https://github.com/material-components/material-components-android/issues/2063
await Task.Yield();
platformButton.SetContentPadding(imageButton);
}

Expand Down

0 comments on commit fabfc55

Please sign in to comment.