Показаны сообщения с ярлыком Java. Показать все сообщения
Показаны сообщения с ярлыком Java. Показать все сообщения

суббота, 17 декабря 2011 г.

Отзывчивый интерфейс на Android

Если вы делате что-то сверх тривиального Hello World, то обеспечить "моментальную", без всяких тормозов реакцию интерфейса на действия пользователя дело не такое тривиальное, как это может показаться. Рассмотрим это на примере из простого графического редактора, который я сейчас разрабатываю.

 

Встроенного механизма для хранения всех операций рисования нет, при каждом вызове процедуры рисования мы должны нарисовать всю картинку. Первый, логичный шаг - сделать класс DrawOperation, хранить все экземпляры в упорядоченном списке и в процедуре рисования быстренько по этому списку пробежаться и все нарисовать. При этом заодно элементарно решается выполнение undo: удалили последний элемент из списка и привет. В каждом экземпляре DrawOperation хранится траектория движения "кисти", вид кисти, цвет, размер и т.п. Очень удобно и логично.

Проблема лишь в том, что "быстренько" не выйдет. Уже после нескольких мазков нашей виртуальной кистью отрисовка начинает заметно притормаживать.

Шаг второй, не менее логичный. Заводим виртуальный холст (Bitmap+Canvas), на котором и будем выполнять само рисование, а затем просто выводить его поверх заранее подготовленного фона. А список операций будем вести параллельно, он нам таки пригодится для undo.

Замечу (но разъяснять не стану, поверьте  - он понадобится), что в действительности придется завести еще один виртуальный холст, для текущей операции. По завершении они объединяются, а в процессе накладываются по очереди.

Все замечательно, но все равно притормаживает, а undo выполняется вообще неимоверно долго.

Шаг третий, вместо списка операций храним список "снимков" экрана. Сжатие в PNG позволяет нам хранить довольно большое их количество, делаем это во вспомогательном потоке с меньшим приоритетом, запускаем его когда текущая операция завершается и возникает естественная пауза, пока пользователь не ведет стилусом (пальцем, языком или чем там еще) по экрану, а переставляет его на новое место.

Undo вписалось в приемлемые рамки, теперь мы просто достаем предпоследний снимок, а последний удаляем. Ну а чего же оно тормозит-то ТЕПЕРЬ?

А сборщик мусора? В момент завершения очередного штриха, когда мы фиксируем результат операции, освобождаются некоторые ресурсы, сборщик мусора вызывается когда захочет, но почему-то всегда совсем не в тот момент, когда следовало бы. Т.е. нам бы этот процесс слегка отложить - пусть сначала все отрисуется, а потом будет время на уборку.

Шаг четвертый. Делаем статический класс Trash, который будет у нас содержать коллекцию объектов, которые надо бы удалить, но не хочется вызвать этим появление на сцене сборщика мусора. И конечно у него будет метод освобождающий весь этот хлам - в тот момент, когда мы надеемся на лучшее ничем важным не заняты. Это не гарантирует, что сборщик вызовется именно в этот момент, но скорей всего так и произойдет, особенно если подсказать системе вызовом System.gc()

Шаг пятый, шерстим весь код и избавляемся от лишнего создания/удаления объектов. Т.е. если объект можно использовать повторно, то лучше его повторно использовать. В частности не зачем для каждой операции создавать новый Bitmap+Canvas, лучше просто почистить тот, что остался от предыдущей.

 

И конечно лучше об этом думать сразу.

среда, 7 декабря 2011 г.

Ошибка резидента MotionEvent & SurfaceView

После пары дней пыхтения и потения над отладкой тривиального графического редактора, обкусав ногти до локтей и выщипав все волосы в разных местах выяснил два момента. Может кому пригодится если сумеет найти данный текст.

