?

Log in

No account? Create an account

elizarov


Блог Романа Елизарова


Previous Entry Share Next Entry
Как избежать утечек памяти из-за потери подписчиков в GUI приложениях
elizarov
Написать этот пост меня cподвиг вопрос в ru_java от and_bel.

Всем кто когда-либо писал большие, сложные, многооконные GUI приложения, где разные окна работают с общими моделями данных, должна быть знакома ситуация: открываем и закрываем какое-либо окошко несколько раз и видим что потребление памяти увеличилось. Классическая утечка памяти, которая у пользователей проявляется как "тормоза" после длительной работы с приложением, а потом и вылет из-за нехватки памяти. Причина таких утечек обычно банальна и проста -- визуальные компоненты приложения должны подписываться к модели данных чтобы получать нотификации об её изменениях и отображать их на экране (классический шаблон Model-View-Controller, только Controller нас сейчас не интересует). Программист забыл отписаться от нотификаций при закрытии окна -- вот вам и утечка. В приложениях написанных на базе Swing ситуация осложняется тем, что при написании отдельных компонентов не очень понятно в какой момент надо отписываться -- стандартных методов типа close или destroy для компонентов не предусмотрено. Можно ли построить дизайн приложения таким образом, чтобы уменьшить вероятность появления таких ошибок?


Один из подходов заключается в явном управлении за жизненным циклом всех окон и компонентов внутри них. В некоторые GUI каркасы такое управление встроено изначально, а в Swing можно прикрутить что-нибудь свое или чужое. Это решает часть проблемы. У вас появляется метод типа close или destroy куда надо написать код отписывания от всех моделей данных. Далее, в компоненте вашего приложения нужно следить чтобы для каждого addXXXListener в инициализации компонента был парный ему removeXXXListener в методе закрытия. Такое дублирование это явно нехороший запах в коде, показывающий непродуманность дизайна.

Мы избавились от этого дублирования в несколько шагов. Сначала, через максимальное абстрагирование и повторное использование кода мы добились, чтобы моделей в приложении было бы существенно меньше чем компонентов. Там где это возможно, используется универсальная модель -- иерархическое дерево контекстов хранящее пары key-value. Её фундаментальное отличие от классических классов типа BeanContext в том, что любое поддерево можно отцепить от контекста приложения вызвав на корне поддерева setParent(null). При этом, все указатели для нотификации о событиях идущих от корня дерева к этому поддереву автоматически обрываются.

Каждое окно или панель приложения создает свой собственный подконтекст в этом дереве. Все компоненты этого окна или панели подписываются на нотификации используя исключительно свой контекст, который окно или панель передает им в конструкторе. А отписываться им вообще не надо. При закрытии окна или панели пользователем вызывается setParent(null) на его контексте, автоматически отписывая все компоненты от идущих снаружи нотификаций.
Tags:

  • 1
А у нас другая система, как я считаю, не хуже. Описывать в ЖЖ лень, так как можно сделать маленькую статейку :) Но она у нас выложена здесь: http://code.google.com/p/twocents/source/browse/#svn/trunk/src/org/almworks/util/detach, можешь глянуть - интересно твое мнение.
Я уже собственно плохо представляю как писать Ui-код без детачей :) Могу рассказать детали при встрече

Идея понятная. Однако, контекст решает не только эту задачу, он еще и универсальная модель и предлагает интерфейс listener-а, чтобы их не плодить для каждой примитивной модели. Например, использую контекст ты так можешь написать компонент, который красиво отрисовывает текущий статус системы и следит за его изменениями.

Context ctx;

public StatusView(Context ctx) {
    this.ctx = ctx;
    ctx.addListener(Status.KEY, this);
}

public void valueChanged(ContextKey key) {
    repaint();
}

public void paint() {
    Status status = ctx.get(Status.KEY);
    // ... paint status
}


Мало того, что ушла необходимость вызывать removeListener, так тебе еще не нужно заводить класс StatusModel и StatusListener (на последнем можно было бы сэкономить воспользовавшись PropertyChangeListener).

А как тоже самое выглядит с Detach-ами?


Edited at 2008-11-20 08:14 am (UTC)

ну во-первых мы addListener в конструкторе стараемся не вызывать ;-P

что касается подписки, будет выглядеть так:
StatusView() {
}

void attach(Lifespan life, EventSource source) {
  source.addListener(life, this);
}


но здесь не демонстрируются преимущества детача, так как они проявляются именно в управлении лайфспэном плюс в том что есть заготовленные детачи на различные свинговые подписки

что касается универсальной модели и ненужности заводить классы, то это, согласись, совсем другая история. к отписке отношения не имеет. и раз уж на то пошло, я считаю такой подход 100% обоснованным только когда типы неизвестны at compile time, в остальных случаях это может оказаться усложнением на ровном месте.

