Pull to refresh

JAVA+Swing в 2013. Стоит ли?

Reading time8 min
Views97K
На Хабре Swing не любят. Поиск по «Swing» дает либо нейтральные, либо негативные упоминания. Вот некоторые из них:
  • «Java-апплеты (доразвивались до смертельной болезни под названием Swing)»
  • «Swing — мягко говоря не самый оптимальный UI фреймворк»
  • «Swing был ужасен»

Я не возьмусь утверждать, что Swing — идеал. Это неправда. Однако я постараюсь описать те плюсы и минусы с которыми пришлось столкнуться.

Почему Swing


Я работаю со Swing с перерывами пару лет. В основном, по вечерам. Пишу программу Visual Watermark для групповой защиты фотографий. Java версия у меня появилась в 2011. Мне захотелось сделать порт под Mac и вылизать интерфейс, но писать отдельную программу под каждую платформу у меня не было никакой возможности.

В начале 2011 UI-библиотеки для кросс-платформенной разработки были в таком состоянии:
  • QML был весь в багах: меню появлялись под компонентами, демка падала, поддержки в QtCreator не было. Ускоренная отрисовка появилась только в Qt5 прошлой осенью.
  • Qt не подошел, т.к. был целиком на “нативных” компонентах, а часто требовалось где-то изменить отрисовку.
  • Juce подходила по функционалу, не глючила и не падала. Стоила приемлемых денег да еще и с открытым кода. Отпугнул меня C++. Это замечательный язык на котором пишут огромное количество умнейших людей. Учитывая мой маленький опыт и размер задачи, сложность C++ – это перебор. Плюс, выяснилось, что Xcode не умеет рефакторить C++.
  • Adobe Air не поддерживает multi-threading.
  • Mono+GTK Мне кажется, что к эту комбинация могла бы решить мои проблемы. В тот момент отпугнул очевидный косяк с неработающими горячими клавишами в GTK. Судя по MonoDevelop, он не пофикшен до сих пор.
  • JavaFX не было под Mac.
  • SWT намного легче, чем Swing и в целом хорош. Не стал писать на SWT потому, что смотрел я его самым последним. Уже было потрачена уйма времени и я закончил эксперименты на первом попавшемся баге («плавали» кнопки по высоте на тулбаре).

На тот момент Java была частью Mac OS X, имела отличный Native Look & Feel, а JRE под Windows весила всего 12 мегабайт. Я был наивно уверен в успехе. В итоге, после 2 или 3х месяцев работы я оказался с первой версией программы на Java Swing.

На сегодняшний день в QML и JavaFX исправлены описанные проблемы. Поэтому, если вы готовы работать со сценическим графом, то вам стоит взять их на тест-драйв.

Qt перешел под крыло фирмы Digia. Выпуск бета-версии под iPhone и Android дает надежду на дальнейшее развитие библиотеки.

JavaFX стала библиотекой с открытым кодом в феврале этого года. Её совместимость с OpenJDK планируется на JDK 9. Когда выйдет 9ка неизвестно. Релиз 8ой версии запланирован на начало 2014 года.

О хорошем


Начну с хорошего. Вдруг подумаете, что я тоже в Swing-хейтеры записался?!

Вся отрисовка hardware-accelerated. Любое Swing-приложение отрисовывается на GPU, от разработчика ничего не требуется. Это делает возможным анимации в приложении. В том числе, когда приложение полноэкранное или развернуто на 24’’ мониторе.

MVC. Swing критикуют за массивность: каждые компонент состои из представления, контроллера и модели. В то же время, это дает возможность быстро добавить нужную фичу в существущий компонент. Все очень гибко.

Java – это managed код. Вы избавляетесь от кучи возможных багов, «доступных» только для C++-разработчиков. Риск Access Violation сведен к минимуму. Хотя это совершенно не означает, что у вас не будет других багов. Утечек памяти, например.

Отличная среда разработки. Eclipse, Intellij IDEA, NetBeans – выбор огромный. Везде есть рефакторинги, форматирование кода, авто-комплит, поддержка unit-тестов,

Очень много библиотек. LayoutManager’ы, работа с нативными объектами, строками, вебом – всего не перечислить. Это огромный козырь Java как платформы.

Очень много ответов на вопросы. Вот, например, доля вопросов на StackOverflow по каждой из UI-библиотек.



Примерно каждый сотый вопрос на StackOverflow – это вопрос о Swing. На практике, это означает, что большинство проблем уже решены. Скорее всего, один-на-один с проблемой вы не останетесь.

О плохом


Предыдущая часть похожа на сладкий пресс-релиз. Исправляюсь. Вот с чем вы можете столкнуться.

