Delphi - с интерфейсом внутри интерфейса я получаю утечку памяти, но не знаю, почему

Моя цель - получить интерфейс настроек, в котором хранится информация о моих настройках приложения. Это может быть информация, которая актуальна только во время работы приложения, и информация, которая должна быть постоянной. Для этого я подумал, что было бы неплохо создать интерфейс приложения и интерфейс постоянных настроек внутри этого интерфейса настроек приложения, чтобы я мог переключаться между, скажем, Ini-файлом, БД, зашифрованным файлом, чем угодно.... потом. Код работает, я думаю. Но при закрытии FastMM оставляет меня с утечкой памяти в TiniFile, и я не знаю, почему. Вот блок интерфейса с классами.

unit UAPPSettings;

interface

uses
  IniFiles;

type
  IPersistentSettings = Interface
    ['{11542859-DEA3-4DBB-9A88-2068E407552C}']
    function ReadString(Section, Name, Default: String): string;
    procedure WriteString(Section, Name, Default: String);
    function ReadPassword: string;
    procedure WritePassword(Password: String);
  end;

type
  IAppSettings = Interface
    ['{559B8219-7D81-44CA-87A0-6D261B4A87E7}']
    function GetPersistentSettings: IPersistentSettings;
    property PersistentSettings: IPersistentSettings read GetPersistentSettings;
  End;

type
  TAppSettings = class(TInterfacedObject, IAppSettings)
  strict private
    FPersistentSettings: IPersistentSettings;
    function GetPersistentSettings: IPersistentSettings;
    procedure SetPersistentSettings(const Value: IPersistentSettings);
    property PersistentSettings: IPersistentSettings read GetPersistentSettings write SetPersistentSettings;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  public
    constructor create(aPersistentSettings: IPersistentSettings);
  end;

type
  TIniSettings = class(TInterfacedObject, IPersistentSettings)
  strict private
    FIniFile: TIniFile;
    FfilePath: String;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    function ReadString(Section, Name, Default: String): string;
    procedure WriteString(Section, Name, Default: String);
    function ReadPassword: string;
    procedure WritePassword(Password: string);
  public
    constructor create(FilePath: String);

  end;

  var
  APPSettings: IAppSettings;

implementation

uses
  FMX.Types;



  { TIniSettings }

constructor TIniSettings.create(FilePath: String);
begin
  FfilePath := FilePath;
end;

function TIniSettings.ReadPassword: string;
begin
  Result := ReadString('App', 'Passwort', '');
end;

function TIniSettings.ReadString(Section, Name, Default: String): string;
begin
  If FIniFile.ValueExists(Section, Name) = false then
    WriteString(Section, Name, Default);
  Result := FIniFile.ReadString(Section, Name, Default);
end;

procedure TIniSettings.WritePassword(Password: string);
begin
  WriteString('App', 'Passwort', Password);
end;

procedure TIniSettings.WriteString(Section, Name, Default: String);
begin
  FIniFile.WriteString(Section, Name, Default);
end;

function TIniSettings._AddRef: Integer;
begin
  log.d('IniSettings _AddRef');
  FIniFile := TIniFile.create(FfilePath);
  Result := inherited _AddRef;
end;

function TIniSettings._Release: Integer;
begin
  log.d('IniSettings _Release');
  Result := inherited _Release;
  FIniFile.Free;
  FIniFile := nil;
end;

{ TAppSettings }

constructor TAppSettings.create(aPersistentSettings: IPersistentSettings);
begin
  FPersistentSettings := aPersistentSettings;
end;

function TAppSettings.GetPersistentSettings: IPersistentSettings;
begin
  Result := FPersistentSettings;
end;

procedure TAppSettings.SetPersistentSettings(const Value: IPersistentSettings);
begin
  FPersistentSettings := Value;
end;

function TAppSettings._AddRef: Integer;
begin
  log.d('AppSettings _AddRef');
  Result := inherited _AddRef;
end;

function TAppSettings._Release: Integer;
begin
  log.d('AppSettings _Release');
  Result := inherited _Release;
  FPersistentSettings := nil;
end;

end.

Вот как я создаю объект:

APPSettings := TAppSettings.create(TIniSettings.create(System.IOUtils.TPath.GetDocumentsPath + System.SysUtils.PathDelim + 'config.ini'));

