接口、匿名方法和内存泄漏

17

这是一个构造性的例子,我不想在这里发布原始代码。尽管如此,我仍然试图提取相关部分。

我有一个接口,用于管理一系列监听器。

TListenerProc = reference to procedure (SomeInt : ISomeInterface);

ISomeInterface = interface
   procedure AddListener (Proc : TListenerProc);   
end;

现在我注册一个监听器:

SomeObj.AddListener (MyListener);

procedure MyListener (SomeInt : ISomeInterface);
begin
  ExecuteSynchronized (procedure
                       begin
                       DoSomething (SomeInt);
                       end);
end;

我确实遇到了内存泄漏的问题。匿名方法和接口都没有被释放。我怀疑这是由于循环引用造成的。匿名方法让接口继续存在,而接口则使匿名方法继续存在。

两个问题:

  1. 您支持这种解释吗?还是我漏掉了其他什么重要的事情?
  2. 有没有办法解决这个问题?

提前感谢!


编辑:在一个足够小的应用程序中复现这个问题并不容易。目前我能做的最好的事情是下面这样。这里匿名方法没有被释放:

program TestMemLeak;

{$APPTYPE CONSOLE}

uses
  Generics.Collections, SysUtils;

type
  ISomeInterface = interface;
  TListenerProc  = reference to procedure (SomeInt : ISomeInterface);

  ISomeInterface = interface
  ['{DB5A336B-3F79-4059-8933-27699203D1B6}']
    procedure AddListener (Proc : TListenerProc);
    procedure NotifyListeners;
    procedure Test;
  end;

  TSomeInterface = class (TInterfacedObject, ISomeInterface)
  strict private
    FListeners          : TList <TListenerProc>;
  protected
    procedure AddListener (Proc : TListenerProc);
    procedure NotifyListeners;
    procedure Test;
  public
    constructor Create;
    destructor  Destroy; override;
  end;


procedure TSomeInterface.AddListener(Proc: TListenerProc);
begin
FListeners.Add (Proc);
end;

constructor TSomeInterface.Create;
begin
FListeners := TList <TListenerProc>.Create;
end;

destructor TSomeInterface.Destroy;
begin
FreeAndNil (FListeners);
  inherited;
end;

procedure TSomeInterface.NotifyListeners;

var
  Listener : TListenerProc;

begin
for Listener in FListeners do
  Listener (Self);
end;

procedure TSomeInterface.Test;
begin
// do nothing
end;

procedure Execute (Proc : TProc);

begin
Proc;
end;

procedure MyListener (SomeInt : ISomeInterface);
begin
Execute (procedure
         begin
         SomeInt.Test;
         end);
end;

var
  Obj     : ISomeInterface;

begin
  try
    ReportMemoryLeaksOnShutdown := True;
    Obj := TSomeInterface.Create;
    Obj.AddListener (MyListener);
    Obj.NotifyListeners;
    Obj := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

你应该展示给我们AddListener如何工作。 - Uwe Raabe
我只是把它们放在了一个 TList <TListenerProc> 中。 - jpfollenius
1
我看到的所有代码都很好。问题一定在隐藏的部分。你能展示一个完整的例子来说明这个泄漏吗? - Uwe Raabe
看一下我对Mghie的回答的评论。检查一下看是否有帮助。 - Mason Wheeler
3个回答

8
您的代码远非最简化。以下是示例代码:
program AnonymousMemLeak;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TListenerProc  = reference to procedure (SomeInt : IInterface);

procedure MyListener (SomeInt : IInterface);
begin
end;

var
  Listener: TListenerProc;

begin
  try
    ReportMemoryLeaksOnShutdown := True;

    Listener := MyListener;
    Listener := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

我也遇到了同样的问题(在这里是Delphi 2009)。这个问题无法解决或设计。看起来像编译器中的一个错误。

编辑:

或许这是内存泄漏检测的问题。这与参数是接口还是无参过程无关,无参过程也会导致相同的“泄漏”。非常奇怪。


那似乎是编译器问题。如果你将代码(try块)从程序的主例程移动到一个过程中,然后让主例程调用该过程,就不会报告泄漏了。 - Mason Wheeler
并+1以提取此问题的本质。 - jpfollenius
很有可能这与单元初始化(和去初始化)顺序有关。 - Jeroen Wiert Pluimers
@Jeroen:同意。不幸的是,对于这些单元,程序员无法控制初始化顺序。 - mghie
@mghie 我没有时间自己测试,但他可能想要插入对FastMM4的引用。(我正在加紧完成一个项目,以便在德国Entwickler Tage期间教授为编写高质量Delphi应用程序而进行的为期2天的实践工作坊) - Jeroen Wiert Pluimers
显示剩余3条评论

3

在我看来,这显然是一个循环引用的问题。匿名方法是通过隐藏接口进行管理的,如果TList<TListenerProc>由实现ISomeInterface的对象所拥有,那么就会出现循环引用的问题。

一种可能的解决方案是在ISomeInterface上添加一个ClearListeners方法,该方法调用TList<TListenerProc>上的.Clear方法。只要没有其他东西持有对匿名方法的引用,那么它们就会全部消失并且删除对ISomeInterface的引用。

我写了几篇关于匿名方法结构和实现的文章,这可能有助于你更好地理解你正在使用的内容以及它们的操作方式。你可以在http://tech.turbu-rpg.com/category/delphi/anonymous-methods找到它们。


清除监听器方法应该在哪里调用? - jpfollenius
抱歉,如果这个回答有点通用,但是“在清理期间”。每当你想让所有这些超出范围。 - Mason Wheeler
迄今为止非常感谢你,Mason!你能否看一下我的编辑后的问题和示例代码?当我把对ClearListeners的调用放在主方法的末尾时,匿名方法仍然会泄漏。 - jpfollenius

1
问题出在dpr主函数中的匿名方法。
只需将代码放入一个例程中,在dpr主函数中调用该例程,即可消除内存泄漏报告。
procedure Main;
var
  Obj: ISomeInterface;
begin
  try
    ReportMemoryLeaksOnShutdown := True;
    Obj := TSomeInterface.Create;
    Obj.AddListener (MyListener);
    Obj.NotifyListeners;
    Obj := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end;

begin
  Main;
end.

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