3 回答

TA貢獻1802條經驗 獲得超5個贊
簡短答案
TComponent在Delphi ARC編譯器(當前為Android和iOS)下釋放任何后代對象時,應遵循兩個規則:
DisposeOf無論對象是否擁有所有者,都必須使用
在析構函數中或在DisposeOf調用后不久引用未超出范圍的情況下,也應將對象引用設置為nil(陷阱詳細說明)
擁有DisposeOfAndNil方法可能會很有吸引力,但是ARC使它比舊FreeAndNil方法復雜得多,我建議使用簡單DisposeOf - nil序列來避免其他問題:
Component.DisposeOf;
Component := nil;
盡管在許多情況下,即使不遵守上述規則,代碼也可以正常運行,但此類代碼相當脆弱,很容易被看似無關的地方引入的其他代碼破壞。
ARC內存管理中的DisposeOf
DisposeOf打破ARC。它違反了ARC的黃金法則。任何對象引用都可以是有效的對象引用,也可以是nil,并且引入了第三種狀態- 放置了“僵尸”對象引用。
任何試圖了解ARC內存管理的人都應該看一下DisposeOf只是解決了Delphi特定框架問題的附件,而不是真正屬于ARC本身的概念。
為什么DisposeOf在Delphi ARC編譯器中存在?
TComponent類(及其所有后代)在設計時考慮了手動內存管理。它使用與ARC內存管理不兼容的通知機制,因為它依賴于破壞析構函數中的強引用周期。由于它TComponent是Delphi框架所依賴的基本類之一,因此它必須能夠在ARC內存管理下正常運行。
除了Free Notification機制之外,Delphi框架中還有其他類似的設計適用于手動內存管理,因為它們依賴于破壞析構函數中的強引用周期,但是這些設計不適合ARC。
DisposeOf方法可以直接調用對象析構函數,并使這些舊代碼可以與ARC一起播放。
這里必須注意一件事。即使在今天編寫了代碼,使用或繼承的任何代碼也將在適當的ARC管理的情況下TComponent自動成為舊代碼。
從艾倫·鮑爾(Allen Bauer)的博客引用進入ARC方面
那么DisoseOf還可以解決什么呢?在各種Delphi框架(包括VCL和FireMonkey)中,將活動的通知或列表管理代碼放在類的構造函數和析構函數中非常普遍。TComponent的所有者/擁有模型是這種設計的關鍵示例。在這種情況下,現有的組件框架設計依賴于許多活動,而不是在析構函數中進行簡單的“資源管理”。
TComponent.Notification()是此類事情的一個關鍵示例。在這種情況下,“處置”組件的正確方法是使用DisposeOf。TComponent派生通常不是瞬態實例,而是一個壽命更長的對象,它也由組成表單,框架和數據模塊之類的其他組件實例的整個系統包圍。在這種情況下,使用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,則什么也沒有發生
Component.DisposeOf->立即執行stage 1,stages,2并且3將在對象引用計數達到0時稍后執行。DisposeOf不會減少調用引用的引用計數。
TComponent通知系統
TComponent Free Notification機制通知注冊的組件特定的組件實例正在釋放。被通知的組件可以在虛擬Notification方法中處理該通知,并確保它們清除了對銷毀的組件可能持有的所有引用。
在非ARC編譯器下,該機制可確保您不會導致指向無效對象(已釋放對象)的懸空指針結束;在ARC編譯器下,清除對銷毀組件的引用將減少其引用計數并破壞強引用周期。
Free Notification在TComponent析構函數中觸發了機制,并且在沒有DisposeOf直接執行析構函數的情況下,兩個組件可以相互保持強引用,從而在整個應用程序生存期內保持自身的生命。
FFreeNotifies包含對通知感興趣的組件列表的list聲明為FFreeNotifies: TList<TComponent>,它將存儲對任何已注冊組件的強引用。
因此,例如,如果您在表單上有TEdit和TPopupMenu并將該彈出菜單分配給edit的PopupMenu屬性,則edit將在其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之外,它本身也是不好的,因為當您破壞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;
- 3 回答
- 0 關注
- 878 瀏覽
添加回答
舉報