Вы здесь

CA-Visual Objects: Рекомендации по удалению использованных объектов из памяти

Не всегда удаётся держать ритм повествования. Видимо, поэтому мой дневник более похож на черновик, чем на «роман в письмах». Я могу прибегнуть к отговоркам про жизненные перипетии, отсутствие времени и прочую чепуху. Но, на то она и жизнь, чтобы мы преодолевали трудности.
 
Получив «втык» от моих товарищей по переписке, за длительное отсутствие новых материалов, я решил открыть свой «долгий ящик», и достать кое-что из заготовок. Думаю, это будет интересным. Наш сегодняшний девиз: «Чистота – залог здоровья!»
 
 
В основе CA-Visual Objects (КаВо) лежит Win32 API, написанный на языке Си. Поэтому с памятью компьютера он работает аналогичным образом. Создавая текст программ, мы используем переменные как фиксированной (заранее известной), так и перманентной длины (меняющейся по ходу исполнения приложения). Для затравки, первый тип – это целочисленные и натуральные, переменные типа «Date», «Logic». А второй – «String», «Array» и др… Классификация задана, остальное – «расфасуете» сами. :)
 
Обычно, переменные первого типа (фиксированной длины) не создают проблем, а вот второй… Имея полную информацию, «владелец» переменной может работать с ней как угодно (в т.ч. – «обнулять», очищая память). Но, если при закрытии, он не «убирается», или потеряет такую возможность (крах объекта) – уборщик ОС, не зная точных параметров задания, может сбойнуть. Память начинает «течь» (занятый объём всё время растёт), а программа - «вылетать».
 
Выход из ситуации – помочь системе очистки Windows. Пойдём по порядку. Все объекты программы описываются классами, у которых есть свой явный (метод «Init») или неявный конструктор. Значит, для решения проблемы с очисткой памяти – надо сделать деструктор, в котором прописать обнуление переменных. (Для простоты, достаточно сбросить только переменные «плавающей» длины). В КаВо его обычно зовут «Destroy»:
 
CLASS Test
   PROTECT cText AS STRING
 
METHOD Destroy() CLASS Test
   SELF:cText := NULL_STRING
   RETURN NIL
 
Если мы хотим самостоятельно управлять процессом закрытия объекта с правильным освобождением памяти от используемых переменных – мы пишем свой деструктор.
 
Давайте разберём частности. Например, надо ли это делать в каждом классе? Нет. Очевидно что, если класс пуст (без переменных) – это пустая трата времени, и если содержит только «фиксированные» переменные – тоже можно ничего не делать. А если у вышеуказанного класса вдруг появится «наследник», как поступать с ним? Если он содержит свои собственные, помимо «родителя», динамические переменные, то для него «Destroy» лучше написать:
 
CLASS Y INHERIT Test
   PROTECT auArray AS ARRAY
 
METHOD Destroy() CLASS Y
   SELF:auArray := NULL_ARRAY
   RETURN ( SUPER:Destroy() )
 
А можно записать вот так?
 
METHOD Destroy() CLASS Y
   SUPER:Destroy()
   SELF:auArray := NULL_ARRAY
   RETURN NIL
 
Можно, но мне первый вариант нравится больше.
 
А можно сократить код и очищать таким способом?
 
METHOD Destroy() CLASS Y
   SELF:auArray := NULL_ARRAY
   RETURN NIL
 
Можно, но так вы разорвёте наследственную цепочку. А это значит, что переменные «родителя» могут не обнулиться. И тут придётся как-то выкручиваться …
 
А если у «папаши» не проводится полная очистка, как, например, здесь:
 
CLASS Test
   PROTECT cText AS STRING
   PROTECT uValue AS USUAL
 
METHOD Destroy() CLASS Test
   SELF:cText := NULL_STRING
   RETURN NIL
 
Можно ли это как-то поправить?
 
Можно. Само правильно – исправить код «родителя» (если у вас есть такая возможность). Но можно поработать и с «сыном»:
 
CLASS Y INHERIT Test
   PROTECT auArray AS ARRAY
 
