创建对象实例触发 AV

Creating object instance triggers AV

我有两个引用计数 类,它们保存对彼此实例的引用。其中一个引用被标记为 [weak] 以防止创建强引用循环。

type
  TFoo = class(TInterfacedObject)
  private
    [weak]
    FRef: IInterface;
  public
    constructor Create(const ARef: IInterface);
  end;

  TBar = class(TInterfacedObject)
  private
    FFoo: IInterface;
  public
    constructor Create; virtual;
    destructor Destroy; override;
    procedure AfterConstruction; override;
  end;

constructor TFoo.Create(const ARef: IInterface);
begin
  inherited Create;
  FRef := ARef;
end;

constructor TBar.Create;
begin
  inherited;
end;

destructor TBar.Destroy;
begin
  inherited;
end;

procedure TBar.AfterConstruction;
begin
  inherited;
  FFoo := TFoo.Create(Self);
end;

procedure Test;
var
  Intf: IInterface;
begin
  Intf := TBar.Create;
  writeln(Assigned(Intf)); // TRUE as expected
end; // AV here

但我无法成功完成 TBar 对象实例的构建,并且退出测试过程在 _IntfClear 处触发了访问冲突异常。

Exception class $C0000005 with message 'access violation at 0x0040e398: read of address 0x00000009'.

单步执行调试器显示 TBar.Destroy 在代码到达 writeln(Assigned(Intf)) 行之前被调用,并且在构造过程中没有异常。

为什么这里构造对象时调用了析构函数,为什么没有异常?

引用计数概述

要了解这里发生的事情,我们需要简要概述 Delphi ARC 在经典编译器下如何处理引用计数对象实例(实现某些接口的实例)。

引用计数基本上计算对对象实例的强引用,当对对象的最后一个强引用超出范围时,引用计数将降至 0,实例将被销毁。

此处的强引用表示接口引用(对象引用和指针不会触发引用计数机制),编译器会在适当的位置插入对 _AddRef_Release 方法的调用,以增加和减少引用计数.例如,当调用分配给接口 _AddRef 时,以及当该引用超出范围 _Release 时。

简化后的方法大致如下:

function TInterfacedObject._AddRef: Integer;
begin
  Result := AtomicIncrement(FRefCount);
end;

function TInterfacedObject._Release: Integer;
begin
  Result := AtomicDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;

引用计数对象实例的构造如下:

  1. 施工 - TInterfacedObject.Create -> RefCount = 0

    • 正在执行 NewInstance
    • 正在执行构造函数链
    • 正在执行 AfterConstruction
  2. 分配给初始强引用Intf := ...

    • _AddRef -> RefCount = 1

为了理解实际问题,我们需要深入挖掘构造顺序,特别是 NewInstanceAfterConstruction 方法

class function TInterfacedObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TInterfacedObject(Result).FRefCount := 1;
end;

procedure TInterfacedObject.AfterConstruction;
begin
  AtomicDecrement(FRefCount);
end;

为什么 NewInstance 中的初始引用计数设置为 1 而不是 0?

初始引用计数必须设置为 1,因为构造函数中的代码可能很复杂并且可能触发瞬时引用计数,这可能会在对象有机会分配给初始强引用之前在构造过程中自动销毁对象让它活着。

然后在 AfterConstruction 中减少初始引用计数,并正确设置对象实例引用计数以进行进一步的引用计数。


问题

这个问题代码中的真正问题实际上是它在 AfterConstruction 方法 调用 inherited 之后触发瞬态引用计数,这会减少初始对象引用计数回到 0。因此,对象的计数将增加,然后减少到 0,并且它将自毁调用 Destroy

虽然对象实例在构造函数链中受到保护免于自毁,但在短时间内它将在 AfterConstruction 方法中处于脆弱状态,我们需要确保没有代码在那段时间可以触发引用计数机制

在这种情况下触发引用计数的实际代码隐藏在意想不到的地方,它以 [weak] 属性的形式出现。因此,应该阻止实例参与引用计数机制的事情实际上会触发它 - 这是 [weak] 属性设计中的缺陷,报告为 RSP-20406


解决方案

  • 如果可能,将可以触发引用计数的代码从AfterConstruction移动到构造函数
  • AfterConstruction 方法的结尾而不是开头调用 inherited
  • 通过在开头调用 AtomicIncrement(FRefCount) 并在 AfterConstruction 结尾调用 AtomicDecrement(FRefCount) 自行执行一些显式引用计数(您不能使用 _Release,因为它会销毁对象)
  • [weak] 属性替换为 [unsafe](这只能在 TFoo 实例生命周期永远不会超过 TBar 实例生命周期
  • 的情况下进行