Automatische Referenzzählung in mobilen Delphi-Compilern

Aus RAD Studio
Wechseln zu: Navigation, Suche

Nach oben zu Gesichtspunkte für geräteübergreifende Delphi-Anwendungen


Delphi-Benutzer sind seit langem mit dem Begriff der automatischen Referenzzählung (ARC, Automatic Reference Counting) vertraut. In der Vergangenheit haben die Delphi-Desktop-Compiler (DCC32, DCC64, DCCOSX) ARC für Interfaces (in Delphi 3 eingeführt), dynamische Arrays und Strings (AnsiString wurde in Delphi 2 eingeführt) unterstützt. Jetzt wurde für die mobilen Delphi-Compiler die automatische Referenzzählung auch für Klassen eingeführt. In diesem Zusammenhang wurde auch der Begriff der schwachen Referenz eingeführt, um "Zyklen" gemeinsam mit einem anderen Feature zu verwalten: Operatorüberladung für Klassen.

Eine detaillierte Beschreibung der automatischen Referenzzählung finden Sie unter Release Notes for ARC (Apple Computers)

Automatische Referenzzählung

Die Automatische Referenzzählung ist eine Möglichkeit zur Verwaltung der Objektlebensdauer ohne Freigabe des Objekts, wenn es nicht mehr benötigt wird. Ein gutes Beispiel dafür ist eine lokale Variable, die ein Objekt referenziert, das den Gültigkeitsbereich verlässt. Daran anschließend wird das Objekt automatisch freigegeben. Wie oben erwähnt, hat Delphi immer schon die Referenzzählung für Strings und durch Variablen vom Typ Interface referenzierte Objekte unterstützt. ARC bietet aber in den mobilen Delphi -Compilern eine gewisse Flexibilität, um Probleme, wie zirkuläre Referenzen, zu beheben, die mit Variablen vom Typ Interface schwer zu handhaben sind.

Ein weiterer in den neuen mobilen Delphi-Compilern eingeführter Begriff ist schwache Referenz. Damit werden auch einige Probleme mit zirkulären Referenzen behoben.

Das Speicherverwaltungsschema ist dasselbe für Apps, die für iOS-Geräte oder den iOS-Simulator compiliert werden:

  • DCCIOSARM (der Delphi-Compiler für das 32-Bit-iOS-Gerät) verfügt standardmäßig über ARC (Automatic Reference Counting).
  • DCCIOSARM64 (der Delphi-Compiler für das 64-Bit-iOS-Gerät) verfügt standardmäßig über ARC (Automatic Reference Counting).
  • DCCIOS32 (der Delphi-Compiler für den iOS-Simulator) aktiviert ARC auch, obwohl DCCIOS32 auf der herkömmlichen Compiler-Architektur basiert.

Hinweis: Diese neuen Features erscheinen im Quellcode von RAD Studio ausschließlich in den konditionalen {$AUTOREFCOUNT}-Blöcken in der RTL (z. B. in System.pas).

Änderungen des Programmierstils

Da die neuen mobilen Compiler die automatische Referenzzählung unterstützen, kann beim Verweisen auf temporäre Objekte in einer Methode der Code deutlich vereinfacht werden. Die Speicherverwaltung wird vollständig ignoriert:

class procedure TMySimpleClass.CreateOnly;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  MyObj.DoSomething;
end;

Im obigen Beispiel wird der Destruktor für das Objekt aufgerufen, wenn das Programm die end-Anweisung erreicht, das heißt, wenn die Variable MyObj den Gültigkeitsbereich verlässt.

Die Verwendung des Objekts kann auch vor dem Ende der Methode beendet werden. Dies kann durch Setzen der Variable auf nil erreicht werden:

class procedure TMySimpleClass.SetNil;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  MyObj.DoSomething (False); // True => raise
  MyObj := nil;	
  // do something else
end;

In diesem Fall wird das Objekt vor dem Ende der Methode freigegeben und zwar genau dann, wenn die Variable auf nil gesetzt wird. Falls die DoSomething-Prozedur eine Exception auslöst, wird die nil-Zuweisungsanweisung übersprungen. Wird die Methode schließlich beendet, wird auch das Objekt freigegeben.

Da sich die Lebensdauer des Objekts nach dem Programmfluss richtet, können Sie den Referenzzähler eines Objekts (nur auf Plattformen mit Referenzzählung) mit der neuen Eigenschaft RefCount abfragen:

public
    property RefCount: Integer read FRefCount;

