创建对象实例触发 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;
引用计数对象实例的构造如下:
施工 - TInterfacedObject.Create -> RefCount = 0
- 正在执行
NewInstance
- 正在执行构造函数链
- 正在执行
AfterConstruction
链
分配给初始强引用Intf := ...
_AddRef -> RefCount = 1
为了理解实际问题,我们需要深入挖掘构造顺序,特别是 NewInstance
和 AfterConstruction
方法
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
实例生命周期 的情况下进行
我有两个引用计数 类,它们保存对彼此实例的引用。其中一个引用被标记为 [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;
引用计数对象实例的构造如下:
施工 -
TInterfacedObject.Create -> RefCount = 0
- 正在执行
NewInstance
- 正在执行构造函数链
- 正在执行
AfterConstruction
链
- 正在执行
分配给初始强引用
Intf := ...
_AddRef -> RefCount = 1
为了理解实际问题,我们需要深入挖掘构造顺序,特别是 NewInstance
和 AfterConstruction
方法
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
实例生命周期 的情况下进行