【问题标题】:Why is this interface not correctly released when the method is exited?为什么退出方法时这个接口没有正确释放?
【发布时间】:2018-07-06 15:33:05
【问题描述】:

我当前的代码如下所示:

program Project1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Winapi.Windows,
  System.Generics.Collections,
  System.SysUtils;

type
  TForm1 = class
  public
    Events: TList<TProc>;
    constructor Create;
    destructor Destroy; override;
  end;

  TTracingInterfacedObject = class(TInterfacedObject)
  public
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;

  ISharedPtr<T> = interface
    ['{CC9EE6C5-F07B-40E5-B05D-2DFDBD3404A1}']
    function Get: T;
    function GetRefCount: Integer;
  end;

  ICatalog = interface
    ['{F421BBA8-8DA3-47EE-ADB9-DED26747472E}']
    function GetView: ISharedPtr<TForm1>;
    property View: ISharedPtr<TForm1> read GetView;
  end;

  ITree = interface
    ['{A1E2F71B-124B-48DB-B038-5F90AC5BE94B}']
    function GetId: TGUID;
    property Id: TGUID read GetId;
  end;

  TSharedPtr<T: class> = class(TTracingInterfacedObject, ISharedPtr<T>)
  private
    FObject: T;
  public
    constructor Create(const AObject: T);
    destructor Destroy; override;
    function GetRefCount: Integer;
    function Get: T;
  end;

  TCatalog = class(TTracingInterfacedObject, ICatalog)
  private
    FView: ISharedPtr<TForm1>;
  public
    constructor Create;
    function GetView: ISharedPtr<TForm1>;
  end;

  TTree = class(TTracingInterfacedObject, ITree)
  private
    FView: ISharedPtr<TForm1>;
  public
    constructor Create(const AView: ISharedPtr<TForm1>);
    function GetId: TGUID;
  end;

function TTracingInterfacedObject._AddRef: Integer;
begin
  OutputDebugString(PChar(ClassName + '._AddRef'));
  Result := inherited _AddRef;
end;

function TTracingInterfacedObject._Release: Integer;
begin
  OutputDebugString(PChar(ClassName + '._Release'));
  Result := inherited _Release;
end;

constructor TForm1.Create;
begin
  inherited;
  Events := TList<TProc>.Create;
end;

destructor TForm1.Destroy;
begin
  Events.Free;
  inherited;
end;

constructor TSharedPtr<T>.Create(const AObject: T);
begin
  inherited Create;
  FObject := AObject;
end;

destructor TSharedPtr<T>.Destroy;
begin
  FObject.Free;
  inherited;
end;

function TSharedPtr<T>.Get: T;
begin
  Result := FObject;
end;

function TSharedPtr<T>.GetRefCount: Integer;
begin
  Result := FRefCount;
end;

constructor TCatalog.Create;
begin
  inherited Create;
  FView := TSharedPtr<TForm1>.Create(TForm1.Create) as ISharedPtr<TForm1>;
end;

function TCatalog.GetView: ISharedPtr<TForm1>;
begin
  Result := FView;
end;

constructor TTree.Create(const AView: ISharedPtr<TForm1>);
begin
  inherited Create;
  FView := AView;
end;

function TTree.GetId: TGUID;
begin
  Result := TGUID.Empty;
end;

procedure Main;
var
  Catalog: ICatalog;
  Tree: ITree;
  Func: TFunc<TGUID>;
  Events: TList<TProc>;
  Event: TProc;
begin
  Catalog := TCatalog.Create as ICatalog;

  Events := Catalog.View.Get.Events;

  Event := procedure
    begin
    end;

  Events.Add(Event);

  Tree := TTree.Create(Catalog.View) as ITree;

  Func := function: TGUID
    begin
      Result := Tree.Id;
    end;
end;

begin
  Main;

end.

我在应用程序的最后一个end. 处设置了一个断点。

此时的事件日志如下所示:

Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TCatalog._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TTree._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Debug Output: TCatalog._Release Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Source Breakpoint at $0047F675: C:\Users\Admin\Documents\Embarcadero\Studio\Projects\ViewFail\Project1.dpr line 168. Process Project1.exe (3456)

所以:

  • 目录是 _AddRefed 一次和 _Released 一次,这很好。
  • 树被 _AddRefed 一次,从不 _Released,这不是我所期望的。
  • Catalog.View 被 _AddRefed 4 次和 _Released 3 次,这也不符合我的预期。

为什么会这样?我在某处缺少参考循环吗?

【问题讨论】:

  • 在单个 dpr 文件中是否不可能将其缩减为最小的、没有 UI 的东西。
  • @DavidHeffernan 我试过了,但如果我删除更多代码,问题就会消失。
  • 主要问题是您的表单有两个所有者 - Application 和 SharedPtr。由于匿名方法变量捕获,应用程序释放表单并留下悬空引用的 SharedPtr。本质上,您的表单被发布了两次。
  • 现在,您可以修复该代码中的直接问题,但不能。那个代码太可怕了。相信我,你不想在生产中使用类似的东西;)
  • 另一个提示。调试时,不要调试引用计数——这绝对不会告诉你什么。将您的日志信息放入析构函数中,您会立即清楚哪些内容被多次发布或以错误的顺序发布。

标签: delphi reference-counting delphi-10.2-tokyo


【解决方案1】:

是的,您的代码中有一个引用循环。它是通过匿名方法变量捕获机制创建的。

匿名方法由引用计数、编译器生成的类支持。匿名方法捕获的任何变量都存储为同一类中的字段。只要匿名方法在作用域内,编译器就会实例化该类的实例并使其保持活动状态。

现在,以上事实不足以创建循环。但是相同的实例(相同的类)将用于备份某个例程中的所有匿名方法。

翻译成你的代码:

  • TForm1 持有 Events
  • Catalog 持有TForm1
  • Tree 持有 TForm1

那里没有循环 - Tree 没有引用 Catalog,也没有 Catalog 引用 Tree

但是,当您查看 Main 程序时,情况会发生变化。

Main 中的匿名方法将由隐藏对象实例支持 - 所以让我们看看会有什么:

  • 第一个匿名方法
  • 第二种匿名方法
  • Tree - 由第二种匿名方法捕获

仍然没有可见的循环 - 但是,然后您将第一个匿名方法 Event 添加到 Tree 持有的 Events 列表中。为了使该方法保持活动状态,整个支持对象也将保持活动状态。

Anonymous method object -> Event 
                        -> Tree -> Events -> Event -> Anonymous method object

要打破这个循环,您必须清除一些引用。例如将Tree 设置为nilMain 中的某处。

【讨论】:

  • 是否在某处记录了所有匿名方法只有一个对象?它肯定会解释我们在这里看到的行为。
  • 我不知道。但这是设计暗示的。如果您有两种方法可以捕获相同的变量,则它们需要由相同的类进行备份——这就是产生这种效果的原因。
猜你喜欢
  • 2011-10-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多