если например мне надо слушать N типов ивентов, то valueChanged() - один на всех - будет представлять из себя простыню из if-ов, что ли?

потом, ты отказываешь себе в лисенерах, которые могут что-то осмысленное принимать помимо T

плюс к тому у тебя по-любому остаются jdk-шные лисенеры, которые надо имплементить


Да, решаются несколько разные проблемы и по уму надо все эти подходы дружить между собой. На вскидку:

Контексты решают проблему универсальной модели и экономя классы банально убыстряют startup приложения. Причем всё это делается type-safe образом (всё проверяется в compile-time -- обрати на отсутствие кастов в моем коде). Я думаю что не ошибусь, если скажу что для 90% слушателей ничего кроме valueChanged не нужно. В остальных случаях придется, конечно, написать модель и listener к ней "ручками". Хотя у нас еще 9% закрывается каркасом EntityLists (который хорошо бы тоже поженить с контекстами) и остается лишь 1% исключений.

Детачи решают пролему add/remove для стандартных jdk-шних листенеров и всего что не укладывается в valueChanged Context-а a entitiesChanged EntityList-а.

Кстати, у нас проблама с removeXXXListener для jdk-шных событий редко возникает -- обычно компонента слушает что-то на самой себе (KeyListener, MouseListener), поэтому ей отписываться не обязательно. А если у компоненты возникает желание слушать что-то снаружи, то тут прямой пусть к написанию честной модели или через Context. Тут работает принцип молотка и гвоздей -- если у тебя есть молоток, то все проблемы кажутся гвоздями ;) Мы, как ты можешь догадаться, любую проблему решаем с помощью Context-а.

если например мне надо слушать N типов ивентов, то valueChanged() - один на всех - будет представлять из себя простыню из if-ов, что ли?

Да, такую цену мы платим за уменьшение к-ва интерфейсов и увеличение скорости запуска приложения. Более того, мы и inner classes для слушателей стараемся не использовать, то есть если надо поставить ActionListener на 10 компонент, то метод actionPerfromed будет прямо в компоненте как цепочка из 10 if-ов. Но это историческая причина. Сейчас я может бы предпочел использовать EventHandler, хотя необходимость указывать имя метода в виде строки меня коробит. В цепном if-e, все-таки, обычно проверяется getSource() события на == нужной компоненте -- это как-то безопасней выглядит.

Кстати, заметил что в дополнение к StatusView в твоем примере надо еще дописать EventSource (его нету среди "стандартных" классов). Тогда это уже скорей будет StatusEventSource ну и StatusEventListener до кучи, так? Или вы такие классы повторно используете? Типа в данном случае воспользуетесь каким-нибудь готовым? Если да, то каким, а если нет, то как этот StatusEventSource будет выглядеть?

Под EventSource я имел в виду нечто, к чему мы подписываемся. Обобщил, так сказать.

Например, если это модель, написанная нами, она будет иметь метод addListener(Lifespan, SomeListener). Тут есть серьезный неочевидный плюс - ты *обязан* передать lifespan. Соответственно ты должен либо сам им управлять, либо тебе его дадут "свыше". То есть риск забыть отписаться серьезно уменьшается. При этом ты можешь передать Lifespan.FOREVER, если отписка тебя не заботит -- но в любой момент можешь сделать на него search usages чтобы увидеть, где в коде нет отписки.