METHOD Destroy() CLASS Y
   SELF:auArray := NULL_ARRAY
   SELF:uValue := NIL
   RETURN ( SUPER:Destroy() )
 
В этом случае (если правка сделана только у «сына») имеет смысл использовать только «наследника», но не «родителя». Причина, думаю, понятна.
 
 
- Ага, - скажет внимательный читатель, - а что делать, если объект внезапно закрылся или мы не сможем применить метод «Destroy»? Да, такая проблема есть. Чтобы её решить, в CA-Visual Objects придумали регистрацию объектов. Идея безумно проста: при запуске приложения, вместе с ним – запускать собственную систему очистки КаВо. И тогда, для наших «клиентов», эта «прачечная» сможет работать, «как часы».
 
Регистрация объекта обычно делается в методе Init():
 
CLASS Test
 
METHOD Init() CLASS Test
 
   RegisterAxit( SELF )
   RETURN SELF
 
В нужный момент, система проверит, зарегистрирована ли у неё ссылка на этот объект (есть ли у него метод «Axit») и при её наличии, передаст в него вызов. Получив его, мы сможем позаботиться о себе даже в аварийной ситуации. А чтобы не умножать сущности (и не писать же один и тот же код обнуления переменных в «Axit» и «Destroy»), лучше сделать объединение:
 
METHOD Axit() CLASS Test
 
   IF ! InCollect() // Если мы ещё в «мусорном» списке
 
      SELF:Destroy() // Идём на уборку в Destroy()
      UnRegisterAxit( SELF )  // Удаляем регистрацию 
   ENDIF
 
   RETURN NIL
 
Здесь можно сделать маленькую правку для случая, если мы хотим оставить «лазейку» (дающую возможность использовать Axit() по своему усмотрению или при отсутствии регистрации):
 
METHOD Axit() CLASS Test
 
   IF ! InCollect() // Если мы ещё в «мусорном» списке
 
      UnRegisterAxit( SELF )  // Удаляем регистрацию 
   ENDIF
 
   SELF:Destroy() // Переходим к Destroy()
   RETURN NIL
 
Примечание. Для объектов без регистрации - Axit() автоматически не сработает. Если выполнена регистрация, её снятие – обязательно. Для этого не обязательно использовать Axit(), можно «отписаться» и в Destroy().
 
Продолжим. Давайте ускорим мой пример:
 
METHOD Axit() CLASS Test
 
   SELF:Destroy() // Переходим к Destroy()
   RETURN NIL
 
METHOD Destroy() CLASS Test
 
            // <Здесь что-то чистим>
 
   IF ! InCollect() // Если мы ещё в «мусорном» списке
      UnRegisterAxit( SELF ) // Удаляем регистрацию
   ENDIF
 
   RETURN NIL
 
Т.е., я предлагаю снимать регистрацию непосредственно в методе, выполняющем очистку. Приятная мелочь такого подхода: если мы завершим работу с классом через «Destroy», он автоматически снимет регистрацию, а встроенной системе «ассенизации» не придётся обращаться к Axit() с просьбой снять свою заявку на «клининговые услуги». (Пример – реализация классов «Control» и «VObject» в SDK).
 
Как думаете, надо ли регистрировать и классы-«потомки»? Можно, но не нужно. Зачем плодить записи? Если «отец» это уже сделал, она распространяется и на его «наследников». А снимать регистрацию в методе Axit() «потомка» надо? Подумайте сами... Есть «золотое» правило, звучащее в моей вольной трактовке как: «кто регистрацию сделал, тот её и снять должен».
 
CLASS Y INHERIT Test
   PROTECT cText AS STRING
 
METHOD Axit() CLASS Y
 
   SELF:cText := NULL_STRING
   RETURN ( SUPER:Axit() )
 
Тот же пример, но для случая, если у «родителя» в методе Axit() делается вызов Destroy(). В таком случае, в классе «Y», вместо Axit() правильнее использовать Destroy():
 
CLASS Y INHERIT Test
   PROTECT cText AS STRING
 
METHOD Destroy() CLASS Y
 
   SELF:cText := NULL_STRING
   RETURN ( SUPER:Destroy() )
 
