NamedScope 和垃圾回收
NamedScope and garbage collection
(这个问题最早是在NinjectGoogle群里问的,不过现在看Whosebug好像比较活跃。)
我正在使用 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 缓存。我认为命名范围应该在释放视图时被释放。
此致,
安德烈亚斯
TL;DR
简答:将 INotifyWhenDisposed
添加到您的 View
。处理视图。这将导致 ninject 自动处理所有绑定 InNamedScope
的东西,而且 ninject 将取消引用这些对象。这将导致(最终)垃圾收集(除非你在其他地方坚持强引用)。
为什么您的实施不起作用
Ninject 在视图被释放/被处置时不会得到通知。
这就是为什么 ninject 有一个计时器 运行 检查范围对象是否仍然存在(活着 = 未被垃圾收集)。如果范围对象不再存在,它 disposes/releases 范围内的所有对象。
我相信定时器默认设置为 30 秒。
现在这到底是什么意思?
- 如果没有内存压力,GC 可能需要很长时间才能回收范围对象(或者他可能永远不会这样做)
- 一旦范围对象被垃圾回收,处理和释放范围对象可能需要大约 30 秒
- 一旦 ninject 释放范围对象,如果没有内存压力,GC 可能需要很长时间来收集对象。
确定性地释放作用域对象
现在,如果您需要对象在范围释放时立即 disposed/released,则需要添加 INotifyWhenDisposed
to the scope object (also see here)。
使用命名范围,您需要将此接口添加到与 DefinesNamedScope
绑定的类型 - 在您的情况下为 View
.
根据 Ninject.Extensions.NamedScope 的集成测试,这就足够了:参见 here
注意:唯一真正确定的是作用域对象的处理。
实际上,这通常也会显着缩短垃圾收集的时间。但是,如果没有内存压力,再次,实际收集仍然需要很长时间。
执行此操作应该可以通过单元测试。
注意:如果绑定了根对象 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();
}
}
它甚至适用于 DefineDependency
和 WithCreatorAsConstructorArgument
我没有 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;
}
}
}
(这个问题最早是在NinjectGoogle群里问的,不过现在看Whosebug好像比较活跃。)
我正在使用 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 缓存。我认为命名范围应该在释放视图时被释放。
此致, 安德烈亚斯
TL;DR
简答:将 INotifyWhenDisposed
添加到您的 View
。处理视图。这将导致 ninject 自动处理所有绑定 InNamedScope
的东西,而且 ninject 将取消引用这些对象。这将导致(最终)垃圾收集(除非你在其他地方坚持强引用)。
为什么您的实施不起作用
Ninject 在视图被释放/被处置时不会得到通知。 这就是为什么 ninject 有一个计时器 运行 检查范围对象是否仍然存在(活着 = 未被垃圾收集)。如果范围对象不再存在,它 disposes/releases 范围内的所有对象。
我相信定时器默认设置为 30 秒。
现在这到底是什么意思?
- 如果没有内存压力,GC 可能需要很长时间才能回收范围对象(或者他可能永远不会这样做)
- 一旦范围对象被垃圾回收,处理和释放范围对象可能需要大约 30 秒
- 一旦 ninject 释放范围对象,如果没有内存压力,GC 可能需要很长时间来收集对象。
确定性地释放作用域对象
现在,如果您需要对象在范围释放时立即 disposed/released,则需要添加 INotifyWhenDisposed
to the scope object (also see here)。
使用命名范围,您需要将此接口添加到与 DefinesNamedScope
绑定的类型 - 在您的情况下为 View
.
根据 Ninject.Extensions.NamedScope 的集成测试,这就足够了:参见 here
注意:唯一真正确定的是作用域对象的处理。 实际上,这通常也会显着缩短垃圾收集的时间。但是,如果没有内存压力,再次,实际收集仍然需要很长时间。
执行此操作应该可以通过单元测试。
注意:如果绑定了根对象 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();
}
}
它甚至适用于 DefineDependency
和 WithCreatorAsConstructorArgument
我没有 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;
}
}
}