Если источник событий - не наш класс, он, скорее всего, не принимает Lifespan. Тогда метод выглядит по-другому. Например, для свинговых лисенеров у нас есть вот такие враппер-методы:
static void addDocumentListener(Lifespan life, final Document doc, final DocumentListener listener) {
  if (life.isEnded()) return;
  doc.addDocumentListener(listener);
  life.add(new Detach() {
    public void doDetach() {
      do.removeDocumentListener(listener);
    }
  }
}

Соотв. один анонимный класс-отписчик на каждый лисенер. Без дублирования кода.

А мы в этом случае создаем класс, который по традиции называется xxxAgent. Он, как и приведенный тобой метод, пишется один раз и отвечает за вызов add/removeXxxListener. Например, компонент, которому надо подписаться к документу, в конструкторе говорит:

agent = new DocumentAgent(ctx, this);

Обрати внимание, что сам документ компоненту передавать не нужно, да он в этом момент может быть ещё и не известен. Где-то там, в вышестоящем контексте кто-нибудь вызывает, например:

Document.KEY.set(ctx, document);

и все агенты сразу подписываются и начинают рассылать нотификации слушателям. Как только документ исчезает из контекста или компонент убивается (что одно и то же с точки зрения агента) -- он отписывается от документа.

Ты, в общем, в курсе что наши "документы" обычно слишком большие чтобы их целиком загрузить в приложениие. Они там сидят на сервере, а приложение подгружает только те кусочки которые ему интересны. Поэтому, на практике, при подписке к такому документу нужно указать, например, какая его часть тебе интересна. Управлением этим интересом у нас тоже занимается агент. Если компоненту вдруг становится нужна какая-то часть документа (пользователь поменял какой-то фильтр), то он говорит:

agent.setPart(part);

и ждет нотификаций через DocumentListener о том что данные подгрузились/поменялись.

В Component есть removeNotify() и addNotify() замое то, где добавить/удалить листенер.

Знаю, но там были грабли. Вспомню где -- напишу.

Там можно super забываь вызвать, но тогда это будет невозможно не заметить, а так пользуюсь и все окей.

Кирилл К. говорит что оно не всегда так хорошо работает. деталей не помню, спроси у него

он работает прекрасно, я говорил о том, что на нем стремно вешать логику

то есть addNotify/removeNotify говорит о том, что какой-то компонент куда-то положили, это может быть посередине event processing'а свиногового например, то есть состояние всего ui еще не устаканилось, и с этим могут быть тараканы, например, с фокусом :)

ну или компонент может быть переложен несколько раз по каким-то нуждам, что никак не отражает тот факт что ui надо диспоузить

Где-то когда-то прочитал, что в рунете сформировался принцип: если кто-то где-то дает объявление без указания города, то это - Москва.
Я к тому, что пока не дочитал до слова Swing, думал что-то интересное, а оказалось - Ява ;)

Ой.. а разве на чем-то другом еще до сих пор программируют? ;)

А если серьезно, то такие же проблемы будут возникать на любом языке в любом GUI каркасе, разве что в других языкам могут быть какие-то свои специфические альтернативные пути решения. Например, шаблон с детачами от igorsereda на C++ мог бы быть оформлен в виде объекта, который отписывается при вызове своего деструктора. Но писать большие GUI на C++ это вообще занятие не для слабонервных, so don't try this at home -- пусть этим в Microsoft развлекаются -- у них программистов толпа и всех надо чем-то занять ;)

Но писать большие GUI на C++ это вообще занятие не для слабонервных
А как же Qt? :)

К сожалению, практического опыта работы с Qt у меня нет. Но можем порассуждать теоретически. К любой технологии у меня есть сразу набор вопросов. Я их задам, и, если вы практически знакомы с Qt, то можете на них ответить.

Итак, для продуктивного создания больших приложений особую важность имеет не столько достоинства или недостатки той или иной библиотеки или языка, сколько наличие IDE которые поддерживало бы всевозможные рафакторинги, начиная от простейшего переименования и дальше. Как у Qt с этим дела? Есть ли, к ней например, IDE которое при переименовании сигнала valueChanged на одном из моих объектов (при наличии еще десятков сигналов с таким же именем на других объектах) нашла бы все связанные с ним вызовы connect?


IDE у Qt есть (появилось, правда, совсем недавно) - http://trolltech.com/developer/qt-creator. На мой взгляд - вполне удобно, правда как оно будет для использования действительно сложных GUI - не знаю.
Увы, мне не хватает времени расковырять его до конца, потому есть ли там подобные автозамены - не скажу.

в Идее есть подобное решение, класс Disposer который хранит иерархию объектов, и когда на нод говорят "dispose" то в обратном порядке пробегаются по сабнодам, всем вызывается dispose а потом ветка отцепляется

то есть при добавлении лисенера используется второй параметр - parentDisposable, перед тем как его грохнут -- сначала гарантирован вызов dispose и сабнода

void addListener(Listener listener, Disposable parent) {
  myListeners.add(listener);
  Disposer.register(parent, new Disposable() {
    public void dispose() {
      myListeners.remove(listener); 
    }
  })
}


а parent disposable обычно всегда есть в конексте, если что можно хоть Project подсунуть который вообще всегда есть

мега-удобно

Ага. Очень похоже на детач от igorsereda. Для полного счастья надо бы один раз написать какой-нибудь специальный DisposableListenerList, чтобы не делать cut-and-paste приведенного кода по всему проекту. Надеюсь что-нибудь подобное у вас есть? А то ведь мало того, что cut-and-paste это зло, так еще и тучу внутренних классов можно сэкономить -- ведь IDEА и так не быстро стартует.

да уж, codebase у нас такой уже нахерачили, что если запускать Идею не в сборке в виде jar'ов а на классах, то класслоадер пыхтит жестко

ага, подтверждаю. использую этот механизм уже на третьем проекте, супер как удобно! :)

И неужели никто до сих пор не сделал их этого какой-нибудь OpenSource framework?

I want a method to remove all listeners from a Component

(Anonymous)
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4380536

Re: I want a method to remove all listeners from a Component

Гм... а зачем?

  • 1