Laskavý čtenář mého blogu jistě už naznal, že Delphi neznamená jen Win32 (Win64, OSX) ale i kompilátory pro iOS a Android.
Tyto kompilátory (kromě kompilace do ARM) přinášejí i podporu ARC (Automatic Reference
Counting) pro objekty (podobně jako k tomu došlo před nedávnem Apple). Princip není v Delphi v podstatě nový - Delphi odjakživa používají stejný mechanismus nejméně pro řetězce a interface.
V podstatě existují dvě možnosti automatického uvolňování paměti - ARC a Garbage Collector, kdy GC je ten nedeterministický (tj. problematický viz Android a nový runtime ART to taky řeší), ale to sem asi nepatří.
Obecně ale oba modely musí řešit cyklické reference. Takže ARC je moc pěkné, ale není vše tak sluníčkové jak by mohlo vypadat. V nejhorším ale jen nedojde k uvolnění, pokud se tomu chceme vyhnout, je nutno jít trochu hlouběji. Upozorňuji, že právě následující text jde v některých detailech hlouběji než je nutné pro normálního programátora.
Pojďme ale od začátku. Záleží na tom, zda budete psát nový kód nebo migrovat starší. V prvém případě si stačí uvědomit několik věcí a není co řešit. Horší situace je s úpravou staršího kódu, který je sice momentálně bezchybný, ale čert nikdy nespí a některé reference se nemusí uvolnit.
Takže v ARC to funguje následovně: Vytvoříte objekt a používáte ho. V případě, že zmizí z viditelnosti, jeho počítadlo referencí se sníží a pokud je 0 objekt je uvolněn. Nesmíte volat destructor přímo (píši destruktor! ne Free!), jelikož se tím dostáváte do interference s ARC (uvolníte objekt a v okamžiku kdy se dostanete mimo viditelnost by se ARC snažilo o uvolnění).
Klasický pattern pro Delphi objekty je
var
obj: TMyObject;
…
obj := TMyObject.Create;
try
….
finally
obj.Free; // nebo FreeAndNil(obj);
end;
V ARC můžete klidně try..finally + Free vynechat a objekt bude uvolněn dle dříve uvedeného pravidla. Problém je, pokud sdílíte kód s ne-ARC Delphi kompilátorem. Delphi v případě ARC kompilátoru přeloží volání Free jako přiřazení nil a ve většině případů to bude stačit, jelikož se tím sníží počet referencí a dojde k uvolnění objektu. V ne-ARC Delphi to bude fungovat jako předtím.
Pro dokreslení kousek zdrojového kódu:
procedure TObject.Free;
begin
// under ARC, this method isn't actually called since the compiler translates
// the call to be a mere nil assignment to the instance variable, which then calls _InstClear
{$IFNDEF AUTOREFCOUNT}
if Self <> nil then
Destroy;
{$ENDIF}
end;
procedure TObject.DisposeOf;
type
TDestructorProc = procedure (Instance: Pointer; OuterMost: ShortInt);
begin
{$IFDEF AUTOREFCOUNT}
if Self <> nil then
begin
Self.__ObjAddRef; // Ensure the instance remains alive throughout the disposal process
try
if __SetDisposed(Self) then
begin
_BeforeDestruction(Self, 1);
TDestructorProc(PPointer(PByte(PPointer(Self)^) + vmtDestroy)^)(Self, 0);
end;
finally
Self.__ObjRelease; // This will deallocate the instance if the above process cleared all other references.
end;
end;
{$ELSE}
Free;
{$ENDIF}
end;
Problémem je něco co se nazývá vazba rodič - dítě, resp. okamžik kdy dítě drží referenci na rodiče. V tomto případě
by objekt žil navždy. Je nutno jednu z těchto referencí jako WEAK. Tyto reference nejsou počítány a dojde ke správnému uvolnění.
TParent = class
public
Child: TChild;
end;
TChild = class
public
[Weak] Parent: TParent;
constructor Create(aParent: TParent);
end;
constructor TChild.Create(aParent: TParent);
begin
inherited;
Parent := aParent;
end;
var
p: TParent;
begin
p := TParent.Create;
p.Child := TChild.Create(p);
end; // <- p je mimo viditelnost (out of scope)
V uvedeném kódu v okamžiku kdy p zmizí s viditelnosti, destructor bude zavolán. V případě, že se atribut weak odstraní, tak ve třídě TChild.Parent by byla
ještě držena reference (tj. reference count = 1) a objekt není uvolněn.
Takže jasné definování weak a normální reference je důležité.
Ale, jelikož zde vstupuje do hry historie Delphi, např. TComponent
notification system. Prostě pro starší kód označení Weak nemusí stačit.
A proto byla přidána metoda DisposeOf, která vyvolá destrutor explicitně.
Příkladem je TForm.
var
f: TMyForm;
begin
f := TMyForm.Create(nil);
f.ShowModal;
end; // 1. <- f je out of scope
nebo
begin
f := TMyForm.Create(nil);
try
f.ShowModal;
finally
f.Free; // 2. <- call destructor
end;
end;
Upozorňuji, že každý Form má referenci v globální proměnné Screen.Forms.
V ne=ARC Delphi první případ je instance držena objektem Screen.Forms během celé života aplikace, v druhém Free vyvolá zmíněný notifikační systém, který v důsledku odstraní referenci ze Screen.Forms. Vše OK.
V ARC Delphi v prvním případě nedojde k uvolnění objektu (referenci drží Screen.Forms), v druhém případě Free je nahrazeno nil (ale reference je držena Screen.Forms), tj. destructor není vyvolán.
Řešení: manuálně zavolat destructor via metody DisposeOf, tím se spustí řetězec notifikací a OK. Takže DisposeOf je takový háček, pro starý kód.
Pozn: DisposeOf je podobné IDisposable z C#, tam je stejný problém, jen je GC vyvoláván až je nejhůře, narozdíl od ARC, kdy je objekt uvolněn hned jak je to možné.
Myslím si (a nejsem sám), že se EMBT snaží postupně toto minimalizovat a nahrazovat konstrukcemi, které jsou pro ARC přirozenější.
Postup konverze starého kódu dle EMBT je:
- identifikovat WEAK reference
- v krizi použít DisposeOf (prakticky by to mělo být minimálně)
Nový kód je samozřejmě něco jiného.
Na závěr ještě alternativní řešení, které samo EMBT používá ve FireDACu (FireDAC.Stan.Util), asi tam těch kruhových referencí je hafo.
{-------------------------------------------------------------------------------}
procedure FDFree(AObj: TObject);
begin
if AObj <> nil then
{$IFDEF AUTOREFCOUNT}
AObj.DisposeOf;
{$ELSE}
AObj.Destroy;
{$ENDIF}
end;
{-------------------------------------------------------------------------------}
procedure FDFreeAndNil(var AObj);
var
p: Pointer;
begin
if Pointer(AObj) <> nil then begin
p := Pointer(AObj);
Pointer(AObj) := nil;
{$IFDEF AUTOREFCOUNT}
if TObject(p).__ObjRelease > 0 then
TObject(p).DisposeOf;
{$ELSE}
TObject(p).Destroy;
{$ENDIF}
end;
end;
Pozn: částečně inspirováno některými poznámkami od Dalija Prasnikar