Hinweis: Das Abfragen des Referenzzählers eines Objekts wird nicht empfohlen und sollte im Allgemeinen nicht verwendet werden.

Geschwindigkeit von ineinandergreifenden Vorgängen

Das Inkrementieren und Dekrementieren des Referenzzählers eines Objekts wird unter Verwendung von Thread-sicher Operationen durchgeführt, damit der Member der Referenzzählerinstanz Thread-sicher ist. Das bedeutet, dass die Referenzzähler-Instanzvariable korrekt durch Speicherbarrieren abgeschirmt ist, damit die Änderung von allen Threads sofort beachtet wird und sie keinen alten Wert ändern können.

Hinweis: Der ARC-Mechanismus schützt nicht vor Racebedingungen und Deadlocks.

ARC und die Direktive {$AUTOREFCOUNT}

Mit der Direktive {$IFDEF AUTOREFCOUNT} kann unter Umständen der beste Code für beide Szenarien erzielt werden: ARC und traditionell. AUTOREFCOUNT definiert Code, der die automatische Referenzzählung verwendet, wie z. B. Code für die mobilen Delphi-Compiler. Diese Direktive ist sehr wichtig und unterscheidet sich von {$IFDEF NEXTGEN} (der Direktive, die die neuen Sprach-Features der mobilen Compiler definiert). AUTOREFCOUNT könnte zukünftig nützlich werden, für den Fall, dass ARC auch für die Delphi-Desktop-Compiler implementiert wird.

Die Methoden Free und DisposeOf unter ARC

Bei Klassen sind Delphi-Entwickler ein anderes Programmiermuster auf Basis der Methode Free und geschützt durch den try-finally-Block gewöhnt.

Zum Beispiel:

class procedure TMySimpleClass.TryFinally;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  try
    MyObj.DoSomething;
  finally
    MyObj.Free;
  end;
end;

Bei den Delphi-Desktop-Compilern ist Free eine Methode von TObject, die überprüft, ob die aktuelle Referenz nicht nil ist, und in diesem Fall den Destroy-Destruktor aufruft, der das Objekt nach Ausführung des entsprechenden Destruktorcodes aus dem Speicher entfernt.

In den neuen mobilen Delphi-Compilern wird der Aufruf von Free durch die Zuweisung von nil zu der Variable ersetzt. Falls es sich dabei um die letzte Referenz auf das Objekt handelt, wird es weiterhin nach dem Aufruf des Destruktors aus dem Speicher entfernt. Falls es noch bestehende Referenzen gibt, passiert nichts (außer einer Verringerung des Referenzzählers).

FreeAndNil (MyObj);

Ebenso setzt ein Aufruf wie der obige das Objekt auf nil und löst wieder die Objektfreigabe nur dann aus, wenn keine anderen Variablen das Objekt referenzieren. In den meisten Fällen ist das korrekt, weil keine Objekte freigegeben werden sollen, die in anderen Programmteilen verwendet werden. Andererseits gibt es Szenarien, in denen der Destruktorcode sofort ausgeführt werden muss, unabhängig davon, ob andere ausstehende Referenzen auf das Objekt vorhanden sind. Damit Entwickler die Ausführung des Destruktors (ohne Freigabe des eigentlichen Objekts aus dem Speicher) erzwingen können, führen die neuen Compiler ein Freigabemuster ein:

MyObject.DisposeOf;

Dieser Aufruf führt den Destruktorcode auch dann aus, wenn ausstehende Referenzen vorhanden sind. Das Objekt wird hierbei in einen speziellen Zustand versetzt, damit der Destruktor bei weiteren Freigabeoperationen oder wenn der Referenzzähler null erreicht, und Speicher tatsächlich freigegeben wird, nicht nochmals aufgerufen wird. Dieser freigegebene Zustand (oder Zombie-Zustand) ist ziemlich wichtig, und Sie können diesen Objektzustand mit der Eigenschaft Disposed abfragen.

Wie weiter oben bereits erwähnt, funktionieren die klassischen try-finally-Blöcke, die mit den Delphi-Desktop-Compiler verwendet werden, auch mit den neuen Compilern, auch wenn sie nicht erforderlich sind. In den meisten Fällen sollten Sie aber den dispose-Programmierstil verwenden (außer Sie möchten den Code für frühere Versionen von Delphi compilieren).