Хорошо. Если в «родительском» Axit() написать вызов Destroy(), надо ли аналогично сделать и в Axit() «наследника»? Нет. Это же породит лишнюю цепочку событий: сначала пройдёт обработка Destroy(), инициализированная «наследником», а потом – ещё раз, запущенная «родителем». Поэтому, если уж написали вызов Destroy() в Axit() «родителя» – делать тоже самое у «потомка» – не надо.
 
Как узнать, а подключён ли класс-«родитель» к системе очистки? Абсолютно точный ответ даст только исходный текст. Но, можно проявить интуицию - поискать метод Axit() (у него и его родителей): если он есть, то, скорее всего, объект подключён к системе очистки. Ещё точнее даст ответ – эксперимент: сделать класс-«наследник», и в его методах поставить точки останова (AltD), проверив ситуации в отладке. Так же, может помочь функция Memory( MEMORY_REGISTERAXIT ) – она возвращает число зарегистрированных объектов (см. её описание в Help-е).
 
Допустим, у родителя уже есть и Axit() и Destroy(), но один из другого не вызывается, а я хочу… Можно исправить? Можно. Лучше – в исходном коде «родителя». При отсутствии возможности – сделать класс–«наследник», и в своих проектах (чтобы ваше «кольцо» работало) использовать только этот новый класс.
 
 
Подведём первые итоги:
  1. В используемых классах (объектах) желательно следить и вовремя «обнулять» переменные неопределённой длины (USUAL, STRING, ARRAY и т.п.).
  2. Естественно, это не касается классов без переменных или с переменными фиксированной длины.
  3. При создании собственного класса, надо принять решение: создавать его самостоятельно «с нуля» или на основе уже существующего. Рекомендую второй вариант – на основе КаВо-класса: контрол – на основе контрола, окно – на основе окна и т.д., т.к. это решит большинство вопросов.
  4. Если создавать класс «с нуля», в качестве основы лучше взять VObject{}, тогда вам останется только вопрос с регистрацией.
  5. А если вы решите сделать свой класс, который ни на что не опирается (не будет ни чьим «наследником»), то делать это надо внимательно, чётко понимая и прописывая все методы и их связи. (О чём я собственно и пишу).
 
 
Давайте потренируемся наводить порядок. Для примера возьмём класс DBServer. Что мы имеем:
  • RegisterAxit() делается в Init().
  • Вместо Destroy() используется приятный на слух Close() с UnRegisterAxit().
  • Метод Axit() - есть, но в нём нет перехода в Close() и не снимается регистрация.
  • Полностью переменные не чистятся ни в Axit() ни в Close()
 
Я первый это заметил? Нет, конечно. Например, в библиотеке «bBrowser» создан свой класс – «bDBServer», в котором решается часть проблем с использованием дополнительного метода Destruct(). Но, мне это не нравится, и я предлагаю пойти прямым и простым способом:
 
CLASS msDBServer INHERIT DBServer
 
METHOD Axit() CLASS msDBServer
 
   SELF:Close()
   RETURN ( SUPER:Axit() )
 
METHOD Close() CLASS msDBServer
 
   LOCAL lRet AS LOGIC
 
   lRet := LOGIC( _CAST, SUPER:Close() )
 
// Чистим остатки от родителя (DataServer):
   SELF:oHyperLabel := NULL_OBJECT
   SELF:oHLStatus := NULL_OBJECT
 
// Чистим остатки от родителя (DBServer):
   SELF:cRDDName := NULL_STRING
   SELF:oFileSpec := NULL_OBJECT
   SELF:aRelationChildren := NULL_ARRAY // Чистится в DBServer:Close(), но могут быть нюансы
   SELF:cbSelectionParentExpression := NIL
   SELF:uSelectionValue  := NIL
   SELF:cbSelectionIndexingExpression := NIL
   SELF:cbStoredForBlock := NIL
   SELF:cbStoredWhileBlock := NIL
   SELF:uStoredScope := NIL
   SELF:lStoredAllRecords := NIL
   SELF:oErrorInfo := NIL
   SELF:aRdds := NULL_ARRAY
 
            < А тут уже чистим свои переменные (если они есть) >
 
   RETURN lRet
 
