NamedScope和垃圾回收

7

(这个问题最初是在Ninject Google小组中提出的,但我现在看到Stackoverflow似乎更活跃。)

我正在使用NamedScopeExtension将相同的ViewModel注入到View和Presenter中。在View被释放后,内存分析显示ViewModel仍然由Ninject缓存保留。如何使Ninject释放ViewModel?当窗体关闭和处理时,所有ViewModel都会被释放,但我正在使用工厂在窗体中创建和删除控件,并希望ViewModel能够被垃圾回收(Presenter和View也会被回收)。

请参见以下使用dotMemoryUnit的单元测试,以说明该问题:

using System;
using FluentAssertions;
using JetBrains.dotMemoryUnit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Ninject;
using Ninject.Extensions.DependencyCreation;
using Ninject.Extensions.NamedScope;

namespace UnitTestProject
{
    [TestClass]
    [DotMemoryUnit(FailIfRunWithoutSupport = false)]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod()
        {
            // Call in sub method so no local variables are left for the memory profiling
            SubMethod();

            // Assert
            dotMemory.Check(m =>
            {
                m.GetObjects(w => w.Type.Is<ViewModel>()).ObjectsCount.Should().Be(0);
            });
        }

        private static void SubMethod()
        {
            // Arrange
            var kernel = new StandardKernel();
            string namedScope = "namedScope";
            kernel.Bind<View>().ToSelf()
                  .DefinesNamedScope(namedScope);
            kernel.DefineDependency<View, Presenter>();
            kernel.Bind<ViewModel>().ToSelf()
                  .InNamedScope(namedScope);
            kernel.Bind<Presenter>().ToSelf()
                  .WithCreatorAsConstructorArgument("view");

            // Act
            var view = kernel.Get<View>();
            kernel.Release(view);
        }
    }

    public class View
    {
        public View()
        {
        }

        public View(ViewModel vm)
        {
            ViewModel = vm;
        }

        public ViewModel ViewModel { get; set; }
    }

    public class ViewModel
    {
    }

    public class Presenter
    {
        public View View { get; set; }
        public ViewModel ViewModel { get; set; }

        public Presenter(View view, ViewModel viewModel)
        {
            View = view;
            ViewModel = viewModel;
        }
    }
}

dotMemory.Check断言失败,并且在分析快照时,ViewModel引用了Ninject缓存。我认为当View释放时,应该释放命名范围。

敬礼, Andreas

1个回答

8

TL;DR

简短回答:在您的View中添加INotifyWhenDisposed。释放视图。这将导致ninject自动处理所有绑定InNamedScope的内容,同时ninject还会取消引用这些对象。这将导致(最终)垃圾收集(除非您在其他地方保留了强引用)。


为什么您的实现不起作用

Ninject不会在视图被释放/被销毁时得到通知。 这就是为什么ninject有一个计时器运行来检查作用域对象是否仍然存活(存活=未被垃圾回收)。如果作用域对象不再存活,则它将处置/释放在作用域中保存的所有对象。

我相信默认情况下计时器设置为30秒。

那么这到底意味着什么?

  • 如果没有内存压力,GC可能需要很长时间才能回收作用域对象(或者他可能永远不会回收)
  • 一旦作用域对象被垃圾回收,作用域对象的处理和释放可能需要约30秒钟
  • 一旦ninject释放了作用域对象,如果没有内存压力,GC可能需要很长时间才能回收对象。

确定性地释放作用域对象

现在,如果您需要在作用域被释放时立即处理和释放对象,则需要将INotifyWhenDisposed添加到作用域对象中(也可以参见这里)。 对于命名作用域,您需要将此接口添加到使用DefinesNamedScope绑定的类型 - 在您的情况下是View

根据Ninject.Extensions.NamedScope的集成测试,这就足够了:请参见这里

注意:唯一真正确定的事情是处理作用域对象的处置。 实际上,这通常会显着减少垃圾回收发生的时间。但是,如果没有内存压力,实际回收仍可能需要很长时间。

实现这一点应该可以使单元测试通过。

注意:如果根对象绑定了InCallScope,则此解决方案无法使用(ninject 3.2.2 / NamedScope 3.2.0)。我认为这是由于InCallScope的一个错误,但可惜的是我在几年前未能报告它(这个错误)。不过,我可能错了。
证明在根对象实现INotifyWhenDisposed将会销毁子对象。
public class View : INotifyWhenDisposed
{
    public View(ViewModel viewModel)
    {
        ViewModel = viewModel;
    }

    public event EventHandler Disposed;

    public ViewModel ViewModel { get; private set; }