Не фиксятся критичные баги. File.exists не работает с момента релиза JDK7 и фикса нет до сих пор. Даже если баг критический, вы можете ждать фикса годами.

Ситуация может стать еще хуже, если вы планируете использовать нативный код. Я столкнулся с ситуацией, когда использование модальных окон (например, открытие OpenFileDialog) приводит к зависаниям на некоторых компьютерах. При том, что Java Native Foundation используется согласно примерам в документации. И баунти на StackOverflow мне не помог:-)

Баг с file.exists можно обойти с помощью классов из java.nio. Это новый API, который был призван решить проблемы производительности с развесистыми папками.

Что нужно сделать:
  1. Запустить приложение с параметром –Dfile.encoding=UTF-8
  2. Вместо File.exists используем Files.exists(Paths.get(fileName))
  3. Вместо File.listFiles используем
    try (DirectoryStream<Path> ds = Files.newDirectoryStream(folder)) {
       for (Path file : ds) {
           // do something
       }
    } catch (IOException e) {
       e.printStackTrace();
    }

Или фиксить этот баг самостоятельно и проталкивать закладку через серию ревью.

Swing – только hardware accelerated. Это значит ваше приложение не будет работать в VMware, Parallels или через удаленный рабочий стол. Если вы не готовы с этим мириться, то смотрите в сторону SWT.

Нет 32-битных билдов под Mac. Официальная сборка только 64 бит. К сожалению, я не знаю в чем причина этого решения. Могу лишь гадать, что дело в каких-то багах.

Некоторое время Henri Gomez поддерживал 32-битные и universal билды. Готовые билды можно было скачать с его странички на code.google.com. К моему сожалению, нехватка времени и новая работа заставили Генри свернуть этот проект. Попрощавшись, он выложил свои билдежные скрипты на GitHub:
https://github.com/hgomez/obuildfactory

С их помощью можно собрать OpenJDK под Mac и Linux. Здорово, но не совсем. С помощью этих скриптов 32-битная версия под Mac не собирается. Внутри JDK огромное количество конфигурационных файлов, в которых зашита сборка строго 64-битной версии для Mac. Изменишь ключ в главном файле и получишь неработоспособную сборку. Каким образом Henri Gomez собирал 32-битные билды мне неизвестно.

Включайте JRE в дистрибутив. Мнение руководителей Oracle о дистрибуции приложений: “standalone self-contained инсталлятор с bundled JRE для целевой платформы – это более удачная модель распространения приложений” (источник). Наиболее вероятная причина этого решения – огромное кол-во уязвимостей в апплетах: Java приняла знамя решета у Flash.

Наиболее жестко поддерживает это ограничение фирма Apple, которая удалила Java в версии Mac OS 10.7 Lion. Также они принудительно отключают ее при установке новых системных обновлений.

JRE 7 весит около 100 Мб. В архиве получается около 50. К сожалению, размер JRE от апдейта к апдейту растет и нам проблему распухшего дистрибутива придется решать.

Не все объекты BufferedImage используют аппаратное ускорение. Только для BufferedImage.TYPE_INT_*. Поэтому, начиная с JDK7, работать с TYPE_4BYTE*, TYPE_3BYTE нецелосообразно.

При доступе к данным растра BufferedImage, картинка перестает рисоваться через GPU. Зачем это сделано понятно: пользователь меняет данные, метода “закончил менять” нет и не понятно когда их пере-заливать в видео-память. По крайней мере, это логично.

В Visual Watermark я использовал C++ библиотеку для загрузки изображений и нужно было полученные пикселы превратить в объект BufferedImage. Менять по-одному пикселу очень медленно и пришлось писать напрямую в буфер растра картинки. Как только я вызвал у растра getData(), все мои картинки перестали ускоряться. Покопавшись в коде DataBufferInt, я нашел решение этой проблемы с помощью reflection и написал небольшой класс-помощник:

import java.awt.*;
import java.awt.image.*;
import java.lang.reflect.Field;

import sun.awt.image.SunWritableRaster;
import sun.java2d.StateTrackableDelegate;

// Standard library prevents image acceleration once getData() method is called
// This class provides a workaround to modify data quickly and still get hw-accel graphics
public class AcceleratedImage {
	// Returns data object not preventing hardware image acceleration
	public static int[] getDataBuffer(DataBufferInt dataBuffer) {
		try {
			Field field = DataBufferInt.class.getDeclaredField("data");		
	        field.setAccessible(true);
	        int[] data = (int[])field.get(dataBuffer);
	        return data;
		} catch (Exception e) {
			return null;
		}
	}
	