Любые предложения по улучшению кодирования также очень ценятся

Почему бы вам не поместить обработку памяти TIniFile в классы constructor и destructor?

Delphi Coder 20.06.2023 13:32

Подумайте о том, что происходит в TIniSettings._AddRef, когда счетчик ссылок становится равным 2. А затем делайте то, что сказал @DelphiCoder.

David Heffernan 20.06.2023 14:46
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
2
94
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема в вашем коде заключается в том, что вы используете методы _AddRef и _Release для целей, отличных от управления памятью экземпляра, для которого вызываются эти два метода.

Следующий код является причиной проблемы:

function TIniSettings._AddRef: Integer;
begin
  log.d('IniSettings _AddRef');
  FIniFile := TIniFile.create(FfilePath);
  Result := inherited _AddRef;
end;

function TIniSettings._Release: Integer;
begin
  log.d('IniSettings _Release');
  Result := inherited _Release;
  FIniFile.Free;
  FIniFile := nil;
end;

Вы создаете экземпляр FIniFile в методе _AddRef, и это не то место для этого, а _Release — неправильное место для освобождения FIniFile.

Методы _AddRef и _Release можно вызывать несколько раз в течение жизни экземпляра, и каждый вызов _AddRef приведет к созданию нового экземпляра TIniFile.

Для правильного управления памятью эти два метода будут вызываться одинаковое количество раз, но возможно, что _AddRef будет вызываться несколько раз подряд, а затем несколько вызовов _Release последуют в какой-то момент в будущем в зависимости от кода.

Другими словами, также возможны следующие последовательности:

_AddRef
_AddRef
_Release
_Release

или

_AddRef
_AddRef
_Release
_AddRef
_Release
_Release

Когда вы вызываете APPSettings := TAppSettings.create(TIniSettings.create(..., сначала будет вызываться _AddRef при передаче экземпляра в качестве параметра aPersistentSettings, потому что он не объявлен как константа. Затем будет вызван следующий _AddRef при назначении aPersistentSettings на FPersistentSettings, и этот второй вызов создаст утечку экземпляра TIniFile.

В конце вызова конструктора будет соответствующий _Release вызов для aPersistentSettings вызова параметра _AddRef. Это также освободит ini-файл, и FIniFile будет nil и непригодным для использования, пока вы снова не активируете подсчет ссылок на экземпляр FPersistentSettings.


Вы могли бы создать FIniFile либо в конструкторе TIniSettings и уничтожить его в его деструкторе, либо ввести дополнительные методы (Open/Close) в TIniSettings, которые будут служить этой цели, если вы не хотите, чтобы этот ini-файл создавался все время, и вы бы вызывайте их вокруг любых операций чтения или записи.

И когда вы удаляете код, связанный с ini-файлом, из _AddRef и _Release, вам больше не нужно реализовывать их в своих классах, поскольку реализация по умолчанию сделает свою работу.


Существует дополнительный способ исправить код, создав TIniFile только в том случае, если он не назначен в _AddRef, но методы подсчета ссылок по-прежнему не подходят для таких вещей, поскольку вам нужно убедиться, что вы случайно не запускаете дополнительный подсчет ссылок в коде. неправильное место, которое может оставить вас с нулевой переменной FIniFile, хотя она вам все еще может понадобиться.

function TIniSettings._AddRef: Integer;
begin
  log.d('IniSettings _AddRef');
  if not Assigned(FIniFile) then
    FIniFile := TIniFile.create(FfilePath);
  Result := inherited _AddRef;
end;

Спасибо за этот подробный ответ. Я не знал о разнице между подсчетом ссылок и управлением памятью. Спасибо!

fisi-pjm 21.06.2023 08:54

Подсчет ссылок — это метод управления памятью. Когда счетчик ссылок падает до нуля, экземпляр освобождается. И фактический подсчет ссылок происходит в методах _AddRef и _Release, которые компилятор вставляет в соответствующие места. Если вы проверите реализацию в TInterfacedObject, вы увидите, как она работает. Другой вариант с отключенным управлением памятью можно увидеть в TNoRefCountObject или TComponent. Если подсчет ссылок отключен, экземпляры такого класса необходимо освобождать вручную.

Dalija Prasnikar 21.06.2023 10:10

Другие вопросы по теме