    public bool IsDisposed { get; private set; }

    public void Dispose()
    {
        if (!this.IsDisposed)
        {
            this.IsDisposed = true;
            var handler = this.Disposed;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }
}

public class ViewModel : IDisposable
{
    public bool IsDisposed { get; private set; }

    public void Dispose()
    {
        this.IsDisposed = true;
    }
}

public class IntegrationTest
{
    private const string ScopeName = "ViewScope";

    [Fact]
    public void Foo()
    {
        var kernel = new StandardKernel();
        kernel.Bind<View>().ToSelf()
            .DefinesNamedScope(ScopeName);
        kernel.Bind<ViewModel>().ToSelf()
            .InNamedScope(ScopeName);

        var view = kernel.Get<View>();

        view.ViewModel.IsDisposed.Should().BeFalse();

        view.Dispose();

        view.ViewModel.IsDisposed.Should().BeTrue();
    }
}

它甚至可以与DefineDependencyWithCreatorAsConstructorArgument一起使用

我没有dotMemory.Unit,但这会检查ninject是否在其缓存中保留对对象的强引用:

namespace UnitTestProject
{
    using FluentAssertions;
    using Ninject;
    using Ninject.Extensions.DependencyCreation;
    using Ninject.Extensions.NamedScope;
    using Ninject.Infrastructure.Disposal;
    using System;
    using Xunit;

    public class UnitTest1
    {
        [Fact]
        public void TestMethod()
        {
            // Arrange
            var kernel = new StandardKernel();
            const string namedScope = "namedScope";
            kernel.Bind<View>().ToSelf()
                .DefinesNamedScope(namedScope);
            kernel.DefineDependency<View, Presenter>();
            kernel.Bind<ViewModel>().ToSelf().InNamedScope(namedScope);

            Presenter presenterInstance = null;
            kernel.Bind<Presenter>().ToSelf()
                .WithCreatorAsConstructorArgument("view")
                .OnActivation(x => presenterInstance = x);

            var view = kernel.Get<View>();

            // named scope should result in presenter and view getting the same view model instance
            presenterInstance.Should().NotBeNull();
            view.ViewModel.Should().BeSameAs(presenterInstance.ViewModel);

            // disposal of named scope root should clear all strong references which ninject maintains in this scope
            view.Dispose();

            kernel.Release(view.ViewModel).Should().BeFalse();
            kernel.Release(view).Should().BeFalse();
            kernel.Release(presenterInstance).Should().BeFalse();
            kernel.Release(presenterInstance.View).Should().BeFalse();
        }
    }

    public class View : INotifyWhenDisposed
    {
        public View()
        {
        }

        public View(ViewModel viewModel)
        {
            ViewModel = viewModel;
        }

        public event EventHandler Disposed;

        public ViewModel ViewModel { get; private set; }

        public bool IsDisposed { get; private set; }

        public void Dispose()
        {
            if (!this.IsDisposed)
            {
                this.IsDisposed = true;
                var handler = this.Disposed;
                if (handler != null)
                {
                    handler(this, EventArgs.Empty);
                }
            }
        }
    }

    public class ViewModel
    {
    }

    public class Presenter
    {
        public View View { get; set; }
        public ViewModel ViewModel { get; set; }

        public Presenter(View view, ViewModel viewModel)
        {
            View = view;
            ViewModel = viewModel;
        }
    }
}

1
实际上,这通常会显著缩短垃圾回收的时间。但是,如果没有内存压力,实际的回收仍可能需要很长时间。 dotMemory.Check会导致垃圾回收,因此测试将显示实际的内存状态。 - Ed Pavlov
非常好的解释,但遗憾的是,这并没有解决我的问题。我在ViewModel绑定上添加了一个OnActivation回调,并检索了作用域对象以进行检查:它似乎不是作为作用域对象的View,而是一个DisposeNotifyingObject,它已经实现了INotifyWhenDisposed。 - Andreas Appelros
顺便提一下,我认为使用Kernel.Release()显式释放视图会清理View绑定定义的命名空间。 - Andreas Appelros
@AndreasAppelros,没错,因为范围对象ninject实际上并没有使用通过“DefinesNamedScope(...)”绑定的对象。然而,正如ninject集成测试所显示的那样,如果绑定的对象(在您的情况下是“View”)实现了“INotifyWhenDisposed”,它仍将起作用。你试过了吗? - BatteryBackupUnit
@AndreasAppelros kernel.Release 方法甚至不会调用对象的 Dispose 方法,它只会将对象从缓存中移除。 - BatteryBackupUnit
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接