Delphi:对象聚合和使用[weak]属性的内存泄漏问题

8
我想要构建一个类TParent,通过聚合包含多个子对象。其中一些对象是独立的,而有些对象也可以依赖于其他子对象。所有子对象都必须具有对父对象的引用。我还想在可能的情况下使用接口。

为此,我使用TInterfacedObject作为TParent,并使用TAggregatedObject作为子对象。由于子对象和父对象都知道彼此,因此我使用弱引用以避免循环依赖。实际上,这种行为已经在TAggregatedObject中定义。当我仅使用独立子对象(TIndependantChild)时,一切都正常。

问题出现在子对象也依赖于其他子对象时,参见TDependantChild的构造函数。我将对另一个子对象的引用存储在fChild变量中,该变量标记为[weak]属性,在Delphi 10 Berlin中引入。FastMM4在关闭时报告内存泄漏:

enter image description here

访问冲突会导致System.TMonitor.Destroy抛出错误,但只有在使用FastMM4且ReportMemoryLeaksOnShutDown为True时才会发生。

program Project1;

{$APPTYPE CONSOLE}

uses
  FastMM4,
  System.SysUtils;

type
  IParent = interface
  ['{B11AF925-C62A-4998-855B-268937EF30FB}']
  end;

  IChild = interface
  ['{15C19A4E-3FF2-4639-8957-F28F0F44F8B4}']
  end;

  TIndependantChild = class(TAggregatedObject, IChild)
  end;

  TDependantChild = class(TAggregatedObject, IChild)
  private
    [weak] fChild: IChild;
  public
    constructor Create(const Controller: IInterface; const AChild: IChild); reintroduce;
  end;

  TParent = class(TInterfacedObject, IParent)
  private
    fIndependantChild: TIndependantChild;
    fDependantChild: TDependantChild;
  public
    constructor Create;
    destructor Destroy; override;
  end;

{ TParent }

constructor TParent.Create;
begin
  fIndependantChild := TIndependantChild.Create(Self);
  fDependantChild := TDependantChild.Create(Self, fIndependantChild);
end;

destructor TParent.Destroy;
begin
  fDependantChild.Free;
  fIndependantChild.Free;
  inherited;
end;

{ TDependantChild }

constructor TDependantChild.Create(const Controller: IInterface; const AChild: IChild);
begin
  inherited Create(Controller);
  fChild := AChild;
end;

var
  Owner: IParent; 

begin
  ReportMemoryLeaksOnShutDown := True;
  Owner := TParent.Create;
  Owner := nil;
end.

我发现,使用 [unsafe] 而不是 [weak] 可以解决问题,但根据 Delphi 的帮助文档 help

它([unsafe])应该只在 System 单元之外的非常罕见的情况下使用。

因此,我不确定是否应该在这里使用 [unsafe],特别是当我不理解发生了什么时。
那么,在这种情况下,内存泄漏的原因是什么,如何克服它们?

1
为什么需要对子对象进行聚合?你了解聚合的实际含义吗?为什么要混合使用对象引用和接口?这总是导致灾难的一个因素。使用调试器查看为什么会出现内存泄漏。 - Remy Lebeau
1
[weak]在10.1 Berlin中为接口添加,但是早期版本中已经存在了。我可以在XE2中按原样编译您的代码,但是[weak]没有效果。当fChild被分配时,由于聚合,TDependantChildTParent有一个非弱引用,因此Owner具有RefCount=1。当销毁Owner时,TInterfacedObject.BeforeDestruction()会在RefCount<>0时引发错误,导致Owner及其子项泄漏。将fChild更改为Pointer可以解决这个问题。我怀疑在使用[unsafe]时您的情况也类似。请通过调试器进行确认。 - Remy Lebeau
Remy,感谢您的评论。我对聚合概念还很陌生,也许我理解错了。在我的情况下,子对象表示某些类功能。如果没有父项,它们就没有意义,但相同的子对象可以用于组成不同的父项。我也不喜欢混合接口和对象引用,但我发现有几个例子是这样做的[链接](https://dev59.com/4uo6XIcBkEYKwwoYTy4d),并且仅使用接口时会出错。我将尽快查看第二条评论。 - VyPu
我在 procedure TInterfacedObject.BeforeDestruction 上设置了调试器断点。没有 [weak] 时,RefCount 确实为1,而有了 [weak] 后,它变成了0。因此看起来,它在其他地方泄漏了。 - VyPu
1个回答

5
使用外部FastMM4内存管理器时,泄漏和崩溃问题与内部HashMap的finalization有关,该HashMap用于跟踪弱引用。由于这个问题,在Delphi 10.1及更新版本中无法使用第三方内存管理器进行泄漏检测,包括外部FastMM4。这就是为什么您在使用[weak]属性时会出现问题,而不使用[unsafe]属性则没有问题的原因。[XE2/10.1 Berlin回归]无法使用第三方内存管理器
就您的代码而言,在上述情况下可以安全地使用[unsafe]。虽然文档中有关于使用[unsafe]属性的警告,但该警告实际上并没有解释为什么不应该使用[unsafe]
简而言之,当[unsafe]引用所引用的对象实例的生命周期长于引用本身的生命周期时,可以使用[unsafe]属性。
换句话说,必须确保在指向[unsafe]引用所指向的对象实例已被释放后,不再访问该引用。
当所指向的对象被销毁时,[unsafe]引用不会被清零,使用此类引用将导致访问冲突异常。
[weak]属性替换为[unsafe]就足以使您呈现的代码正常运行。
  TDependantChild = class(TAggregatedObject, IChild)
  private
    [unsafe] fChild: IChild;

感谢@Dalija提供非常清晰的答案!我花了好几天时间才理解发生了什么。我还添加了一个链接到另一个相关的讨论,这个讨论是在你提供的链接中找到的。使用FastMM4,这个示例项目生成了与那里讨论的相同的访问冲突错误。 - VyPu

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