Читаю книгу Delphi Memory Management, делаю небольшой конспект основных моментов. Учился под музыку Emy Care Read my Mind
Automatic Reference Counting – система, в которой использование каждого объекта отслеживается по ссылкам на него. Что это дает? Простоту кодирования. Например, мы можем писать так…
1 2 3 4 5 6 |
var sl: ISmartPointer<TStringList>; begin sl := TSmartPointer<TStringList>.Create(); sl.Add('I am inside automanaged StringList'); end; |
Вместо традиционного…
1 2 3 4 5 6 7 8 9 10 |
var sl: TStringList; begin sl := TStringList.Create; try sl.Add('I am inside regular StringList'); finally sl.Free; end; end; |
Но когда же уничтожается объект? В момент, когда число ссылок на него станет равным нулю.
Золотое правило ARC
Ссылка на объект или интерфейс должна либо быть равна nil, либо указывать на существующий объект в памяти
Если мы пишем через интерфейсы, то в момент входа в контекст ссылка будет равна nil, например
1 2 3 4 5 |
var calc: ICalc; ... // calc is nil here calc := TCalc.Create; caption := calc.Add(1, 2).ToString(); |
Виды ссылок
Сильные – участвуют в подсчете ссылок на объект
Слабые – не участвуют в подсчете ссылок на объект.
Виды слабых ссылок
[weak] – не участвуют в подсчете ссылок (ARC), когда объект уничтожается, ссылка обнуляется. Поэтому у нас нет “висящих ссылок, указывающих в никуда”. Система следит за этими ссылками и обнуляет их автоматически. Когда этих ссылок много – может возникнуть проблема производительности.
[unsafe] – не участвуют в подсчете ссылок (ARC), когда объект уничтожается, ссылка НЕ обнуляется. Поэтому потенциально возможна проблема “висящих ссылок”
Pointer – начиная с Delphi 10.1 Berlin, можно использовать указатели, они будут работать также как [unsafe], но нужны также ещё касты…
Параметры, отмеченные как const или [ref], также не будут участвовать в механизме подсчета ссылок (ARC).
Каждый объект должен иметь хотя бы одну сильную ссылку, чтобы существовать, иначе он будет уничтожен.
Циклы сильных ссылок
Вот простой пример. На Bar у нас 2 ссылки, поэтому он не уничтожится нормальным образом через ARC. Нужно такие циклы распознавать и разбивать слабыми ссылками. Цепочки зависимостей могут быть самыми разными, но в целом картинка выглядит так, что ссылки в ARC работают правильно, если они выстроены как дерево, а не граф. В общем, никаких обратных сильных ссылок, сильные ссылки только вперед. Если нужна ссылка обратно, делаем ее слабой, то есть так…
Другой способ разбить ссылку занилить таким образом
1 2 |
Bar := nil; FooBar := nil; |
Классический пример цикла и его разбиения в ARC через классы Parent Child
Тоже самое в коде
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
type TChild = class; TParent = class(TObject) public var Child: TChild; constructor Create; destructor Destroy; override; end; TChild = class(TObject) public var Parent: TParent; constructor Create(AParent: TParent); destructor Destroy; override; end; constructor TParent.Create; begin Writeln('Parent constructor'); inherited; Child := TChild.Create(Self); end; destructor TParent.Destroy; begin Child.Free; inherited; Writeln('Parent destructor'); end; constructor TChild.Create(AParent: TParent); begin Writeln('Child constructor'); inherited Create; Parent := AParent; end; destructor TChild.Destroy; begin inherited; Writeln('Child destructor'); end; procedure Test; var Root: TParent; begin Root := TParent.Create; Root.Free; end; begin Test; end. |
Такой код хорошо отработает в ручном режиме управления памятью,
1 2 3 4 |
Parent constructor Child constructor Child destructor Parent destructor |
но ARC компилятор даст утечку памяти.
1 2 |
Parent constructor Child constructor |
Тоже самое через интерфейсы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
type IParent = interface end; IChild = interface end; TParent = class(TInterfacedObject, IParent) public var Child: IChild; constructor Create; destructor Destroy; override; end; TChild = class(TInterfacedObject, IChild) public var Parent: IParent; constructor Create(const AParent: IParent); destructor Destroy; override; end; constructor TParent.Create; begin Writeln('Parent constructor'); inherited; Child := TChild.Create(Self); end; destructor TParent.Destroy; begin inherited; Writeln('Parent destructor'); end; constructor TChild.Create(const AParent: IParent); begin Writeln('Child constructor'); inherited Create; Parent := AParent; end; destructor TChild.Destroy; begin inherited; Writeln('Child destructor'); end; procedure Test; var Root: IParent; begin Root := TParent.Create; Root := nil; end; begin Test; end. |
Разобьем этот цикл следующим образом
В коде это будет выглядеть так
1 2 3 |
TChild = class(TObject) public [weak] var Parent: TParent; |
или так
1 2 3 |
TChild = class(TInterfacedObject, IChild) public [weak] var Parent: IParent; |
Все отработает как нужно…
1 2 3 4 |
Parent constructor Child constructor Parent destructor Child destructor |
Если у нас Child работает в виде класса, то уничтожать его надо будет ручками, то есть так…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
type IParent = interface end; TChild = class; TParent = class(TInterfacedObject, IParent) public var Child: TChild; constructor Create; destructor Destroy; override; end; TChild = class(TObject) public [weak] var Parent: IParent; constructor Create(const AParent: IParent); destructor Destroy; override; end; constructor TParent.Create; begin Writeln('Parent constructor'); inherited; Child := TChild.Create(Self); end; destructor TParent.Destroy; begin Child.Free; inherited; Writeln('Parent destructor'); end; constructor TChild.Create(const AParent: IParent); begin Writeln('Child constructor'); inherited Create; Parent := AParent; end; destructor TChild.Destroy; begin inherited; Writeln('Child destructor'); end; procedure Test; var Root: IParent; begin Root := TParent.Create; Root := nil; end; begin Test; end. |
Когда использовать [unsafe] и безопасно ли это?
Когда время жизни ссылки меньше или равно времени жизни объекта, на который она указывает. В контексте объектов Parent Child, Parent можно было бы пометить ссылкой [unsafe], при условии, что Parent никогда не переживет Child.
Смешивание интерфейсных ссылок и ссылок на объекты – ать по рукам!
Смешивать их не надо, это может привести к непредсказуемым последствиям.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
type IFoo = interface procedure Foo; end; TFooBar = class(TInterfacedObject, IFoo) public procedure Foo; procedure Bar; end; procedure CanMix; var Foo: IFoo; FooBar: TFooBar; begin // Reference counted class must be stored in interface reference // after construction. Foo := TFooBar.Create; Foo.Foo; // Cast interface reference to temporary object instance // for accessing other members. FooBar := TFooBar(Foo); FooBar.Bar; // WRONG - don't explicitly release reference counted instance // it will be managed automatically through Foo reference. FooBar.Free; // Assigning nil to interface reference will release the object instance // before it goes out of scope assuming there are no other strong // references to that instance. FooBar is weak reference. // FooBar represents stale pointer at this point and should not // be used afterwards Foo := nil; // WRONG - object instance is already gone FooBar.Bar; end; |
Другой пример – метод System.SysUtils.Supports
Здесь мы создаем экземпляр класса, и далее пытаемся проверить поддерживает ли он интерфейс, а метод Supports запускает механизм ARC и уничтожает объект.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
type TFoo = class(TInterfacedObject) public destructor Destroy; override; end; destructor TFoo.Destroy; begin Writeln('Foo is gone'); inherited; end; var Foo: TFoo; begin Foo := TFoo.Create; if Supports(Foo, IInterface) then begin end; Writeln('Foo must be alive here'); // Foo.Free; // this would crash end. |
Если мы объявим Foo через интерфейс, то все получится как нужно.
1 2 |
var Foo: IInterface; |
Отключение ARC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
type TNonManaged = class(TObject, IInterface) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end; function TNonManaged.QueryInterface(const IID: TGUID; out Obj): HResult; begin if GetInterface(IID, Obj) then Result := 0 else Result := E_NOINTERFACE; end; function TNonManaged. _AddRef: Integer; begin Result := -1; end; function TNonManaged. _Release: Integer; begin Result := -1; end; |
Используем как обычный класс
1 2 3 4 5 6 7 8 9 10 |
var Obj: TNonManaged; begin Obj := TNonManaged.Create; try ... finally Obj.Free; end; end; |
Класс Хамелеон
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
type TManagedObject = class(TObject, IInterface) // rest of the code is the same as in TInterfacedObject protected FManaged: boolean; public constructor Create; overload; constructor Create(AManaged: boolean); overload; end; constructor TManagedObject.Create; begin inherited; FManaged := false; end; constructor TManagedObject.Create(AManaged: boolean); begin inherited Create; FManaged := AManaged; end; function TManagedObject. _AddRef: Integer; begin if FManaged then begin // same code as in TInterfacedObject end else Result := -1; end; function TManagedObject. _Release: Integer; begin if FManaged then begin // same code as in TInterfacedObject end else Result := -1; end; |
Примеры использования
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var Obj: TManagedObject; begin // Reference counting is disabled Obj := TManagedObject.Create; try ... finally Obj.Free; end; end; var Intf: IInterface; begin // Reference counting is enabled // will be released when last strong reference goes out of scope Intf := TManagedObject.Create(true); ... end; |
Утечка памяти при const параметре
1 2 3 4 5 6 7 |
procedure LeakTest(const Intf: IInterface); begin end; begin ReportMemoryLeaksOnShutdown := true; LeakTest(TInterfacedObject.Create); end; |
Возможное решение, которое отложит проблему – убрать параметр const
1 |
procedure LeakTest(Intf: IInterface); |
Ещё одно решение
1 2 3 4 5 6 7 8 9 10 |
procedure LeakTest(const Intf: IInterface); begin end; var Intf: IInterface; begin ReportMemoryLeaksOnShutdown := true; Intf := TInterfacedObject.Create; LeakTest(Intf); end; |
И ещё одно
1 |
LeakTest(TInterfacedObject.Create as IInterface); |
В общем решение в том, чтобы не создавать объекты inline
1 2 |
Don’t construct ob jects inline and always use an additional variable. This is easy to rememb er and easy to follow. |
Коллекции для интерфейсов
IInterfaceList
1 2 3 4 5 6 7 8 9 10 11 |
var List: IInterfaceList Intf: IInterface; begin List := TInterfaceList.Create; Intf := TInterfacedObject.Create; List.Add(Intf); Intf := TInterfacedObject.Create; List.Add(Intf); Writeln(List.Count); end. |
Или так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
uses System.Classes, System.SysUtils; type IFoo = interface ['{E46B52B5-78EA-41AF-9C44-9FD59FA1A7F8}'] procedure Foo; end; IBar = interface ['{180296D2-CCB8-4033-9F47-4D70434A9721}'] procedure Bar; end; TFooBar = class(TInterfacedObject, IFoo, IBar) public procedure Foo; procedure Bar; end; procedure TFooBar.Bar; begin Writeln('Bar'); end; procedure TFooBar.Foo; begin Writeln('Foo'); end; var List: IInterfaceList; Intf: IInterface; Foo: IFoo; begin List := TInterfaceList.Create; Intf := TFooBar.Create; List.Add(Intf); Intf := TFooBar.Create; List.Add(Intf); Intf := TFooBar.Create; List.Add(Intf); Intf := List[0]; (Intf as IFoo).Foo; (Intf as IBar).Bar; if Supports(Intf, IFoo, Foo) then Foo.Foo; end. |
System.Generics.Collections.TList<T>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var List: TList<IFoo>; Intf, Foo: IFoo; begin ReportMemoryLeaksOnShutdown := true; List := TList<IFoo>.Create; try Intf := TFooBar.Create; List.Add(Intf); Intf := TFooBar.Create; List.Add(Intf); Intf := TFooBar.Create; List.Add(Intf); Foo := List[0]; Foo.Foo; finally List.Free; end; end. |