Первая проблема известна в англоязычных энторнетах как SurfaceView flickering. Так вот если у вас картинка моргает, то почти сто процентов уверенности, что вы накосячили с синхронизацией потоков. Т.е. рисуете вы в одном потоке, а готовите картинку в другом и где-то лопухнулись. Вдумчиво изучите как у вас происходит синхронизация, прокол может быть совершенно неочевидным. Второй вариант - вынести все действия по подготовке картинки в один поток, тот же, что занимается рисованием. Тут нас может подстерегать другая проблема, названия ей я не нашел, но крови она мне попортила изрядно.

Итак, есть у нас обработчик кликов:

boolean onTouchEvent(MotionEvent e)

и есть желание эту обработку перенести в тот же поток, где у нас происходит формирование картинки. К слову сказать, SurfaceView был разработан в частности для того, чтобы эту обработку разнести по разным потокам :), но иногда это все таки может быть нужно. Так вот, делаем мы это элементарно, вариантов много, но все они сводятся к тому чтобы поместить событие в очередь, а в другом потоке эту очередь опросить и обработать. Так вот внимание, сюрприз:

 

Объект MotionEvent e, который получает наш обработчик передавать в другой поток для отложенной обработки НЕЛЬЗЯ. Потому что он существует в единственном экземпляре и в наш обработчик передается лишь ссылка на этот экземпляр. И как только вы выйдете из обработчика (положив эту ссылку в очередь), система с чистой совестью может поместить туда новое значение. В результате, если обработка у нас запаздывает, и мы успели  положить к себе в очередь три события MotionEvent, то все три будут указывать на одно и то же событие.

В результате мы одновременно а) теряем события и б) они у нас дублируются. Я плакаль...

Хрен вы это где найдете в документации. Это наглядный пример результатов телепатической связи между разработчиками Android SDK (которые полагают подобное поведение очевидным) и конечными его пользователями.

Вывод: верить можно только себе, и то, с осторожностью. Т.е. любой системный объект (в данном случае MotionEvent) должен быть обработан там, где он получен  и не следует рассчитывать что он сохранит своё состояние после выхода из обработчика. Если нам нужно сделать "ленивую обработку", то нужно получить "твердую копию", т.е. объект состояние которого полностью контролируется нашей программой, а не системой.

понедельник, 5 декабря 2011 г.

Андроид. Вести с полей

Наконец-то я плотно занялся этим девайсом. Делаю несложный графический редактор, узнаю много нового и интересного.

 

Моя первая попытка сделать его с использованием OpenGL ES1.0 потерпела полнейшее фиаско. Для редактора пиксельной графики OpenGL (по крайней мере ES1.0) подходит как квадратные колеса для велосипеда. Т.е. как-то приспособить может быть и можно, и даже едет, но совершенно не тривиально и с жутким скрыпом.

 

Вторая попытка была более удачной. Обычный Canvas заточен как раз под пиксельную графику, без всяких заморочек с 3D. Осложняет задачу то что я с ним не работал никогда (в отличие от OpenGL) и многие проблемы и их решение для меня неочевидны. А от некоторых спецэффектов, появившихся в процессе работы у меня волосы встали дыбом во всех местах, я так и не понял как такое возможно. Скажем появился у меня объект класса Picture, который по разному рисуется при четном и нечетном вызове процедуры рисования. 

 

Пришлось сразу задуматься об оптимизации. Если рисовать всю картинку каждый раз заново - быстродействие падает стремительными темпами. Хранить все действия в Picture - возможный вариант, но не столь тривиальный, как хотелось бы. В результате мучительных поисков, решение было найдено и оказалось совершенно тривиальным, более того, о нем вскользь упомянуто в гугловской документации. Их документация это вообще отдельная тема. Полнотой и подробностью она не страдает. 

 

Если кому интересно - нужно создать Bitmap, обернуть его в Canvas и спокойно рисовать туда. А когда дело доходит до обновления экрана - рисовать этот Bitmap в Canvas экрана. В этом случае время отрисовки стабильно и достаточно невелико.

 

