За кулисами системы обработки событий |
75 |
тип — это промежуточные результаты, у нас это текст для кнопки, которые мы выводим на экран.
Для запуска задачи в отдельном потоке применяется метод execute(). Можно запускать экземпляры SwingWorker и в своих собственных потоках, так как класс этот реализует интерфейс Runnable. Далее все интересное происходит в переопределенных нами методах, все они вызываются внутренними механизмами класса SwingWorker. Сама долгая работа выполняется в методе doInBackground(), он исполняется в другом потоке, и в нем ни в коем случае не следует работать с интерфейсом программы. Зато из этого метода мы может вызывать метод publish(), передавая туда список данных, этот список попадает в метод process(), и вызывается он уже из потока рассылки событий, где мы вольны так обновить интерфейс, как нам пожелается. Мы просто обновляем текст на кнопке. Ну а метод done() вызывается после окончания работы метода doInBackground() и его можно использовать для окончательного обновления интерфейса после окончания задачи.
Преимуществ использования класса SwingWorker вместо ручного создания потоков несколько. Во-первых, его методы несут больше смысловой нагрузки по сравнению с беспорядочными вызовами очереди событий и созданием кучи объектов Runnable, и понять что именно делает задача, обладая нехитрым знанием SwingWorker, очень легко. Ну а главное — это оптимизация SwingWorker. Он не позволит вам расплодить очень много маленьких потоков (на данный момент максимальное количество потоков в нем ограничено 10), ставя новые потоки в очередь ожидания, попытается оптимизировать вызовы publish(), да и просто использование системного класса предполагает что поддержка программ с ним в будущем всегда проще, случись вдруг в Swing какие-то архитектурные перемены. В системных классах поддержка перемен всегда быстра, достаточно загрузить новую версию пакета JDK.
Почему мы так запускаем Swing-приложения
Начиная с первых глав, мы то и дело пишем маленькие примеры, чтобы лучше разобраться во всем, что происходит, и нетрудно заметить, особенно теперь, когда мы знаем о существовании потока рассылки и очереди событий, что мы с самого начала метода main() создаем объекты своих интерфейсов в этом самом потоке. Когда-то пользователи Swing наивно полагали, что создавать графические компоненты в других потоках до того момента, как те перейдут в реализованное состояние (помните, мы обсудили что это за состояние чуть ранее), вполне безопасно. Ведь так оно и есть для любого легковесного компонента AWT. Но, только если это не компонент библиотеки Swing.
Впрочем, зачем нам слова, если можно увидеть собственными глазами. Рассмотрим пример, который все проиллюстрирует:
//StartingEventThread.java
//Проверка момента запуска потока рассылки событий import javax.swing.*;
import java.awt.*;
public class StartingEventThread {
public static void main(String[] args) {
//заменяем системную очередь событий своей
Toolkit.getDefaultToolkit(). getSystemEventQueue().push(new CustomQueue());
//создаем окно
76 ГЛАВА 3
JFrame frame = new JFrame("Тест"); System.out.println("(1) JFrame()"); // добавляем флажок
JCheckBox checkBox = new JCheckBox("Тест"); frame.add(checkBox, "South"); System.out.println("(2) Добавлен флажок"); // создаем список
DefaultListModel model = new DefaultListModel(); JList list = new JList(model);
frame.add(list);
System.out.println("(3) Добавлен список");
//обновляем модель model.addElement("Тест"); System.out.println("(4) Обновление модели");
//окончательно выводим интерфейс на экран frame.setVisible(true); System.out.println("(5) Интерфейс построен");
}
//специальная очередь событий, сообщающая
//отладочную информацию о событиях и потоках static class CustomQueue extends EventQueue {
//метод кладет событие в очередь public void postEvent(AWTEvent event) {
System.out.println("post(), поток: " + Thread.currentThread().toString());
System.out.println("post(), событие: " + event); super.postEvent(event);
}
//метод распределяет событие по компонентам protected void dispatchEvent(AWTEvent event) {
System.out.println("dispatch(), поток: " + Thread.currentThread().toString()); System.out.println("dispatch(), событие: " + event);
super.dispatchEvent(event);
}
}
}
В примере мы используем тот факт, что очередь событий можно заменить на свою реализацию, используя метод фабрики AWT Toolkit. Так как изобретать заново велосипед мы не желаем, мы просто унаследуем от стандартной очереди событий, разбавив ее реализацию отладочными сообщениями, которые покажут нам, кто и когда, а самое главное, из какого потока, кладет в очередь события. Нам понадобится два метода —
За кулисами системы обработки событий |
77 |
postEvent() применяется, когда в очередь кладется событие высокого уровня, например, события внутренних систем Swing, в метод же dispatchEvent() попадают события уже для окончательной обработки, некоторые из них могут и не попасть в postEvent() в текущей реализации очереди событий.
Далее мы, нарушая архитектуру остальных примеров и программ книги, начинаем создавать и настраивать графические компоненты Swing прямо в методе main(), которые запускается в своем отдельном потоке. После каждого действия, произведенного с компонентами пользовательского интерфейса, в консоль выводится диагностическое сообщение, которое даст нам знать, на каком этапе создания интерфейса мы находимся. С другой стороны, наша специальная очередь, внедренная в самое сердце Swing, расскажет нам, откуда появятся первые события и кем они будут поставлены в обработку. Самих действий не так много — создается окно JFrame, флажок, список JList с моделью по умолчанию, и производится изменение этой модели, что согласно модели MVC, должно привести к оповещению списка о необходимости перерисовки, хотя он еще и не виден на экране.
Все, что произойдет далее, мы увидим в консоли. По сути, результаты могут разниться в зависимости от вашей версии JDK, операционной системы и любых других факторов, способных повлиять на внутренние механизмы виртуальной машины JVM и реализации AWT. В момент написания этой главы первые строчки на консоли выглядели следующим образом:
(1)JFrame()
(2)Добавлен флажок
Таким образом, создание окна и флажка не вызвало добавление в очередь новых событий пользовательского интерфейса, и потоку рассылки событий запускаться нет смысла. Но затем происходит следующее:
(3) Добавлен список
post(), поток: Thread[main,5,main]
post(), событие: java.awt.event.InvocationEvent[...]
По некой причине добавление списка в окно добавляет в очередь новое событие InvocationEvent, а такие события обычно приходят от внутренних механизмов библиотеки Swing. Причем, как мы видим, событие было положено из потока main, потому что добавление списка произошло из метода main(). Все это означает, что поток рассылки событий запустился и вскоре доставит событие компонентам. Но мы еще не закончили создавать интерфейс и теперь с легкостью можем столкнуть поток рассылки и поток main. Посмотрим, что происходит далее:
(4) Обновление модели
dispatch(), поток: Thread[AWT-EventQueue-1,6,main] dispatch(), событие: java.awt.event.InvocationEvent[...] post(), поток: Thread[main,5,main]
post(), событие: java.awt.event.InvocationEvent[...] dispatch(), поток: Thread[AWT-EventQueue-1,6,main] dispatch(), событие: java.awt.event.InvocationEvent[...] post(), поток: Thread[AWT-Windows,6,main]
post(), событие: java.awt.event.InvocationEvent[...] post(), поток: Thread[main,5,main]
post(), событие: java.awt.event.InvocationEvent[...]
(5) Интерфейс построен
78 |
ГЛАВА 3 |
Теперь ситуация совсем вышла изпод контроля и может причинить нам массу неудобств. Обновление модели снова заставляет некие механизмы Swing посылать все новые события в очередь событий, и эти события рассылает (через метод dispatch()) другой поток рассылки событий. Все это видно в нашем консольном выводе. Любое случайное столкновение потоков приведет к проблемам — в легком случае может нарушиться внешний вид приложения, а в тяжелом оно запросто может зависнуть, потеряв все бесценные данные тяжелой работы пользователя.
СОВЕТ
Не следует создавать или заранее подготавливать компоненты Swing в отдельном потоке, отличном от потока рассылки событий. Реализация компонентов Swing подразумевает любые манипуляции с ними из потока рассылки событий, даже если компоненты еще не добавлены на экран.
Последний вопрос — что же это за внутренняя работа Swing, которая пробуждает поток рассылки событий, даже если компоненты еще не добавлены на экран. Как мы узнаем из главы 5, это в основном сделано для нашего с вами удобства, так как любое обновление моделей и сложных компонентов автоматически обновляет их на экране, что и приводит к запуску потока рассылки (так называемая автоматическая проверка корректности и перерисовка). За удобство приходится платить, в том числе и таким обрахом: немного вычурной конструкцией запуска приложений Swing в методе main.
Отладка потоков в системе событий
Как мы уже поняли, наиболее «щекотливым» вопросом при работе с системой событий Swing является необходимость практически для всех видов приложений использовать отдельные потоки для выполнения действий и при этом не переходить дорогу потоку рассылки событий. Даже если программу вы пишете сами и старайтесь все делать правильно и без ошибок, но при написании потоков настолько естественно вызывать методы компонентов Swing или их моделей напрямую, не думая о вызове через очередь событий, что очень легко немного промахнуться и подставить программу под удар.
В таких ситуациях удобно постоянно проверять себя, и всех своих коллег, если вы работаете не в одиночку, с помощью вспомогательных инструментов. Как мы вскоре узнаем в главе 4, в Swing все действия с компонентами приводят в объект RepaintManager, где можно проверить, из какого потока проводится работа с компонентами и найти проблемные ситуации. Писать с нуля нам ничего не понадобится, так как хорошие реализации уже есть.
Прежде всего, это хорошо известный сторонний внешний вид Swing, называемый Substance, который по умолчанию запрещает работу с компонентами Swing из «неправильных» потоков, создавая исключения. Таким образом, небрежно написанная программа вообще не имеет шансов стабильно работать в этом внешнем виде. Даже если вы используете другой внешний вид, вы можете тестировать свою программу во внешнем виде Substance для поиска возможных проблем с множественными потоками. Это не единственный вариант: существует также целая библиотека для модульного тестирования Swing-приложений под названием FEST, которая вызовом одного метода включает режим запрета работы с компонентами Swing из потоков, отличных от потока рассылки событий. В противном случае возникает исключение. Библиотека FEST очень полезна для автоматизированного тестирования Swing-приложений, попробуйте найти ее в Интернете, после ознакомления со Swing.
За кулисами системы обработки событий |
79 |
Резюме
Система обработки событий Swing по-настоящему элегантна и прозрачна, она позволяет создавать гибкие и легко расширяемые программы. Чем более сложные программы и пользовательские интерфейсы вы будете создавать, тем больше вы будете ценить разнообразие и гибкость способов, которыми можно обработать события. События можно обрабатывать даже на самом низком уровне, вмешиваясь в сокровенные механизмы распределения событий и реализуя то поведение, которое вам необходимо. С другой стороны, за простотой и элегантностью системы обработки событий скрывается несколько нетривиальных механизмов, незнание которых может сделать ваш интерфейс неотзывчивым и, более того, привести всю программу к сложной ситуации конфликта нескольких потоков выполнения (тупику). В этой главе мы исследовали все нюансы процесса обработки событий в Swing, начав с самого простого и закончив исследованием «начинки» системы обработки событий.