Im folgenden Beispiel wird derselbe Effekt wie mit dem klassischen Compiler erzielt, weil DisposeOf Free aufruft. Der Code führt wie erwartet den Destruktor aus (zur selben Zeit wie der klassische Compiler, aber der Speicher wird vom ARC-Mechanismus verwaltet).

var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  try
    MyObj.DoSomething;
  finally
    MyObj.DisposeOf;
  end;
end;

Hinweis: Die Speicherung des Disposed-Flags wird durch ein Bit im Feld FRefCount erreicht, unter Berücksichtigung, dass die Obergrenze der Referenzen für ein Objekt bei 230 liegt.

Erstellungs- und Freigabemethoden für TObject unter ARC

Die folgenden Abschnitte enthalten eine Übersicht über die Methoden in der Klasse TObject in Bezug auf Erstellung und Freigabe.

Die Unterschiede zwischen Free und DisposeOf können auch unter Berücksichtigung des Zwecks betrachtet werden. Free wird verwendet, um eine bestimmte Referenz von einer Instanz zu trennen. Dabei ist eine Freigabe des Objekts oder von Speicher nicht impliziert. Im Gegensatz dazu teilt der Programmierer mit DisposeOf der Instanz explizit mit, "sich selbst zu bereinigen". Eine Speicherfreigabe ist dabei nicht notwendigerweise impliziert, DisposeOf führt eine explizite "Vorbereinigung" der Instanz durch. Anschließend ist die Instanz von der normalen Referenzzählungssemantik abhängig, die die Instanz dann schließlich freigeben kann.

Schwache Referenzen

Ein weiteres wichtiges Konzept für ARC ist die Rolle von schwachen Referenzen. Sie erstellen schwache Referenzen, indem Sie Referenzen mit dem Attribut [weak] versehen. Angenommen, zwei Objekte referenzieren sich gegenseitig über ein Feld, und eine externe Variable referenziert das erste Objekt. Der Referenzzähler des ersten Objekts ist 2 (die externe Variable und das zweite Objekt) und der Referenzzähler des zweiten Objekts ist 1. Wenn die externe Variable den Gültigkeitsbereich verlässt, haben beide Objekte einen Referenzzähler von 1, und sie verbleiben unendlich im Speicher.

Um diese Situation und viele ähnlichen Szenarien zu lösen, wird eine schwache Referenz verwendet, um die zirkulären Referenzen aufzuheben, wenn die letzte externe Referenz den Gültigkeitsbereich verlässt.

Eine schwache Referenz ist eine Referenz auf ein Objekt, dessen Referenzzähler nicht erhöht wird. Eine schwache Referenz wird mit dem Attribut [weak] deklariert, das von den mobilen Delphi-Compilern unterstützt wird.

Wenn in dem vorherigen Szenario die Referenz von dem zweiten Objekt auf das erste Objekt schwach ist, werden – sobald die externe Variable den Gültigkeitsbereich verlässt – beide Objekte freigegeben. Hier ist ein Beispiel für diese Situation:

type
  TMyComplexClass = class;

  TMySimpleClass = class
  private
    [Weak] FOwnedBy: TMyComplexClass;
  public
    constructor Create();
    destructor Destroy (); override;
    procedure DoSomething(bRaise: Boolean = False);
  end;

  TMyComplexClass = class
  private
    fSimple: TMySimpleClass;
  public
    constructor Create();
    destructor Destroy (); override;
    class procedure CreateOnly;
  end;

Im folgenden Codefragment erstellt der Konstruktor der komplexen Klasse ein Objekt der anderen Klasse:

constructor TMyComplexClass.Create;
begin
  inherited Create;
  FSimple := TMySimpleClass.Create;
  FSimple.FOwnedBy := self;
end;

Weil das Feld FOwnedBy eine schwache Referenz ist, wird der Referenzzähler des Objekts, auf das es verweist – in diesem Fall auf das aktuelle Objekt (self) – nicht erhöht.

Daher können Sie für diese Klassenstruktur Folgendes schreiben:

class procedure TMyComplexClass.CreateOnly;
var
  MyComplex: TMyComplexClass;
begin
  MyComplex := TMyComplexClass.Create;
  MyComplex.fSimple.DoSomething;
end;

Dieser Code verursacht kein Speicherleck, vorausgesetzt, dass die schwache Referenz richtig angewendet wird.

Ein weiteres Beispiel für die Verwendung von schwachen Referenzen ist dieses Codefragment aus der Delphi-RTL, das Teil der Klassendeklaration von TComponent ist:

type
  TComponent = class(TPersistent, IInterface,
    IInterfaceComponentReference)
  private
    [Weak] FOwner: TComponent;

Wenn Sie das Attribut weak in Code angeben, der von einem der Delphi-Desktop-Compiler compiliert wird, dann wird das Attribut ignoriert. Für DCC32 müssen Sie sicherstellen, dass Sie den korrekten Code im Destruktor eines "Eigentümer"-Objekts angeben, damit auch das Objekt, das der Eigentümer "besitzt", freigegeben wird. Der Aufruf von Free ist zulässig, obwohl damit ein anderer Effekt als in den mobilen Delphi-Compilern erreicht wird. Das Verhalten ist in den meisten Fällen korrekt.

Hinweis: Wenn für eine Instanz der Speicher freigegeben wird, werden alle aktiven [weak]-Referenzen auf nil gesetzt. Genau wie bei starken (normalen) Referenzen kann eine [weak]-Variable nur nil sein oder eine gültige Instanz referenzieren. Das ist der Hauptgrund, warum eine schwache Referenz vor dem Testen auf nil und dem Dereferenzieren einer starken Referenz zugewiesen werden soll. Durch Zuweisung zu einer starken Referenz kann die Instanz nicht vorzeitig freigegeben werden.

var
  O: TComponent;// a strong reference
begin
  O := FOwner;  // assigning a weak reference to a strong reference
  if O <> nil then
    O.<method>;// safe to dereference
end;

Hinweis: Eine [weak]-Variable kann nur einem var- oder out-Parameter übergeben werden, der auch mit [weak] gekennzeichnet ist. Eine normale starke Referenz kann keinem [weak] var- oder out-Parameter übergeben werden.

Das Attribut "Unsafe"

Verwenden Sie die folgende Syntax, um das Attribut unsafe für eine Funktion festzulegen:

[Result: Unsafe] function ReturnUnsafe: TObject;

Warnung: [Unsafe] kann auch für Variablen (Member) und Parameter verwendet werden. Es sollte außerhalb der Unit System nur in sehr seltenen Situationen verwendet werden. Es gilt als gefährlich, und die Verwendung von [Unsafe] wird nicht empfohlen, weil keine Referenzzählung generiert wird.

Hinweis: Eine [Unsafe]-Variable kann nur einem var- oder out-Parameter übergeben werden, der auch mit [Unsafe] gekennzeichnet ist. Eine normale starke Referenz kann keinem [Unsafe] var- oder out-Parameter übergeben werden.

Operatorüberladung für Klassen

Ein interessanter Nebeneffekt der ARC-Speicherverwaltung ist, dass der Compiler die Lebensdauer temporärer von Funktionen zurückgegebener Objekte handhaben kann. Einen speziellen Fall stellen temporäre von Operatoren zurückgegebene Objekte dar. Ein ganz neues Feature der neuen Delphi-Compiler ist die Möglichkeit, Operatoren für Klassen zu definieren, mit derselben Syntax und demselben Modell, das für Records seit Delphi 2006 verfügbar ist.

Hinweis: Das folgende Codebeispiel funktioniert für die mobilen Delphi-Compiler (iOS), kann aber nicht von den Delphi-Desktop-Compilern compiliert werden.

Als Beispiel dient die folgende einfache Klasse:

type
  TNumber = class
  private
    FValue: Integer;
    procedure SetValue(const Value: Integer);
  public
    property Value: Integer read FValue write SetValue;
    constructor Create(Value: Integer);
    function ToString: string; override;
    class operator Add (a, b: TNumber): TNumber;
    class operator Implicit (n: TNumber): Integer;
    class operator Implicit(n: Integer): TNumber;
  end;

constructor TNumber.Create(Value: Integer); begin
  inherited Create;
  Self.Value := Value;
end;

class operator TNumber.Add(a, b: TNumber): TNumber; begin
  Result := TNumber.Create(a.Value + b.Value); end;

class operator TNumber.Implicit (n: TNumber): Integer; begin
  Result := n.Value;
end;

class operator TNumber.Implicit (n: Integer): TNumber; begin
  Result := TNumber.Create(n);
end;

procedure TNumber.SetValue(const Value: Integer); begin
  FValue := Value;
end;

function TNumber.ToString: string;
begin
  Result := IntToStr(Value);
end;

procedure Test;
var
  a, b, c: TNumber;
begin
  a := 10;
  b := 20;
  c := a + b;
  Writeln(c.ToString);
end;

Siehe auch

Codebeispiel