Przeglądaj źródła

Fix binding related memory leaks (#15485)

Julien Lebosquain 1 rok temu
rodzic
commit
aa60e5d2f9

+ 9 - 8
src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs

@@ -1,17 +1,18 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
+using Avalonia.Utilities;
 
 namespace Avalonia.Data.Core.ExpressionNodes;
 
-internal sealed class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNode
+internal sealed class AvaloniaPropertyAccessorNode :
+    ExpressionNode,
+    ISettableNode,
+    IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>
 {
-    private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _onValueChanged;
-
     public AvaloniaPropertyAccessorNode(AvaloniaProperty property)
     {
         Property = property;
-        _onValueChanged = OnValueChanged;
     }
 
     public AvaloniaProperty Property { get; }
@@ -39,7 +40,7 @@ internal sealed class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNo
     {
         if (source is AvaloniaObject newObject)
         {
-            newObject.PropertyChanged += _onValueChanged;
+            WeakEvents.AvaloniaPropertyChanged.Subscribe(newObject, this);
             SetValue(newObject.GetValue(Property));
         }
     }
@@ -47,12 +48,12 @@ internal sealed class AvaloniaPropertyAccessorNode : ExpressionNode, ISettableNo
     protected override void Unsubscribe(object oldSource)
     {
         if (oldSource is AvaloniaObject oldObject)
-            oldObject.PropertyChanged -= _onValueChanged;
+            WeakEvents.AvaloniaPropertyChanged.Unsubscribe(oldObject, this);
     }
 
-    private void OnValueChanged(object? source, AvaloniaPropertyChangedEventArgs e)
+    public void OnEvent(object? sender, WeakEvent ev, AvaloniaPropertyChangedEventArgs e)
     {
-        if (e.Property == Property && source is AvaloniaObject o)
+        if (e.Property == Property && Source is AvaloniaObject o)
             SetValue(o.GetValue(Property));
     }
 }

+ 5 - 4
src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.ComponentModel;
 using System.Text;
 using System.Windows.Input;
+using Avalonia.Utilities;
 
 namespace Avalonia.Data.Core.ExpressionNodes;
 
@@ -10,7 +11,7 @@ namespace Avalonia.Data.Core.ExpressionNodes;
 /// A node in an <see cref="BindingExpression"/> which converts methods to an
 /// <see cref="ICommand"/>.
 /// </summary>
-internal sealed class MethodCommandNode : ExpressionNode
+internal sealed class MethodCommandNode : ExpressionNode, IWeakEventSubscriber<PropertyChangedEventArgs>
 {
     private readonly string _methodName;
     private readonly Action<object, object?> _execute;
@@ -41,7 +42,7 @@ internal sealed class MethodCommandNode : ExpressionNode
     protected override void OnSourceChanged(object source, Exception? dataValidationError)
     {
         if (source is INotifyPropertyChanged newInpc)
-            newInpc.PropertyChanged += OnPropertyChanged;
+            WeakEvents.ThreadSafePropertyChanged.Subscribe(newInpc, this);
 
         _command = new Command(source, _execute, _canExecute);
         SetValue(_command);
@@ -50,10 +51,10 @@ internal sealed class MethodCommandNode : ExpressionNode
     protected override void Unsubscribe(object oldSource)
     {
         if (oldSource is INotifyPropertyChanged oldInpc)
-            oldInpc.PropertyChanged -= OnPropertyChanged;
+            WeakEvents.ThreadSafePropertyChanged.Unsubscribe(oldInpc, this);
     }
 
-    private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    public void OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e)
     {
         if (string.IsNullOrEmpty(e.PropertyName) || _dependsOnProperties.Contains(e.PropertyName))
         {

+ 20 - 5
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs

@@ -21,11 +21,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
             => new IndexerAccessor(target, property, argument);
     }
 
-    internal class AvaloniaPropertyAccessor : PropertyAccessorBase
+    internal class AvaloniaPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>
     {
         private readonly WeakReference<AvaloniaObject?> _reference;
         private readonly AvaloniaProperty _property;
-        private IDisposable? _subscription;
 
         public AvaloniaPropertyAccessor(WeakReference<AvaloniaObject?> reference, AvaloniaProperty property)
         {
@@ -56,15 +55,31 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
             return false;
         }
 
+        public void OnEvent(object? sender, WeakEvent ev, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == _property)
+            {
+                PublishValue(Value);
+            }
+        }
+
         protected override void SubscribeCore()
         {
-            _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
+            if (_reference.TryGetTarget(out var reference))
+            {
+                var value = reference.GetValue(_property);
+                PublishValue(value);
+
+                WeakEvents.AvaloniaPropertyChanged.Subscribe(reference, this);
+            }
         }
 
         protected override void UnsubscribeCore()
         {
-            _subscription?.Dispose();
-            _subscription = null;
+            if (_reference.TryGetTarget(out var reference))
+            {
+                WeakEvents.AvaloniaPropertyChanged.Unsubscribe(reference, this);
+            }
         }
     }
 

+ 164 - 3
tests/Avalonia.LeakTests/AvaloniaObjectTests.cs

@@ -1,5 +1,15 @@
-using System;
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
 using System.Reactive.Subjects;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Data.Core;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
 using Avalonia.Threading;
 using JetBrains.dotMemoryUnit;
 using Xunit;
@@ -30,7 +40,7 @@ namespace Avalonia.LeakTests
 
             var weakSource = setupBinding();
 
-            GC.Collect();
+            CollectGarbage();
 
             Assert.Equal("foo", target.Foo);
             Assert.True(weakSource.IsAlive);
@@ -56,11 +66,135 @@ namespace Avalonia.LeakTests
             };
 
             completeSource();
+            CollectGarbage();
+            Assert.False(weakSource.IsAlive);
+        }
+
+        [Fact]
+        public void CompiledBinding_To_InpcProperty_With_Alive_Source_Does_Not_Keep_Target_Alive()
+        {
+            var source = new Class2 { Foo = "foo" };
+
+            WeakReference SetupBinding()
+            {
+                var path = new CompiledBindingPathBuilder()
+                    .Property(
+                        new ClrPropertyInfo(
+                            nameof(Class2.Foo),
+                            target => ((Class2)target).Foo,
+                            (target, value) => ((Class2)target).Foo = (string?)value,
+                            typeof(string)),
+                        PropertyInfoAccessorFactory.CreateInpcPropertyAccessor)
+                    .Build();
+
+                var target = new TextBlock();
+
+                target.Bind(TextBlock.TextProperty, new CompiledBindingExtension
+                {
+                    Source = source,
+                    Path = path
+                });
+
+                return new WeakReference(target);
+            }
+
+            var weakTarget = SetupBinding();
+
+            CollectGarbage();
+            Assert.False(weakTarget.IsAlive);
+        }
+
+        [Fact]
+        public void CompiledBinding_To_AvaloniaProperty_With_Alive_Source_Does_Not_Keep_Target_Alive()
+        {
+            var source = new StyledElement { Name = "foo" };
+
+            WeakReference SetupBinding()
+            {
+                var path = new CompiledBindingPathBuilder()
+                    .Property(StyledElement.NameProperty, PropertyInfoAccessorFactory.CreateAvaloniaPropertyAccessor)
+                    .Build();
+
+                var target = new TextBlock();
+
+                target.Bind(TextBlock.TextProperty, new CompiledBindingExtension
+                {
+                    Source = source,
+                    Path = path
+                });
+
+                return new WeakReference(target);
+            }
+
+            var weakTarget = SetupBinding();
+
+            CollectGarbage();
+            Assert.False(weakTarget.IsAlive);
+        }
+
+        [Fact]
+        public void CompiledBinding_To_Method_With_Alive_Source_Does_Not_Keep_Target_Alive()
+        {
+            var source = new Class1();
+
+            WeakReference SetupBinding()
+            {
+                var path = new CompiledBindingPathBuilder()
+                    .Command(
+                        nameof(Class1.DoSomething),
+                        (o, _) => ((Class1) o).DoSomething(),
+                        (_, _) => true,
+                        [])
+                    .Build();
+
+                var target = new Button();
+
+                target.Bind(Button.CommandProperty, new CompiledBindingExtension
+                {
+                    Source = source,
+                    Path = path
+                });
+
+                return new WeakReference(target);
+            }
+
+            var weakTarget = SetupBinding();
+
+            CollectGarbage();
+            Assert.False(weakTarget.IsAlive);
+        }
+
+        [Fact]
+        public void Binding_To_AttachedProperty_With_Alive_Source_Does_Not_Keep_Target_Alive()
+        {
+            var source = new StyledElement { Name = "foo" };
+
+            WeakReference SetupBinding()
+            {
+                var target = new TextBlock();
+
+                target.Bind(TextBlock.TextProperty, new Binding
+                {
+                    Source = source,
+                    Path = "(Grid.Row)",
+                    TypeResolver = (_, name) => name == "Grid" ? typeof(Grid) : throw new NotSupportedException()
+                });
+
+                return new WeakReference(target);
+            }
+
+            var weakTarget = SetupBinding();
+
+            CollectGarbage();
+            Assert.False(weakTarget.IsAlive);
+        }
+
+        private static void CollectGarbage()
+        {
             GC.Collect();
             // Forces WeakEvent compact
             Dispatcher.UIThread.RunJobs();
             GC.Collect();
-            Assert.False(weakSource.IsAlive);
         }
 
         private class Class1 : AvaloniaObject
@@ -83,6 +217,33 @@ namespace Avalonia.LeakTests
                 get { return _foo; }
                 set { SetAndRaise(FooProperty, ref _foo, value); }
             }
+
+            public void DoSomething()
+            {
+            }
+        }
+
+        private sealed class Class2 : INotifyPropertyChanged
+        {
+            private string? _foo;
+
+            public string? Foo
+            {
+                get => _foo;
+                set
+                {
+                    if (_foo != value)
+                    {
+                        _foo = value;
+                        OnPropertyChanged();
+                    }
+                }
+            }
+
+            public event PropertyChangedEventHandler? PropertyChanged;
+
+            private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+                => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
         }
     }
 }