P.S. Всплыл интересный факт - размер сенсорной области у меня меньше видимой области экрана. Если разместить какой-то мелкий элемент на краю экрана, то для нажатия он будет недоступен. Практический пример: ползунок (SeekBar) шириной во весь экран невозможно довести до крайних положений.

понедельник, 28 ноября 2011 г.

Блокировка при записи в ObjectOutputStream

Получил сегодня такой интересный эффект. Разбирательство показало, что добавился класс, которому не задан интерфейс Serializable. При этом сам он в явном виде в поток не пишется, а может оказаться в значении поля внутри другого класса, который имеет Serializable и пишется в поток.

 

Но самая главная пакость заключается в другом. На тестовых примерах такая ошибка вызывает java.io.NotSerializableException, но в данном конкретном случае этого почему-то не происходит. Оладка показала, что ObjectOutputStream обнаруживает проблемы, но вместо того чтобы тихо мирно вывалить эксепшн, пытается записать его в выходной поток (writeFatalException)! Немножко поразмыслив понял что это правильно - на другой стороне в процессе приема объекта вместо очередного поля обнаружится эксепшен и вывалится с ним, а иначе мы рискуем получить блокировку уже там (поскольку объект целиком никогда не прийдет.) В моем случа это не важно, но универсальность должна быть универсальной, хуже от этого быть не должно... Однако стало и по прежнему неясно, где же приключился затык.

 

Смотрим дальше... затык происходит при вызове slotDesc.invokeWriteObject(obj, this) ,  где obj это экземпляр NotSerializableException. Теоретически должен вызваться метод writeObject этого класса.

NotSerializableException и его родитель, ObjectStreamException, IOException, Exception  нужного метода не содержат, обнаруживается он только в Throwable и тут мы снова возвращаемся к ObjectOutputStream, метод defaultWriteObject(). Все что я могу тут сказать - мы туда приходим и впадаем в ступор где-то на выводе состояния стека. Что там такого криминального обнаружилось я право не знаю. Отладка становится трудоемкой и слабоосмысленной без каких-нибудь специальных инструментов, которыми я не владею. 

суббота, 26 ноября 2011 г.

Berkeley DB Java Edition, ч.3

Импортировал в БД 7 записей, исходный файл занимал 2Кб, при том что большая часть это вспомогательный мусор, который в базу никоим образом попасть не может. Реальный объем данных - байт 100, не больше.

 

База после импорта заняла 100Кб. Проверил программу - ничего лишнего я туда не толкаю. Буду надеяться, что это просто резервируется место и следующие 7777 записей займут столько же сколько заняли 7. Иначе придется плотно разбираться что это за ерунда такая .

Berkeley DB Java Edition, ч.2

Для любого объекта, который хранится в BDB опосредованно (как часть другого объекта) его класс должен быть объявлен как @Persistent и иметь конструктор по умолчанию. Это не требуется только базовым типам вроде String, Long, Integer а также коллекциям (по крайней мере для ArrayList, до прочих у меня руки не дорасли).

 

И как я уже писал, если мы хотим хранить в БД потомков некоторогоо класса, то они все должны быть помечены как @Persistent (@Entity для них мы использовать уже не можем). И если мы хотим иметь к ним доступ не только через базовый класс, но и индивидуально, то они должны иметь поле помеченное как @SecondaryKey, причем имя поля должно быть уникальным среди всех потомков базового класса. Пример:

 

@Entity
class Base {
@PrimaryKey
String id;
Data data;
public Base() {}
}

@Persistent
class Data {
String somedata;
public Data() {}
}

@Persistent
class Child1 extends Base{
@SecondaryKey(relate=Relationship.ONE_TO_ONE)
String id1;
public Child1() {}
}

@Persistent
class Child2 extends Base{
@SecondaryKey(relate=Relationship.ONE_TO_ONE)
String id2;

public Child2() {}
}

