3DO и Android NDK и как бы во что не вляпаться…
Найдется наверное не мало приложений, которые почти невозможно сделать на Java, в силу большой исходной кодовой базы C++ или требований к производительности. И так вышло, что я разрабатывал одно из таких приложений, а именно эмулятор игровой консоли 3DO — Real3DOPlayer. В моем случае роль играла как кодовая база, так и требования к производительности. Код базировался на моем десктопном проекте «Феникс», и он тормозил даже на средних десктопах, не то что на встраиваемых процессорах. Сколько проклятий вырывалось в адрес корпорации Gooogle я уже и не помню, но опыт я получил бесценный, которым и хочу здесь поделиться.
Новичок, бьющийся лбом об стену…
Несмотря на то, что я опытный программист (больше 15 лет), я почувствовал себя в шкуре новичка сразу же, как только взялся за NDK.
Шишка первая. Установив Android Studio под Windows, я решил набросать простейший «Hello, world!» с вызовом JNI. Все очень просто:
JNIEXPORT jstring JNICALL Java_ru_arts_union_real3doplayer_NativeCore_stringFromJNI(JNIEnv* env, jclass clazz)
{
return env->NewStringUTF("Hello, World!");
}
Ошибка компиляции. Просто ошибка компиляции. Понимаете, код не компилируется и точка. День я пытался скомпилировать код, активно гугля проблему, но по истечению дня код так и не удалось собрать. На следующий день взял ноутбук с Linux и решил продолжить мучать эти две строчки, к моему удивлению, все сразу же собралось. Начал читать форумы, почему под Windows нельзя собрать код? Оказалось — одного файла C/CPP мало для для сборки проекта, требуется как минимум 2!
Шишка вторая. Запускаю приложение, а оно падает, сообщая, что метод stringFromJNI не найден. Как так не найден?! Вот же он, прямо тут и в дизассемблере виден! Не паниковать, смотрю весь код от и до, слава богу, его не так уж много. Смотрю на префикс в названии функции ru_arts_union_real3doplayer, и что-то мне подсказывает, что что-то здесь не так… Ведь имя моего пакета выглядело так: ru.arts-union.real3doplayer, а имя метода кодирует точку и тире одинаково, это не хорошо, и странно, что сама среда допустила такое имя для пакета. Меняем имя пакета и метода, убрав тире. Ура! Метод видится, и спустя два дня работает «Hello, world!».
Шишка третья. Не беда, файлов будет много, название поправили, едем дальше, думая, что самое страшное позади. Добавляем свои исходники, которые проверялись под Linux/Windows для различных процессоров (в том числе ARM). Все отлично скомпилировалось, без единой ошибки. Приложение падает при старте без каких-либо предупреждений. Продолжая мидитировать над кодом, наблюдаю проплывающую строчку с long double, вспоминаю, что NEON максимум держит double, но этот код даже не вызывается, он просто есть, а в заголовке типов я удалил real80. И потом, код ведь скомпилировался без ошибок! Хм…, чем стучать в бубен, лучше выпилю этот код… Запуск, и бинго! Код работает, игры заускаются, хоть и с дикими тормозами!
Сюда же стоит упомянуть работу с казалось бы банальными вещами — стандартными библиотеками языка С, будьте осторожны, довольно часто случается так, что некоторые методы, которые присутствуют в одной версии — отсутствуют в другой, и ваше приложение падает из-за невозможности загрузить нужный метод. Тут поможет только обширное тестирование или отказ в пользу STL со статической линковкой. Так, например, TinyXML падает на достаточно большом количестве устройств из-за отсутствия функций преобразования чисел в строку и обратно.
Чем меньше у вас опыта, тем больше у вас будет шишек — будьте морально готовы, наверняка я не все припомнил…
В погоне за производительностью или вляпался раз…
Как увеличить производительность эмулятора, который местами напрягал даже i7? Очень просто, нужно ограничить себя вычислительным ресурсом, что и произошло с попыткой портировать его на Андроид.
В такой непростой ситуации пришлось реализовать кеширование текстур с предварительно частично или полностью обработанными пикселями графическим процессором приставки; триангуляцию квадратных полигонов (у 3DO нелинейное наложение текстур); статическую рекомпиляцию кода, часто используемых библиотек ОС консоли; распараллеливание эмуляции подсистем консоли. Все это позволило снизить аппаратные требования более чем в три раза. Где еще можно сэкономить такты, не считая классических техник, оптимизации на ассемблере и длительного просиживания в профайлере? Очевидно, на том, в чем большинство пользователей разницы не заметит. Так, например, я хорошо сэкономил на 16-битном растре, вместо 24-битного, при заполнении кадра.
Сам процесс оптимизации очень занятный и тянет на отдельную статью, но все вышесказанное не относится к Андроиду как к таковому. А что же может дать нам сама платформа и ее особенности? Очевидно прямую запись в текстурную память, поскольку CPU и GPU на мобильных платформах в подавляющем большинстве устройств ее разделяют. Но увы, данный механизм, известный как GraphicBuffer, не предназначен для общего пользования! Ничего, его можно достать через dlopen. Но делать это надо опционально, поскольку этот хак может работать не везде. Реализацию можно глянуть здесь (довольно непросто найти через поиск): android.googlesource.com/platform/external/deqp/+/deqp-dev/framework/platform/android/tcuAndroidInternals.cpp.
А что нам очень не хочет давать Андроид, но очень надо? Конечно контроль над циклом приложения! Но, если ооочень надо, то поможет NativeActivity или вот этот вот примерчик, который подкупает своей простотой и удобством: github.com/tsaarni/android-native-egl-example. В отличии от NativeActivity, с которым непонятно как работать из обычного Activity, данный подход позволяет захватить контекст окна и рисовать в отдельном потоке, тогда, когда надо нам, а не когда решит Java. И вот тут-то я вляпался, сам того не зная.
Дело в том, что данный механизм работает не на всех версиях Андроида (по крайней мере по моим тестам, версии 4.1–4.3 его не поддерживают), а это порядка 40% целевых устройств для моего приложения, судя по статистике Гугла, т. е. 40% прибыли долой. И узнать вы об этом можете очень поздно, ведь обычно пользователь поставив приложение и увидев, что оно не работает, сразу сносит его и не пишет отзыва, оно и понятно, ведь надо быстрей вернуть деньги. Исправлять же неправильный цикл приложения — дело не из приятных, ведь гарантированно определить на каком устройстве он работает, а на каком нет — нельзя, ставить по умолчанию более медленное решение, но более совместимое — можно, но карму вы себе попортите, ведь не каждый полезет в настройки проверять: «А нельзя ли что-то подкрутить, чтобы стало как раньше и не тормозило?» Лучше сразу сделать более дубовый вариант, а потом уже добавить опцию, но в моем случае уже поздно.
Сокращая сущности или вляпался два…
Довольно часто в SDK уже есть функционал, который не хочется дублировать в нативном коде, например, для работы со шрифтами или изображениями, да много чего, ведь у Java огромная библиотека. И возникает закономерное желание сделать вызов Java-метода из C++. У меня как раз такое желание возникло, и я сделал классную вещь — рендер шрифтов в текстуру по запросу из нативного кода, все протестировал на своих устройствах и куче виртуальных устройств. Все отлично работало, и в очередной раз вляпался — сделал релиз… У примерно 1% пользователей (уже купивших приложение) программа начала крашиться. Это было, мягко говоря — очень плохо, негативные отзывы, да и сама ситуация — человек заплатил и тут бац — у него не работает. В чем причина, сказать трудно, подозреваю некоторые производители используют модифицированную ось или что-то еще. Ошибку у себя я конечно не исключаю, но глядя на такой вот рапорт, я сильно в этом сомневаюсь (разумеется метод такой был, черным по белому):
java.lang.NoSuchMethodError: no method with name='loadConfig' signature='(Ljava/lang/String;)Ljava/lang/String;' in class Lru/vastness/altmer/real3doplayer/MainActivity;
at dalvik.system.NativeStart.run(Native Method)
Пришлось очень быстро выпиливать это решение и заменять другим. Обошлось малой кровью. Само по себе решение очень хорошее (1% несовместимости можно стерпеть, просто пройдут мимо и не купят), но только до запуска приложения, потом, когда продано уже много копий — я бы настоятельно не советовал.
Обновляя арсенал или вляпался три…
Переставив систему, я поставил SDK поневее и, соответственно, targetSdkVersion тоже поменял. Тут же случилась неприятность. Перестали видеться SD-накопители, что вызвано странными махинациями Гугла с правами доступа. При этом загрузка APK с более старой версией SDK оказалась возможной! Нельзя! Но я отделался легким испугом, проблема решилась запросом разрешений. А если проблема не решилась бы так легко? Что делать тогда? Вопрос очень интересный…
Самая же неприятная вещь, пришедшая с обновлениями, от которой не могу избавиться по сей день — это нежелание среды обновлять APK при изменениях в нативной библиотеке, проблему наблюдаю и под Linux и под Windows. Это вызывает раздражение, теперь чтобы на устройстве в процессе отладки появился актуальный свежесобранный APK, надо проделать следующее (Android Studio 2.1.1):
1. Пересобрать проект.
2. Запустить проект (при этом он пересоберется еще раз, если первый шаг пропустить — не пересоберется).
3. Остановить приложение.
4. Собрать APK.
5. Запустить приложение.
А вы думали, если сделали ребилд, то запустив приложение вы получите актуальный свежесобранный APK на устройстве? Не тут-то было, проверяйте, а лучше сделайте себе всплывающее окошко или сообщение в лог с версией изменений. Иначе рискуете потерять кучу времени на проверках своего исправного кода. И не надейтесь, что со второго или третьего запуска получится.
Тем у кого аналогичные проблемы, в качестве компенсации времени, если этого еще не сделали, советую указать jobs N для параллельной сборки нативного кода.
В сухом остатке…
Я вообще в осадке от средств разработки на эту платформу, такой кривизны уже лет десять не видел, а мне есть с чем сравнивать. Я конечно понимаю — Java в приоритете, но все же… С этим всем может потягаться разве что найденный мной много лет назад баг в интеловском компиляторе при сдвиге на ноль в переменной, с тех пор нет, нет, да напишу:
if(shift)return x>>shift;
return x;
Тем не менее, стоит отдать должное Гуглу, они сделали все, чтобы зарплаты у нативных разработчиков под Андроид были высокими!
Для меня, как С++ разработчика, ситуация с приложениями под Андроид выглядит парадоксальным образом, с одной стороны механизмы нужные есть, но совокупный объем проблем в платформе такой, что при попытке сделать какие-либо улучшения, я должен выбирать между потерей репутации и развитием приложения. Ни одна платформа не ставила меня перед таким выбором.
Невольно ловишь себя на мысли, что может лучше ничего не улучшать под эту платформу, а то как бы чего не случилось…
ПС. Не подумайте — эмулятор будет развиваться и дальше, просто накипело.