Из-за особенности закрытия в DBServer-а я применил небольшую «инверсию» - сделал SUPER:Close() не в конце, а в начале, сохранив полученное значение. Важно чтобы по окончанию работы с сервером всегда выполнялся Close(), ведь там снимается регистрация. И мы это сделали!
 
Тут кое-кто пробурчит, что было бы неплохо, если после закрытия всё-таки оставались какие-то переменные (типа, статуса и ошибки). Чтобы можно было что-то проверить или перепроверить, использовать… Дело - ваше. Тогда в Close() нельзя чистить эти данные. Придётся их «обнулять» как-то по-другому…
 
Тут есть тонкость: в DBServer переменная с данными об ошибке (oErrorInfo) не имеет регистрации и метода-«деструктора». Поэтому, вам придётся решать этот вопрос. Можно сделать на её основе класс-«наследник», заменив им в сервере стандартный DBError (oErrorInfo), что может вылиться в переписывание кода… А можно сделать «ход конём» - подключить ваш новый Error-класс в качестве дополнительной переменной, и в методе Close(), перед сбросом oErrorInfo, сохранять её значение в вашу новую переменную (которая подключена к системе очистки).
 
 
Давайте продолжим теорию. Мы уже разобрались с методами-деструкторами, поговорили о способе решения проблемы с «обнулением» при внезапном разрушении объекта. Но это ещё не всё, этого – недостаточно. Многие объекты событийно-зависимые, т.е., их поведение может управляться некими «толчками» (нажатием клавиш клавиатуры, мышки, запросов от других программ и пр.), что выражается в виде поступлений сигналов от других объектов.
 
Для этого в КаВо применяется обработчик (перехватчик) событий, который направляет их в нужное русло, в метод(ы) класса(ов). Как это сделано – хорошо видно в SDK. Обратите внимание, на классы окон.
 
// Это кусочек текста из Dispatch():
    CASE (uMsg == WM_CLOSE)
        IF SELF:QueryClose(oEvt)
            SELF:__Close(oEvt)
        ENDIF
        RETURN SELF:EventReturnValue
 
METHOD __Close(oEvent AS @@Event) AS VOID PASCAL CLASS Window
 
    SELF:Close(oEvent)
    SELF:Destroy()
    RETURN
 
По событию «WM_CLOSE» (закрытие) вызывается метод __Close(), который запускает последовательность: Close() и Destroy(). Т.е., закрытие по событию (в нормальной ситуации) и здесь приводит к Destroy(). «Дедушкой» класса окна является VObject, поэтому о «закольцовке» Axit() в Destroy() тоже заморачиваться не придётся.
 
Всё уже сделано до нас! Остаётся только правильно пользоваться методом Destroy().
 
И тут пытливый разум задаст сногсшибательный вопрос: «а надо ли в методе Destroy() окна обнулять переменные контролов?» Давайте посмотрим SDK и подумаем… Контролы сделаны на основе VObject, плюс правильно прописан Axit(). Значит, они «аннигилируют» самостоятельно и без нашего вмешательства! Ответ: можно не писать.
 
Если можно «не писать», то зачем всё это? А «это всё» полезно знать при создании своих классов или исправлении существующих!
 
 
Жду ваших отзывов!
 
 
23.02.2016 г.    Карандаш.
 
 
P.S.: Рецензентом статьи (по моей просьбе) выступил наш старый знакомый – leo_bond. В дополнении к сказанному, он рекомендует использовать _RegisterExit() (описание см. в Help-е КаВо). Она предназначена для вызова функции очистки программы, что весьма полезно. В этой вызываемой функции можно, кроме стандартного обнуления глобальных переменных, включить анализ памяти, и узнать какие переменные «не ушли» из памяти, где и как они созданы.
 
Код его анализа я решил не прилагать к данной статье, т.к. теперь это стало стандартным примером использования функции CreateGCDump() в справке КаВо. Там вы его и найдёте.
 

Theme by Danetsoft and Danang Probo Sayekti inspired by Maksimer