Если вы фактически не используете SecondaryKey. то использовать

Relationship.ONE_TO_ONE

не обязательно, можно и

Relationship.MANY_TO_ONE

это избавит вас от заботы об уникальности ключа. Для SecondaryKey нет встроенного механизма генерации уникального значения, так сто это может быть полезно.

среда, 23 ноября 2011 г.

Berkeley DB Java Edition, ч.1

Осознал что пора задуматься о хранении данных и нахожусь в творческом поиске БД. Хочется:

  • нечто заточенное под хранение произвольных объектов с заранее неизвестной структурой. Т.е. в идеале - объектно-ориентированная БД, или просто key-value хранилище
  • нечто компактное, но потенциально расширяемое. Поднимать на недорогом (на дорогой нет денег) хостинге серьезную БД задача не для слабонервных. Это в любом случае не просто "с нуля", в том числен и с нуля знаний. MySQL в минимальном варианте я конечно подниму, но как раз MySQL мне не очень подойдет по п.1. Да и он достаточно прожорлив в плане ресурсов

По всей видимости мне нужна компактная "embedded" БД и после некоторых поисков я остановился на Berkeley DB. Она не совсем проходит по первому пункту, БД не объектно-ориентированная. Но собственно мне не так много этого объектно-ориентированного функционала и нужно. Т.е. не помешает, но ставить ради этого отдельный сервер БД я не готов.

Посмотрел еще всякие разные объектные БД, в том числе и встраиваемые в приложения, но либо откровенное не то, либо нехорошие отзывы, либо лицензия не подходящая. Попробую познакомиться поближе с BDB JE и обойти ее ограничения, которые начали всплывать практически сразу.

java.lang.IllegalArgumentException: An entity class may not be derived from another entity class

У меня сложная иерархия объектов, но хранить их нужно единообразно. Т.е. получив некоторый ключ, я не знаю заранее, объекту какого типа он соответствует. Для примера:

class Ship {
 ArrayList<Equipment> equipment;
}

Equipment это может быть и Gun и PowerGenerator и бог знает что еще. Если у меня есть необходимость хранить их как отдельные Entity (а такая необходимость в общем случае у меня есть), а не просто как часть Ship, то возникают некоторые сложности. А именно - Gun, PowerUnit и GoodOnlyKnowsWhatElse - это отдельные Entity и записать их вместе и просто получить потом по значению ключа одним запросом нельзя. Т.е:

PrimaryIndex<Key,Equipment> gunindex = store.getPrimaryIndex(Key.class,Equipment.class)

Gun gun=new Gun();
PowerUnit pu=new PowerUnit();
index.put(gun); // вылетит Exception

если же я сделаю

PrimaryIndex<Key,Gun> gunindex = store.getPrimaryIndex(Key.class,Gun.class)

PrimaryIndex<Key,PowerUnit> puindex = store.getPrimaryIndex(Key.class,PowerUnit.class)

то и записывать и получать я их потом буду отдельно:
gunindex.put(gun);
puindex.put(pu);
gun=gunindex.get(key);
pu=puindex.get(key);
а как я уже сказал - я не знаю заранее к чему относится key, т.е. придется делать обращение ко всем индексам чтобы выяснить к какому относится данный key. Либо закладывать в key информацию о типе объекта и выбирать нужный index.
Есть другой путь
Даже 2. Первый - использовать Base API вместо Direct Persistent Layer. Не вдаваясь в подробности - это возможность закопаться глубоко в недра БД и делать с ней все что захочется. Никаких надстроек, чистая работа с key-value и не важно что именно в них находится. По сути это ядро БД, на которое можно накрутить свою надстройку которая будет делать то что нужно и так как нужно именно вам. Но возни будет много.
Второй путь проще. Делаем обертку:
@Entity
class Item {
  @PrimaryKey
  Key key;
  Equipment data;
  public Item() {}
  public Item(Equipment dt) {
     data=dt;
     key=dt.getKey();
     }
}
а классы Gun, PowerUnit и GoodOnlyKnowsWhatElse помечаем как @Persistent и реализуем в них метод getKey. В результате мы имеем избыточность в данных, но зато все они вставляются и извлекаются единообразно как Item, чтобы у него там внутри не было. Можно добавить еще вторичный ключ который задает тип хранимого объекта. - избыточность возрастет еще, но можно будет выбирать объекты по их типу
Update: Все это бред
Ларчик открылся просто.
Есди у меня есть иерархия объектов, но я хочу хранить их вместе, то я должен пометить базовый класс как @Entity, а его потомки  - как @Persistent.
Тогда не нужны никакие левые обертки.

