如何在Android/iOS中释放组件

mic*_*mok 13 delphi android components ios firemonkey

TEdit在Android上动态创建表单:

edit := TEdit.Create(Self);
Run Code Online (Sandbox Code Playgroud)

我想用它来释放它edit.Free,但它仍然在形式上.

此代码在win32上运行正常,但在Android上失败.

同样的情况似乎不仅发生在TEdit上,也发生在任何使用Android或iOS的组件上.

Dal*_*kar 44

简答

TComponent在Delphi ARC编译器(目前是Android和iOS)下发布任何后代对象时,应遵循两条规则:

  • DisposeOf无论对象是否拥有,使用都是强制性的
  • 在析构函数中或者在DisposeOf调用之后不久引用不会超出范围的情况下,对象引用也应该设置为nil(陷阱中的详细说明)

使用DisposeOfAndNil方法可能很有吸引力,但ARC使它比旧FreeAndNil方法的情况复杂得多,我建议使用普通DisposeOf - nil序列来避免其他问题:

Component.DisposeOf;
Component := nil;
Run Code Online (Sandbox Code Playgroud)

虽然在许多情况下,即使不遵循上述规则,代码也能正常运行,但这样的代码会相当脆弱,并且很容易被看似无关的地方引入的其他代码破坏.

在ARC内存管理的上下文中DisposeOf

DisposeOf打破ARC.它违反了ARC的黄金法则任何对象引用都可以是有效的对象引用或者是nil,并引入第三个状态 - 处理的"僵尸"对象引用.

任何试图理解ARC内存管理的人都应该看一下DisposeOf只能解决Delphi特定框架问题的附加内容,而不是真正属于ARC本身的概念.

为什么DisposeOf存在于Delphi ARC编译器中?

TComponentclass(及其所有后代)的设计考虑了手动内存管理.它使用与ARC内存管理不兼容的通知机制,因为它依赖于破坏析构函数中的强引用循环.由于TComponent是Delphi框架所依赖的基类之一,它必须能够在ARC内存管理下正常运行.

除了Free Notification机制外,Delphi框架中还有其他类似的设计适用于手动内存管理,因为它们依赖于在析构函数中打破强大的参考周期,但这些设计不适合ARC.

DisposeOf方法可以直接调用对象析构函数,并使这些遗留代码能够与ARC一起使用.

这里必须注意一件事.即使您今天编写,任何使用或继承的代码都会在正确的ARC管理环境中TComponent自动成为遗留代码.

引用Allen Bauer的博客Give in ARC一侧

那么DisoseOf还能解决什么呢?在各种Delphi框架(包括VCL和FireMonkey)中,将活动通知或列表管理代码放在类的构造函数和析构函数中是很常见的.TComponent的所有者/所有者模型是这种设计的关键示例.在这种情况下,现有的组件框架设计依赖于除了在析构函数中发生的简单"资源管理"之外的许多活动.

TComponent.Notification()是这种事情的一个关键例子.在这种情况下,"处理"组件的正确方法是使用DisposeOf.TComponent派生词通常不是瞬态实例,而是一个寿命较长的对象,它也被构成表单,框架和数据模块等其他组件实例的整个系统所包围.在这种情况下,使用DisposeOf是合适的.

DisposeOf如何工作

为了更好地理解DisposeOf调用时究竟发生了什么,有必要了解Delphi对象销毁过程的工作原理.

在ARC和非ARC Delphi编译器中发布对象涉及三个不同的阶段

  1. 调用destructor Destroy方法链
  2. 清理对象管理字段 - 字符串,接口,动态数组(在包含普通对象引用的ARC编译器下)
  3. 从堆中释放对象内存

使用非ARC编译器释放对象

Component.Free - >立即执行阶段 1 -> 2 -> 3

使用ARC编译器释放对象

  • Component.FreeComponent := nil- >减少对象引用计数,然后a)b)

    • a)如果对象引用计数为0 - >立即执行阶段1 -> 2 -> 3
    • b)如果对象引用计数大于0,则不会发生其他情况

  • Component.DisposeOf- >立即执行阶段1,阶段,23在对象引用计数达到0时DisposeOf执行.不会减少调用引用的引用计数.

