创建对象实例会触发AV

Dal*_*kar 8 delphi automatic-ref-counting

我有两个引用计数的类,它们相互引用。这些参考之一被标记为[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
Run Code Online (Sandbox Code Playgroud)

但是我无法成功完成TBar对象实例的构造,退出测试过程会在处触发Access Violation异常_IntfClear

带有消息“在0x0040e398发生访问冲突:读取地址0x00000009”的异常类$ C0000005。

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

为什么在这里构造对象期间调用析构函数,为什么没有例外?

Dal*_*kar 7

参考计数概述

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

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

强引用在这里代表接口引用(对象引用和指针不会触发引用计数机制)和编译器插入通话_AddRef_Release递增和递减引用计数方法,在适当的地方。例如,当分配给interface _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;
Run Code Online (Sandbox Code Playgroud)

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

  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;
Run Code Online (Sandbox Code Playgroud)

为什么将初始参考计数NewInstance设置为1而不是0?

初始引用计数必须设置为1,因为构造函数中的代码可能很复杂,并且可能触发瞬态引用计数,这可能会在构造过程中自动破坏对象,然后才有机会将其分配给使它保持生命的初始强引用。

然后减少该初始参考计数,AfterConstruction并正确设置对象实例参考计数以进行进一步的参考计数。


问题

该问题代码中的真正问题实际上是它调用触发AfterConstruction方法中的瞬态引用计数,从而将初始对象引用计数减小回0。因此,对象的计数将增加,然后减小为0,并会自毁打电话。 inheritedDestroy

虽然对象实例在构造函数链中受到保护而不会自我破坏,但在很短的时间内它会处于AfterConstruction方法内部的脆弱状态,我们需要确保在此期间没有可触发引用计数机制的代码

在这种情况下,触发引用计数的实际代码隐藏在相当意外的位置,并且以[weak]属性的形式出现。因此,应该阻止实例参与引用计数机制的实际上是触发了它-这是[weak]报告为RSP-20406的属性设计中的缺陷。


解决方案

  • 如果可能的话,将可以触发引用计数的代码AfterConstruction移到构造函数中
  • inheritedAfterConstruction方法的末尾而不是开始处调用。
  • 通过AtomicIncrement(FRefCount)在的开头和AtomicDecrement(FRefCount)结尾进行调用来自行执行一些显式引用计数AfterConstruction(您无法使用,_Release因为它将破坏对象)
  • [weak]属性替换为[unsafe](仅在TFoo实例生存期永远不会超过TBar实例生存期的情况下才能执行