繁体   English   中英

创建对象实例会触发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处触发访问冲突异常。

异常类$ C0000005,消息'访问冲突位于0x0040e398:读取地址0x00000009'。

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

为什么在构造对象期间调用析构函数以及为什么没有异常?

参考计数概述

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

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

此处的强引用表示接口引用(对象引用和指针不触发引用计数机制),并且编译器在适当的位置插入对_AddRef_Release方法的_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减少初始引用计数,并且正确设置对象实例引用计数以进一步引用计数。


问题

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

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

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


解决方案(S)

  • 如果可能,将可以触发引用计数的代码从AfterConstruction到构造函数
  • 调用在AfterConstruction方法结束时inherited ,而不是AfterConstruction开始。
  • 通过在开始时调用AtomicIncrement(FRefCount)和在AtomicDecrement(FRefCount)结束时AfterConstruction AtomicDecrement(FRefCount)执行一些显式的引用计数(你不能使用_Release因为它会破坏对象)
  • [weak]属性替换为[unsafe] (只有在TFoo实例生命周期永远不会超过TBar实例生命周期时才能执行此操作

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM