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

Delphi 接口、匿名方法和内存泄漏,delphi,memory-management,interface,delphi-2010,anonymous-methods,Delphi,Memory Management,Interface,Delphi 2010,Anonymous Methods,这是一个构造的示例。我不想在这里发布原始代码。不过,我试图提取相关部分 我有一个界面,可以管理侦听器列表 TListenerProc = reference to procedure (SomeInt : ISomeInterface); ISomeInterface = interface procedure AddListener (Proc : TListenerProc); end; 现在我注册一个侦听器: SomeObj.AddListener (MyListener)

这是一个构造的示例。我不想在这里发布原始代码。不过,我试图提取相关部分

我有一个界面,可以管理侦听器列表

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;
我确实有内存泄漏。匿名方法和接口都不会被释放。我怀疑这是由于某种循环引用。匿名方法保持接口alife,接口保持匿名方法alife

两个问题:

  • 你支持这个解释吗?还是我错过了什么
  • 我能做些什么吗
  • 提前谢谢


    编辑:在一个小到可以发布到这里的应用程序中复制它并不容易。到目前为止,我能做的最好的事情如下。此处未发布匿名方法:

    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.
    
    程序TestMemLeak;
    {$APPTYPE控制台}
    使用
    泛型。集合,系统;
    类型
    接口=接口;
    TListenerProc=程序参考(SomeInt:ISomeInterface);
    接口
    [{DB5A336B-3F79-4059-8933-27699203D1B6}]
    程序AddListener(Proc:TListenerProc);
    过程通知侦听器;
    程序测试;
    结束;
    TSomeInterface=class(TInterfacedObject,ISomeInterface)
    严格保密
    FListeners:TList;
    受保护的
    程序AddListener(Proc:TListenerProc);
    过程通知侦听器;
    程序测试;
    公众的
    构造函数创建;
    毁灭者毁灭;推翻
    结束;
    过程TSomeInterface.AddListener(Proc:TListenerProc);
    开始
    FListeners.Add(Proc);
    结束;
    构造函数TSomeInterface.Create;
    开始
    FListeners:=TList.Create;
    结束;
    析构函数TSomeInterface.Destroy;
    开始
    FreeAndNil(FListeners);
    继承;
    结束;
    程序TSomeInterface.NotifyListeners;
    变量
    监听器:TListenerProc;
    开始
    对于Flistener中的侦听器
    听者(自我);
    结束;
    程序TSomeInterface.测试;
    开始
    //无所事事
    结束;
    程序执行(Proc:TProc);
    开始
    过程;
    结束;
    程序MyListener(SomeInt:ISomeInterface);
    开始
    执行(程序)
    开始
    somint.试验;
    (完),;
    结束;
    变量
    Obj:接口;
    开始
    尝试
    ReportMemoryLeaksOnShutdown:=True;
    Obj:=TSomeInterface.Create;
    Obj.AddListener(MyListener);
    Obj.NotifyListeners;
    Obj:=零;
    除了
    关于E:Exception-do
    Writeln(E.ClassName,“:”,E.Message);
    结束;
    结束。
    
    在我看来,这是一个明确的循环参考问题。匿名方法是通过隐藏接口来管理的,如果
    TList
    属于在其上实现ISomeInterface的对象,那么就会出现循环引用问题

    一个可能的解决方案是在接口上放置一个
    ClearListeners
    方法,该方法调用
    列表上的
    .Clear
    。只要没有其他东西保存对匿名方法的引用,这将使它们全部消失并删除对接口的引用


    我已经写了几篇关于匿名方法的结构和实现的文章,这些文章可能会帮助您了解您真正使用的是什么,以及它们如何更好地运行。您可以在中找到它们。

    您的代码远远不是最小的。以下是:

    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)。这是不能工作或设计周围。在我看来就像编译器中的一个bug

    编辑:


    或者这可能是内存泄漏检测的问题。它与作为接口的参数无关,无参数过程会导致相同的“泄漏”。非常奇怪。

    问题在于dpr main中的匿名方法

    只需将代码放入例程中,并在dpr main中调用该例程,内存泄漏报告就会消失

    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.
    

    你应该告诉我们AddListener是如何工作的。我只是把它们放在一个
    t列表中。
    我看到的所有代码看起来都不错。问题一定在隐藏的部分。你能给出一个导致泄漏的完整例子吗?见我对Mghie答案的评论。检查一下这是否有帮助。在哪里调用
    ClearListeners
    方法?如果这是一个有点通用的答案,很抱歉,但是“在清理过程中”。只要你想让这一切超出范围。谢谢你,梅森!你能看一下我编辑的问题和示例代码吗?当我在main方法的末尾调用
    ClearListeners
    时,匿名方法仍然泄漏。这似乎是编译器的问题。如果您将代码(try块)从程序的主例程移动到一个过程,然后让主程序调用该过程,则不会报告泄漏。好的,请注意示例代码太长。我认为必须涉及接口引用计数。似乎不是这样。和+1用于提取这个问题的本质。@Mason:我也发现了这一点。不幸的是,在原始代码中这并不是那么容易。因此,除了在这里避免匿名方法之外,似乎没有其他解决办法。@Smasher:对于过程引用,接口引用计数肯定是涉及的。当您在调试器中单步执行此操作时,您将看到在分配给
    侦听器的过程中,ref计数从1变为2变为1。它不会回到零,或者只有在FastMM4已经抱怨过之后才会回到零。这也解释了为什么将其移动到另一个过程中会起作用,这引入了另一个作用域级别,在程序退出之前实际上可以保留该级别。