	// Marks the buffer dirty. You should call this method after changing the data buffer
	public static void markDirty(DataBufferInt dataBuffer) {
		try {
			Field field = DataBuffer.class.getDeclaredField("theTrackable");		
	        field.setAccessible(true);
	        StateTrackableDelegate theTrackable = (StateTrackableDelegate)field.get(dataBuffer);
	        theTrackable.markDirty();
		} catch (Exception e) {
		}
	}
	
	// Checks whether current image is in acceleratable state
	public static boolean isAcceleratableImage(BufferedImage img) {
		try {
			Field field = DataBuffer.class.getDeclaredField("theTrackable");
	        field.setAccessible(true);
	        StateTrackableDelegate trackable = (StateTrackableDelegate)field.get(img.getRaster().getDataBuffer());
	        if (trackable.getState() == sun.java2d.StateTrackable.State.UNTRACKABLE)
	        	return false;
	        field = SunWritableRaster.class.getDeclaredField("theTrackable");
	        field.setAccessible(true);
	        trackable = (StateTrackableDelegate)field.get(img.getRaster());
	        return trackable.getState() != sun.java2d.StateTrackable.State.UNTRACKABLE;
		} catch(Exception e) {
			return false;
		}
	}
		
	public static BufferedImage convertToAcceleratedImage(Graphics _g, BufferedImage img) {
		if(!(_g instanceof Graphics2D))
			return img;	// We cannot obtain required information from Graphics object
		Graphics2D g = (Graphics2D)_g;
		GraphicsConfiguration gc = g.getDeviceConfiguration();
		if (img.getColorModel().equals(gc.getColorModel()) && isAcceleratableImage(img))
			return img;
		BufferedImage tmp = gc.createCompatibleImage(img.getWidth(), img.getHeight(), img.getTransparency());
		Graphics2D tmpGraphics = tmp.createGraphics();
		tmpGraphics.drawImage(img, 0, 0, null);
		tmpGraphics.dispose();
		img.flush();
		return tmp;
	}
}



Использовать его нужно вот так:
DataBufferInt dataBuffer = (DataBufferInt)bufferedImage.getRaster().getDataBuffer();
int[] data = AcceleratedImage.getDataBuffer(dataBuffer);
// Меняем данные
AcceleratedImage.markDirty(dataBuffer);

Я не проверял этот код для изображений, которые уже были выведены на экран.

Нет встроенной анимации и полу-прозрачности. Объект javax.swing.Timer делает две вещи:
  1. Можно сделать анимацию компоентов.
  2. Из-за простоты класса, делать ее очень долго.

Есть библиотека Timing Framework, которая позволяет создавать анимации проще. Анимацию можно сделать вот так:

Animator viewAnimator = new Animator.Builder()
		.setDuration(duration, TimeUnit.MILLISECONDS)   //  Устанавливаем длительность анимации
		.setStartDirection(Direction.FORWARD)
		.setInterpolator(new AccelerationInterpolator(0.3, 0.7))    //  Заставляем двигаться с ускорением
		.setRepeatCount(1).addTarget(new TimingTargetAdapter() {
			@Override
			public void timingEvent(Animator source, double fraction) {
				// Меняем состояние
				repaint();
			}
			@Override
			public void end(Animator source) {
				// Делаем что-то по окончанию анимации
			}
		}).build();
viewAnimator.start();

Чаще всего используется анимация положения и полу-прозрачности. Если с контролем положения в Swing все OK, то полу-прозрачность стандартные компоненты не поддерживают. Проблема не в возможностях графического движка, а в том, что компоненты не имеют свойства getAlpha/setAlpha.

Java-приложение не будет запускаться в Mountain Lion из-за GateKeeper. Чтобы решить эту проблему вам нужно подписаться на программу Mac Developer за $99/год. В замен фирма Apple выдаст вам сертифакт для подписи кода и проблема уйдет.
Подписать бандл с приложением можно вот так:
codesign –s “Developer ID” –f “path-to-my-app.app”

В сумме


На мой взгляд, самый главный минус Swing – это неуверенность в будущем платформы, т.к критичные баги остаются открытыми. Своих фиксов дожидаются только уязвимость в браузерных плагинах. Складывается ощущение, что библиотеку бросили. Все остальные проблемы уже не так важны.

И мне будет очень грустно, если это действительно так. Потому что писать десктоп приложения на Swing быстро, просто и вокруг огромное количество готового и бесплатного кода.

Пока у меня остается надежда, что у разработчиков в Oracle появится время, чтобы решить системные проблемы.

UPDATE: Из комментариев выяснил, что кастомизация контролов в Qt возможна:

Спасибо пользователю silvansky
UPDATE: Пользователь Skyggedans заметил, что в Air появилась поддержка много-поточности. Делается она за счет дополнительных SWF файлов, с которыми можно настроить коммуникацию.
Tags:
Hubs:
+53
Comments41

Articles