Некоторые замечания

Долго колебался, делать ли значения типа (корабля, оборудования, сообщения) цифровыми или строкой. В конце концов во многих случаях склонился к строке. Пусть это медленнее, и отнимает больше памяти но зато проще делать экспорт/импорт данных в/из человекочитаемый формат. Да и при отладке проще догадаться что означает "user.login" в поле msgtype, чем 42.

 

Жалко switch (строка) появился только в Java 1.7

вторник, 22 ноября 2011 г.

IOException: Read end dead

Сегодня затеял небольшой рефакторинг и очень удивился получив вот такой вот эксепшн и еще ряд интересных глюков. Выяснил интересную вещь:

допустим у нас есть поток 1, который создает поток 2, а сам тихо завершается - за не надобностью. Если у вас возник вопрос зачем это надо, сразу могу сказать - это только тестовый пример. В действительности у меня их запускается несколько штук, а родительский можно как-то использовать, но пока это не сделано, да и стройность программы от этого пострадает. 

Так вот, родительский поток 1 среди прочего инициализирует парочку PipedInput/OutputStream, а использоваться эта связка будет исключительно в потоке 2. Тут и ожидает сюрприз. Эти Piped как-то привязаны к потоку в котором были созданы, и если он завершается, то при попытке их использовать получаем тот самый эксепшн. Открытие неприятное ввиду того что

а) совершенно не очевидно

б) непонятно, касается ли это чего-то еще? Какие еще ресурсы не являются в этом отношении thread-safe?

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

д) непонятно, на что еще может повлиять такая зависимость? А не окажется ли, что используя некоторые объекты (те же PipedInput/OutputStream) я неявным образом могу вызывать блокировку потока 2, из-за необходимости  синхронизации его с потоком 1 (который в это время занят чем-нибудь)?

 

пятница, 18 ноября 2011 г.

Java и NPAPI

Низзя.

 

Вчера уже спать ложился, когда задумался - а нельзя ли сделать игру в виде плагина к браузеру? Сегодня с утра выяснил - в принципе можно, но не на java. Увы. Причина - интерфейс плагинов у популярных браузеров ориентирован на нативные плагины, написанные на языке Си. Либо кроссплатформенные плагины, написанные на JavaScript. Поддержки Java нет и не планируется, более того, отчетливо просматривается тенденция по выжиманию java из браузеров. Java - продукт проприетарный, и потому нелюбимый разработчиками открытого софта. Как по религиозным соображениям, так и в связи с лицензионными ограничениями. Ну а гугл со своим chrome видимо не желает плевать проив ветра и поддерживать не то чтобы конкурента... но с какой стати им вообще поддерживать постороннюю фирму?

 

Так что - апплет, вебстарт или обычное приложение. Ну и ладно, и этого хватит.

X3D vs Collada

Попробовал экспорт модели из блендера. X3D и Collada. До сих пор в думках, что лучше. И то и другое нормально стало работать с версии 2.59 (в 2.58 Collada отсутствует, X3D экспортируется с ошибками).

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

Вот такая модель корабля при экспорте в X3D занимает 3.7Мб против 530 Кб в виде blend файла