TComponent通知系统

TComponent Free Notification机制通知已注册的组件正在释放特定组件实例.通知的组件可以在虚拟Notification方法中处理该通知,并确保它们清除它们可能在被销毁的组件上保留的所有引用.

在非ARC编译器下,该机制确保您不会最终指向指向无效释放对象的悬空指针,并且在ARC编译器下清除对销毁组件的引用将减少其引用计数并打破强引用周期.

Free Notification机制是在TComponent析构函数中触发的,没有DisposeOf和直接执行析构函数,两个组件可以在整个应用程序生命周期中保持对彼此的强大引用.

FFreeNotifies保存对通知感兴趣的组件列表的列表被声明为FFreeNotifies: TList<TComponent>,它将存储对任何已注册组件的强引用.

因此,例如,如果您拥有TEditTPopupMenu在您的表单上并将该弹出菜单指定为编辑PopupMenu属性,则编辑将在其FEditPopupMenu字段中保留对弹出菜单的强引用,并且弹出菜单将在其FFreeNotifies列表中保留对编辑的强引用.如果要释放这两个组件中的任何一个,则必须调用DisposeOf它们,否则它们将继续存在.

虽然您可以尝试手动跟踪这些连接并在释放任何在实践中可能不那么容易的对象之前打破强引用周期.

以下代码基本上会泄漏ARC下的两个组件,因为它们将保持对彼此的强引用,并且在过程完成后,您将不再有任何外部引用指向其中任何一个组件.但是,如果您更换Menu.FreeMenu.DisposeOf你会触发Free Notification机制,打破有力的参考周期.

procedure ComponentLeak;
var
  Edit: TEdit;
  Menu: TPopupMenu; 
begin
  Edit := TEdit.Create(nil);
  Menu := TPopupMenu.Create(nil);

  Edit.PopupMenu := Menu; // creating strong reference cycle

  Menu.Free; //  Menu will not be released because Edit holds strong reference to it
  Edit.Free; // Edit will not be released because Menu holds strong reference to it
end;
Run Code Online (Sandbox Code Playgroud)

DisposeOf的陷阱

除了打破ARC之外,这本身就很糟糕,因为当你打破它时你没有太多使用它,DisposeOf开发人员应该注意的实现方式也存在两个主要问题.

1. DisposeOf不会减少调用参考 QP报告RSP-14681的引用计数

type
  TFoo = class(TObject)
  public
    a: TObject;
  end;

var
  foo: TFoo; 
  b: TObject;

procedure DoDispose;
var
  n: integer;
begin
  b := TObject.Create;
  foo := TFoo.Create;
  foo.a := b;
  foo.DisposeOf;
  n := b.RefCount; // foo is still alive at this point, also keeping b.RefCount at 2 instead of 1
end;

procedure DoFree;
var
  n: integer;
begin
  b := TObject.Create;
  foo := TFoo.Create;
  foo.a := b;
  foo.Free;
  n := b.RefCount; // b.RefCount is 1 here, as expected 
end;
Run Code Online (Sandbox Code Playgroud)

2. DisposeOf不清理实例内部托管类型引用 QP报告RSP-14682

type
  TFoo = class(TObject)
  public
    s: string;
    d: array of byte;
    o: TObject;
  end;

var
  foo1, foo2: TFoo;

procedure DoSomething;
var
  s: string;
begin
  foo1 := TFoo.Create;
  foo1.s := 'test';
  SetLength(foo1.d, 1);
  foo1.d[0] := 100;
  foo1.o := TObject.Create;
  foo2 := foo1;
  foo1.DisposeOf;
  foo1 := nil;
  s := IntToStr(foo2.o.RefCount) + ' ' + foo2.s + ' ' + IntToStr(foo2.d[0]); 
  // output: 1 test 100 - all inner managed references are still alive here, 
  // and will live until foo2 goes out of scope
end;
Run Code Online (Sandbox Code Playgroud)

解决方法

destructor TFoo.Destroy;
begin
  s := '';
  d := nil;
  o := nil;
  inherited;
end;
Run Code Online (Sandbox Code Playgroud)

上述问题的综合效应可以以不同的方式表现出来.从保持更多分配的内存到难以捕获由包含的非拥有对象和接口引用的错误,意外引用计数引起的错误.

