Skip to content

Commit 13cfac6

Browse files
[ios] fix memory leak in Editor (#16348)
Context: #16346 This addresses the memory leak discovered by: src/Core/src/Platform/iOS/MauiTextView.cs(37,30): error MA0001: Event 'TextSetOrChanged' could cause memory leaks in an NSObject subclass. Remove the event or add the [UnconditionalSuppressMessage("Memory", "MA0001")] attribute with a justification as to why the event will not leak. src/Core/src/Platform/iOS/MauiTextView.cs(12,20): error MA0002: Member '_placeholderLabel' could cause memory leaks in an NSObject subclass. Remove the member, store the value as a WeakReference, or add the [UnconditionalSuppressMessage("Memory", "MA0002")] attribute with a justification as to why the member will not leak. I could reproduce a leak in a test such as: await InvokeOnMainThreadAsync(() => { var layout = new Grid(); var editor = new Editor(); layout.Add(editor); var handler = CreateHandler<LayoutHandler>(layout); viewReference = new WeakReference(editor); handlerReference = new WeakReference(editor.Handler); platformViewReference = new WeakReference(editor.Handler.PlatformView); }); await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference); Assert.False(viewReference.IsAlive, "Editor should not be alive!"); Assert.False(handlerReference.IsAlive, "Handler should not be alive!"); Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!"); I will create a similar PR for `Entry` as well.
1 parent 0fb5704 commit 13cfac6

File tree

3 files changed

+113
-41
lines changed

3 files changed

+113
-41
lines changed

src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,43 @@ namespace Microsoft.Maui.DeviceTests
1010
[Category(TestCategory.Editor)]
1111
public partial class EditorTests : ControlsHandlerTestBase
1212
{
13+
void SetupBuilder()
14+
{
15+
EnsureHandlerCreated(builder =>
16+
{
17+
builder.ConfigureMauiHandlers(handlers =>
18+
{
19+
handlers.AddHandler<Editor, EditorHandler>();
20+
});
21+
});
22+
}
23+
24+
[Fact(DisplayName = "Does Not Leak")]
25+
public async Task DoesNotLeak()
26+
{
27+
SetupBuilder();
28+
29+
WeakReference viewReference = null;
30+
WeakReference platformViewReference = null;
31+
WeakReference handlerReference = null;
32+
33+
await InvokeOnMainThreadAsync(() =>
34+
{
35+
var layout = new Grid();
36+
var editor = new Editor();
37+
layout.Add(editor);
38+
var handler = CreateHandler<LayoutHandler>(layout);
39+
viewReference = new WeakReference(editor);
40+
handlerReference = new WeakReference(editor.Handler);
41+
platformViewReference = new WeakReference(editor.Handler.PlatformView);
42+
});
43+
44+
await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference);
45+
Assert.False(viewReference.IsAlive, "Editor should not be alive!");
46+
Assert.False(handlerReference.IsAlive, "Handler should not be alive!");
47+
Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!");
48+
}
49+
1350
#if !IOS && !MACCATALYST
1451
// iOS is broken until this point
1552
// https://github.com/dotnet/maui/issues/3425

src/Core/src/Handlers/Editor/EditorHandler.iOS.cs

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Maui.Handlers
99
{
1010
public partial class EditorHandler : ViewHandler<IEditor, MauiTextView>
1111
{
12-
bool _set;
12+
readonly MauiTextViewEventProxy _proxy = new();
1313

1414
protected override MauiTextView CreatePlatformView()
1515
{
@@ -40,31 +40,17 @@ public override void SetVirtualView(IView view)
4040
{
4141
base.SetVirtualView(view);
4242

43-
if (!_set)
44-
PlatformView.SelectionChanged += OnSelectionChanged;
45-
46-
_set = true;
43+
_proxy.SetVirtualView(PlatformView);
4744
}
4845

4946
protected override void ConnectHandler(MauiTextView platformView)
5047
{
51-
platformView.ShouldChangeText += OnShouldChangeText;
52-
platformView.Started += OnStarted;
53-
platformView.Ended += OnEnded;
54-
platformView.TextSetOrChanged += OnTextPropertySet;
48+
_proxy.Connect(VirtualView, platformView);
5549
}
5650

5751
protected override void DisconnectHandler(MauiTextView platformView)
5852
{
59-
platformView.ShouldChangeText -= OnShouldChangeText;
60-
platformView.Started -= OnStarted;
61-
platformView.Ended -= OnEnded;
62-
platformView.TextSetOrChanged -= OnTextPropertySet;
63-
64-
if (_set)
65-
platformView.SelectionChanged -= OnSelectionChanged;
66-
67-
_set = false;
53+
_proxy.Disconnect(platformView);
6854
}
6955

7056
public override Size GetDesiredSize(double widthConstraint, double heightConstraint)
@@ -152,38 +138,84 @@ public static void MapFormatting(IEditorHandler handler, IEditor editor)
152138
public static void MapIsEnabled(IEditorHandler handler, IEditor editor) =>
153139
handler.PlatformView?.UpdateIsEnabled(editor);
154140

155-
bool OnShouldChangeText(UITextView textView, NSRange range, string replacementString) =>
156-
VirtualView.TextWithinMaxLength(textView.Text, range, replacementString);
157-
158-
void OnStarted(object? sender, EventArgs eventArgs)
141+
class MauiTextViewEventProxy
159142
{
160-
if (VirtualView != null)
161-
VirtualView.IsFocused = true;
162-
}
143+
bool _set;
144+
WeakReference<IEditor>? _virtualView;
163145

164-
void OnEnded(object? sender, EventArgs eventArgs)
165-
{
166-
if (VirtualView != null)
146+
IEditor? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null;
147+
148+
public void Connect(IEditor virtualView, MauiTextView platformView)
167149
{
168-
VirtualView.IsFocused = false;
150+
_virtualView = new(virtualView);
169151

170-
VirtualView.Completed();
152+
platformView.ShouldChangeText += OnShouldChangeText;
153+
platformView.Started += OnStarted;
154+
platformView.Ended += OnEnded;
155+
platformView.TextSetOrChanged += OnTextPropertySet;
171156
}
172-
}
173157

174-
void OnTextPropertySet(object? sender, EventArgs e) =>
175-
VirtualView.UpdateText(PlatformView.Text);
158+
public void Disconnect(MauiTextView platformView)
159+
{
160+
_virtualView = null;
176161

177-
private void OnSelectionChanged(object? sender, EventArgs e)
178-
{
179-
var cursorPosition = PlatformView.GetCursorPosition();
180-
var selectedTextLength = PlatformView.GetSelectedTextLength();
162+
platformView.ShouldChangeText -= OnShouldChangeText;
163+
platformView.Started -= OnStarted;
164+
platformView.Ended -= OnEnded;
165+
platformView.TextSetOrChanged -= OnTextPropertySet;
166+
if (_set)
167+
platformView.SelectionChanged -= OnSelectionChanged;
168+
169+
_set = false;
170+
}
171+
172+
public void SetVirtualView(MauiTextView platformView)
173+
{
174+
if (!_set)
175+
platformView.SelectionChanged += OnSelectionChanged;
176+
_set = true;
177+
}
178+
179+
void OnSelectionChanged(object? sender, EventArgs e)
180+
{
181+
if (sender is MauiTextView platformView && VirtualView is IEditor virtualView)
182+
{
183+
var cursorPosition = platformView.GetCursorPosition();
184+
var selectedTextLength = platformView.GetSelectedTextLength();
185+
186+
if (virtualView.CursorPosition != cursorPosition)
187+
virtualView.CursorPosition = cursorPosition;
188+
189+
if (virtualView.SelectionLength != selectedTextLength)
190+
virtualView.SelectionLength = selectedTextLength;
191+
}
192+
}
181193

182-
if (VirtualView.CursorPosition != cursorPosition)
183-
VirtualView.CursorPosition = cursorPosition;
194+
bool OnShouldChangeText(UITextView textView, NSRange range, string replacementString) =>
195+
VirtualView?.TextWithinMaxLength(textView.Text, range, replacementString) ?? false;
184196

185-
if (VirtualView.SelectionLength != selectedTextLength)
186-
VirtualView.SelectionLength = selectedTextLength;
197+
void OnStarted(object? sender, EventArgs eventArgs)
198+
{
199+
if (VirtualView is IEditor virtualView)
200+
virtualView.IsFocused = true;
201+
}
202+
203+
void OnEnded(object? sender, EventArgs eventArgs)
204+
{
205+
if (VirtualView is IEditor virtualView)
206+
{
207+
virtualView.IsFocused = false;
208+
virtualView.Completed();
209+
}
210+
}
211+
212+
void OnTextPropertySet(object? sender, EventArgs e)
213+
{
214+
if (sender is MauiTextView platformView)
215+
{
216+
VirtualView?.UpdateText(platformView.Text);
217+
}
218+
}
187219
}
188220
}
189221
}

src/Core/src/Platform/iOS/MauiTextView.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Runtime.InteropServices;
34
using CoreGraphics;
45
using Foundation;
@@ -9,6 +10,7 @@ namespace Microsoft.Maui.Platform
910
{
1011
public class MauiTextView : UITextView
1112
{
13+
[UnconditionalSuppressMessage("Memory", "MA0002", Justification = "Proven safe in test: EditorTests.DoesNotLeak")]
1214
readonly UILabel _placeholderLabel;
1315
nfloat? _defaultPlaceholderSize;
1416

@@ -34,6 +36,7 @@ public override void WillMoveToWindow(UIWindow? window)
3436
// Native Changed doesn't fire when the Text Property is set in code
3537
// We use this event as a way to fire changes whenever the Text changes
3638
// via code or user interaction.
39+
[UnconditionalSuppressMessage("Memory", "MA0001", Justification = "Proven safe in test: EditorTests.DoesNotLeak")]
3740
public event EventHandler? TextSetOrChanged;
3841

3942
public string? PlaceholderText

0 commit comments

Comments
 (0)