понедельник, 7 ноября 2011 г.

Проблемы новичков

Есть вещи, видимо настолько очевидные для опытных разработчиков, что они о таком никогда не пишут, считая что уж ЭТО настолько очевидно, что и разжевывать тут нечего. А новички сталкиваются с совершенно тупыми проблемами и не могут найти как же эту проблему решать.

 

Одна из таких проблем - работа с ресурсами. На Си меня совершенно не волновало куда положить всякие картинки, звуки и как их потом оттуда взять. Создал папку, накидал как хотел, если много - привел в порядок и поехали. На Java я первоначально именно так и поступил, но потом пришлось чесать репу:

чтобы сделать из приложения апплет, или сделать возможным Web Start - ресурсы должны хранится в jar. Т.е. можно конечно обойтись и без этого, даже в таких случаях, но некошерными методами. Главное засунуть их туда проблемы нет вообще - jar, это тот же zip, и положить что-то внутрь можно даже силами файлового менеджера, многие из них давно умеют обращаться с такими файлами как собычными папками. Но как потом из программы обратится к положенному?

Оказалось все достаточно тривиально (однако чувствуются некоторые подводные камни и спрятанные грабли). Итак, руководство для новичков, как обращаться с ресурсами в Java:

предполагается что вы пользуетесь Eclipse, в прочих IDE не должно сильно отличаться. 

  • создаем новый package: File->New->Package и даем ему какое-то разумное имя. Например resources. Или images
  • ложим туда все наши картинки. Если вы уже сделали простую папку и натолкали их туда - перетаскиваем мышой, если они лежат где-то отдельно, не подключенные к проекту - можно сделать импорт.
  • InputStream is=getClass().getResourceAsStream("/images/example.png") - получаем InputStream для файла example.png лежащего в пакете images
  • Вуаля! 

Если вы привыкли работать с File - отвыкайте. В общем-то он нужен вам чтобы получить все тот же InputStream. Если какие-то методы требуют File, то поищите - и обнаружите что они имеют вариант с InputStream.

Если вы сделали пакет resources.images, то обращение к файлу будет выглядеть так: 

InputStream is=getClass().getResourceAsStream("/resources/images/example.png");

Все точки в названии пакета заменяются на / и не забывайте про ведущий слэш.

Если обнаружите что можно сделать так

InputStream is=getClass().getResource("/resources/images/example.png").getFile();

то вдумчиво читайте описание всех упомянутых методов, ничего хорошего у вас так не выйдет :). 

Когда придет время явить свое творение миру, вы сформируете единый jar со всем что нужно для счастья при помощи maven, или ручками. Если второй вариант, то не забывайте: во время отладки эклипса сама формирует переменные типа classpath и прочую фигню так чтобы ваша программа видела свои ресурсы - но в bin, как откомпилированные классы, их не копирует. Так что когда будете делать jar, кроме классов не забывайте затолкать лежащие отдельно ресурсы - не нарушая структуры packages.

Т.е. если у вас класс yourpackage.YourApp и ресурс resources.images.example.png, то структура jar-файла:

  • yourpackage
    • YourApp.java
  • resources
    • images
      • example.png

Про нативные либы песня отдельная. Их ложат в отдельный jar, прямо в корень и подключают к апплету или web start application путем указания соответствующего элемента в jnlp файле.

Если вы не собираетесь делать апплет или webstart - просто положите в отдельную папку и в скрипте запуска вашего приложения укажите путь:

 

#!/bin/sh

java -Djava.library.path=lib -jar myapp-1.0.0-jar-with-dependencies.jar

 

в папке где у меня лежит приложение в виде одного jar со всеми прибамбасами и ресурсами myapp-1.0.0-jar-with-dependencies.jar есть еще папочка lib, где валяются нативные либы. Можно сделать несколько папочек, но тогда придется указывать их все через ":".