由于DisposeOf不减少调用引用的引用计数,因此nil在析构函数中对此类引用很重要,否则整个对象层次结构可以比所需的更长时间保持活动,并且在某些情况下甚至在整个应用程序生命周期中.

3. DisposeOf不能用于解决所有循环引用

最后,但并非最不重要的问题DisposeOf是,只有在析构函数中有解析它们的代码时才会破坏循环引用 - 就像TComponent通知系统那样.

这些不由析构函数处理的循环应该使用其中一个引用的[weak]和/或[unsafe]属性来破坏.这也是ARC的首选做法.

DisposeOf不应该用作快速修复来打破所有参考周期(它从未设计过的参考周期),因为它不起作用并且滥用它会导致难以跟踪内存泄漏.

不会被破坏的循环的简单示例DisposeOf是:

type
  TChild = class;

  TParent = class(TObject)
  public
    var Child: TChild;
  end;

  TChild = class(TObject)
  public
    var Parent: TParent;
    constructor Create(AParent: TParent);
  end;

constructor TChild.Create(AParent: TParent);
begin
  inherited Create;
  Parent := AParent;
end;

var
  p: TParent;
begin
  p := TParent.Create;
  p.Child := TChild.Create(p);
  p.DisposeOf;
  p := nil;
end;
Run Code Online (Sandbox Code Playgroud)

上面的代码将泄漏子对象实例和父对象实例.结合DisposeOf不清除内部托管类型(包括字符串)的事实,这些泄漏可能很大,具体取决于您存储在内部的数据类型.打破这个循环的唯一(正确)方法是改变TChild类声明:

  TChild = class(TObject)
  public
    [weak] var Parent: TParent;
    constructor Create(AParent: TParent);
  end;
Run Code Online (Sandbox Code Playgroud)

  • @Rudy请阅读我在Allen Bauer博客文章中添加的引文.如果这不是官方建议在`TComponent`后代使用`DisposeOf`,那么我真的不知道还有什么能让你满意. (5认同)
  • @Rudy`TComponent`后代主要与IDE设计器中的`Forms`,`Frames`和`DataModules`结合使用.他们可能与其他组件和控件有着复杂的关系,这些组件和控件将获取其引用.试图手动解决所有问题是西西弗斯的工作.FMX框架使用`DisposeOf`来释放控制子项和拥有的组件是有原因的. (4认同)
  • 使`TComponent`成为ARC-aware根本不会破坏任何代码.问题是在移动设备上,`TComponent`对其拥有的组件使用**强引用**,而在桌面上它使用**弱引用**.所有EMBT必须做的是在移动设备上使`TComponent`使用**弱引用**,然后通知与桌面上的工作相同,不需要`DisposeOf()`. (4认同)
  • 与`TControl`及其子/父控件相同.他们也需要改为**弱**. (3认同)
  • 像大卫一样,我不同意DisposeOf就是这里的解决方案.它是最方便的一种,它可能适用于大多数情况,但不是推荐的方法.这个答案促使`DisposeOf`成为某种标准解决方案.**它似乎完全没有意识到一些可能的含义.**现在可以使用,但有一天它可能会让你陷入困境,难以跟踪其他代码部分的问题.`DisposeOf`只应在特殊情况下使用.正确的方法是摆脱Onwer和Parent以及诸如此类的引用. (2认同)

Dav*_*nan 11

在移动平台上,使用ARC管理生命周期.只有在没有对象剩余的引用时才会销毁对象.您的对象具有对它的引用,特别是来自其父对象.

现在你可以使用DisposeOf强制对象被销毁.更多详情:http://blogs.embarcadero.com/abauer/2013/06/14/38948

但是我怀疑更好的解决方案是删除对象的引用.将其从容器中取出.例如,通过将其父级设置为nil.

  • @MikeSutton和你添加弹出菜单来编辑控件创建另一个强大的参考周期然后呢?也可以手动打破这个循环?如果你愿意,你可以沿着这条路走下去,但这很乏味且容易出错.只要`TComponent`依赖于在析构函数中打破强循环的通知系统`DisposeOf`是唯一的方法. (6认同)