Ответ 1
Короткий ответ
Существует два правила, которые должны соблюдаться при выпуске любого объекта TComponent
потомка под компиляторами Delphi ARC (в настоящее время Android и iOS):
- с использованием
DisposeOf
является обязательным, независимо от объекта, имеющего владельца, или нет - в деструкторах или в тех случаях, когда ссылка не выходит за рамки сразу после вызова
DisposeOf
, ссылка объекта также должна быть установлена наnil
(подробное объяснение в Pitfalls)
Возможно, имеет смысл иметь метод DisposeOfAndNil
, но ARC делает его намного сложнее, чем в случае старого метода FreeAndNil
, и я бы предложил использовать обычную последовательность DisposeOf - nil
, чтобы избежать дополнительных проблем:
Component.DisposeOf;
Component := nil;
В то время как во многих случаях код будет функционировать должным образом, даже если вышеприведенные правила не соблюдаются, такой код будет довольно хрупким и может быть легко нарушен другим кодом, введенным в кажущиеся несвязанными местами.
DisposeOf в контексте управления памятью ARC
DisposeOf
прерывает ARC. Это нарушает золотое правило ARC Любая ссылка на объект может быть либо действительной ссылкой на объект, либо nil, и вводит ссылку на объект с тремя состояниями - , расположенную "zombie" .
Любой, кто пытается понять управление памятью ARC, должен смотреть на DisposeOf
как дополнение, которое просто решает проблемы с инфраструктурой Delphi, а не концепцию, которая действительно принадлежит самому ARC.
Почему DisposeOf существует в компиляторах Delphi ARC?
TComponent
класс (и все его потомки) был разработан с учетом ручного управления памятью. Он использует механизм уведомления, который несовместим с управлением памятью ARC, потому что он основан на нарушении сильных эталонных циклов в деструкторе. Поскольку TComponent
- это один из базовых классов, на которые полагаются фреймворки Delphi, он должен иметь возможность нормально функционировать при управлении памятью ARC.
Помимо механизма Free Notification
существуют и другие аналогичные конструкции в инфраструктурах Delphi, подходящие для ручного управления памятью, потому что они полагаются на нарушение сильных эталонных циклов в деструкторе, но эти конструкции не подходят для ARC.
DisposeOf
метод позволяет прямое обращение к деструктору объекта и позволяет использовать такой старый код вместе с ARC.
Здесь нужно отметить одно. Любой код, который использует или наследует от TComponent
автоматически, становится устаревшим кодом в контексте правильного управления ARC, даже если вы пишете его сегодня.
Цитата из блога Allen Bauer Дайте стороне ARC
Так что же еще делает DisoseOf? Это очень распространено среди различных Рамки Delphi (включая VCL и FireMonkey), чтобы разместить активные код уведомления или списка управления внутри конструктора и деструктор класса. Собственная модель TComponent - это ключ пример такой конструкции. В этом случае существующий компонент каркасный дизайн опирается на многие виды деятельности, кроме простого "ресурса" управление "произойдет в деструкторе.
TComponent.Notification() является ключевым примером такой вещи. В этом случай, правильный способ" распоряжаться" компонентом - использовать DisposeOf. Производная TComponent обычно не является временным экземпляром, скорее более долгоживущий объект, который также окружен всей системой другие экземпляры компонентов, которые составляют такие вещи, как формы, фреймы и datamodules. В этом случае целесообразно использовать DisposeOf.
Как работает DisposeOf
Чтобы лучше понять, что именно происходит при вызове DisposeOf
, необходимо знать, как работает процесс уничтожения объектов Delphi.
Существуют три различных этапа, связанных с выпуском объекта как в компиляторах ARC, так и не в ARC Delphi.
- вызов
destructor Destroy
методов цепочка - очистка объектов, управляемых полей - строк, интерфейсов, динамических массивов (в компиляторе ARC, который также включает ссылки на обычные объекты)
- освобождение памяти объекта из кучи
Освобождение объекта с компиляторами, отличными от ARC
Component.Free
→ немедленное выполнение этапов 1 -> 2 -> 3
Освобождение объекта с помощью компиляторов ARC
-
Component.Free
илиComponent := nil
→ уменьшает количество ссылок на объекты, а затем a) или b)- a), если счетчик ссылок на объекты равен 0 → немедленное выполнение этапов
1 -> 2 -> 3
- b), если количество ссылок на объекты больше 0, ничего не происходит
- a), если счетчик ссылок на объекты равен 0 → немедленное выполнение этапов
-
Component.DisposeOf
→ немедленное выполнение этапа1
, этапы2
и3
будут выполнены позже, когда количество ссылок на объекты достигнет 0.DisposeOf
не уменьшает количество ссылок на вызов.
Система уведомлений TComponent
Механизм TComponent
Free Notification
уведомляет зарегистрированные компоненты о том, что экземпляр конкретного компонента освобождается. Оповещаемые компоненты могут обрабатывать это уведомление внутри виртуального Notification
метода и следить за тем, чтобы они очищали все ссылки, которые они могут удерживать на уничтожаемом компоненте.
В компиляторах, отличных от ARC, этот механизм гарантирует, что вы не получите оборванных указателей, указывающих на недействительные выпущенные объекты, а в компиляторах ARC очистка ссылок на уничтожающий компонент уменьшит его количество ссылок и сломает сильные ссылочные циклы.
Механизм Free Notification
запускается в TComponent
деструкторе и без DisposeOf
и прямое выполнение деструктора, два компонента могут содержать сильные ссылки друг на друга, сохраняя себя живыми в течение всего срока службы приложения.
FFreeNotifies
список, который содержит список компонентов, заинтересованных в уведомлении, объявляется как FFreeNotifies: TList<TComponent>
, и он будет хранить сильную ссылку на любой зарегистрированный компонент.
Итак, если у вас есть TEdit
и TPopupMenu
в вашей форме и назначить это всплывающее меню для редактирования свойства PopupMenu
, редактирование будет содержать сильную ссылку на всплывающее меню в поле FEditPopupMenu
, а всплывающее меню будет сильная ссылка на редактирование в его списке FFreeNotifies
. Если вы хотите выпустить любой из этих двух компонентов, вы должны называть DisposeOf
на них или они просто будут продолжать существовать.
Пока вы можете попытаться отслеживать эти соединения вручную и разрывать сильные ссылочные циклы, прежде чем выпускать любой из этих объектов, которые на практике не так просто сделать.
Следующий код в основном приведет к утечке обоих компонентов в ARC, потому что они будут содержать сильную ссылку друг на друга, а после завершения процедуры вы больше не будете иметь внешних ссылок, указывающих на один из этих компонентов. Однако, если вы замените Menu.Free
на Menu.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;
Ловушки 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;
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;
обходной путь
destructor TFoo.Destroy;
begin
s := '';
d := nil;
o := nil;
inherited;
end;
Комбинированный эффект вышеуказанных проблем может проявляться разными способами. Из-за хранения более выделенной памяти, чем необходимо, чтобы трудно поймать ошибки, вызванные неправильным, неожиданным количеством ссылок на содержащиеся ссылки на объекты и интерфейсы, не принадлежащие им.
Так как 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;
Над кодом будет утечка как дочерних, так и родительских объектов. В сочетании с тем, что DisposeOf
не очищает внутренние управляемые типы (включая строки), эти утечки могут быть огромными в зависимости от того, какие данные вы храните внутри. Единственный (правильный) способ разбить этот цикл - это изменить объявление класса TChild
:
TChild = class(TObject)
public
[weak] var Parent: TParent;
constructor Create(AParent: TParent);
end;