Пресловутая «нативка»

Со дня возникновения мобильного геймдева, разработчики борются с нативными плагинами для Unity. Не интегрируют, не внедряют, а именно борются. Размахивая заплатками и костылями. Обливаясь слезами и потом.

Десять лет я разрабатывал нативные плагины и фреймворки для Android и iOS, а затем почти три года интегрировал, поддерживал и фиксил SDK-шки в геймдеве. Сейчас я готов ответственно рассказать, что это за три буквы, какие бывают SDK для Unity приложений, где можно провалиться с разработкой, а главное – как сделать так, чтобы не провалиться.

Какие бывают SDK

Unity разработчики обычно понимают термин SDK (Software Development Kit) в более широком смысле и включают в это понятие все подключаемые библиотеки, плагины, пэкейджи и иже с ними. В джентльменский набор SDK-шек Unity приложения чаще всего входят:

  • реклама – плагины рекламных сетей и их агрегаторы (Unity Ads, Google AdMob, MaxSDK, etc);

  • нативные логины (Facebook, Apple Sign-In, Play Games, etc);

  • пакетики от Unity (IAP, Addressables, Notifications, etc);

  • аналитика/треккинг (Adjust, MaxSDK, Facebook, Firebase, etc);

  • здоровье приложения (Sentry, Firebase, etc);

  • и другие third-party нативные плюшки.

Это далеко не полный список, лишь несколько примеров. Суть у них одна – они берут на себя выполнение некой конкретной задачи, спрятав под капот нюансы более низкоуровневой реализации и предоставляя программистам удобный интерфейс управления.

Как делают SDK

Разработка плагинов и SDK ничем не отличаются от разработки любого современного программного обеспечения. Возьмём довольно сложный и болезненный пример – рекламную сеть. Придумывается сочный продукт и под него собирается MVP (Minimum Viable Product — минимально жизнеспособный продукт). Этот MVP состоит из большого сложного бэкэнд решения и крохотного клиентского кусочка. На бэке крутится биддинговая система (аукцион за показы рекламы), API для рекламодательской админки, подключаемые модули для медиации, какие-то платежки, верификаторы, иногда сервис стриминга. У админки есть веб-приложение, т.е. помимо самого фронтэнда – деление на роли, сбор статистики, поддержка конкурентного доступа, балансировка реквестов, и еще куча всего. Серьезный в общем-то энтерпрайз.

Крохотный же клиентский кусочек – это как раз наш SDK. Его функция очевидна – загружать рекламный креатив, показывать его, и, в случае успеха, говорить серверу, что можно брать деньги с рекламодателя. Из-за простоты работы этого кусочка, внимания ему немного и поэтому чаще всего этот клиентский кусочек собирают на коленке. Мало того, сам по себе он – не продукт, и отношение к нему соответствующее.

Главное не забыть прикрутить драфтовый API для общения с серверной частью и красиво назвать xxxSDK. Если про «SDK» в названии не забыли, значит можно отдавать софт в пользование клиентским программистам в их продуктах. Клиентские программисты изворотливо интегрируют это творение, в процессе генерируя обильный фидбэк. Как правило фидбэк довольно простой, чтобы:

  • перестала падать сборка билда;

  • методы API выполняли то, что написано в документации;

  • инициализация была разбита на логические части;

  • функции были идемпотентными или хотя бы детерминированными.

SDK-разработчики к фидбэку внимательно прислушиваются и делают всё возможное. Естественно прямо поверх своего уже готового MVP.

После нескольких таких итераций SDK превращается в ходячий замок Хаула, который безусловно продолжает выполнять свою простую функцию, но бонусом производит бесконечные краши с анрами, для инициализации появляется пять способов из которых обязательно нужно одновременно юзать два, и образуется идеальный клубок анти-паттернов программирования – хоть в парижскую палату мер и весов неси. Давайте посмотрим на конкретные примеры ниже.

Животрепещущие примеры

Сразу оговорюсь, к чести SDK-разработчиков, многие из перечисленных проблем уже исправлены в более свежих версиях. Тем не менее, приведу здесь несколько SDK-related бед, которые крепче всего застряли в моей памяти.

  • Zendesk SDK. Для того, чтобы спросить у Zendesk-a, нет ли у саппорта сообщения для юзера, нужно прям на сцене создать экземпляр полуметрового префаба. Вообще любое API в Zendesk только через этот префаб;

  • Facebook. Если инициализовать Facebook SDK с пустым appId, то sdk бросит наружу злое исключение. Facebook SDK в принципе любит результат своей работы представлять исключением – есть стойкое ощущение, что его писали Java-программисты;

  • Снова Facebook. В dll-ке Facebook.Unity.Editor.dll есть editor-скрипт, который делает [PostProcessBuild] и построчно коверкает код UnityAppController.mm – очень интересно реверс-инжинирить полученный iOS-билд со сломанной логикой работы обновления SafeArea;

  • MaxSDK. Коллбеки статуса показа рекламы взаимозависимы и пробрасывают исключения на самый верх. А еще крестик закрытия рекламы станет доступным для нажатия только когда придёт ответ на веб-реквест о том, что реклама просмотрена. В результате – незакрываемая реклама, если веб-реквест почему-то не преуспел;

  • SafeDK. Штука призванная следить за качеством рекламных креативов, весьма неожиданно влияет на качество собственной донорской аппки, через версию продуцируя львиную долю ANR-ов в Android билдах;

  • Sentry. Штука для слежения за стабильностью приложения в одной из версий крашит каждую десятую сессию на Android девайсах.

Однако, проблема не только в самих SDK, но и в том как мы ими пользуемся.

Как нужно использовать SDK

Клиентские разработчики привыкли читать документацию, смотреть в примеры реализации, утаскивать их себе, а затем методом проб и ошибок пытаться заставить эту конструкцию работать.

Как ни странно – вполне себе рабочий подход. Дополним его несколькими крохотными, но очень важными пунктами, на которые стоит обратить внимание в процессе.

1. Разделить все SDK по критичности

Примем тут, что критическое – это IAP и Addressables. Если что-то упало там, действительно стоит ребутнуть игру. Пусть даже через краш. Вылета игры между оплатой и выдачей награды за покупку желательно избегать, но в целом – вполне законно. Некритические SDK – это всё остальное. Отломалась вся реклама – у нас должен быть вариант работы игры без рекламы: рекламные офферы должны исчезнуть, точки рекламной монетизации погаснуть – продолжаем работать будто ничего не произошло. Пропал хаптик – никак не реагируем. Геймплей идет, платежи проходят, анимации крутятся – всё великолепно. Легко переживём одну сессию без вибраций. Отлетел логин по FB/AppleID/GoogleAccount – попробуем перелогиниться. Не получилось – игнорируем. И так далее.

2. Имплементация

Разделили по критичности, поехали имплементить. Как универсальное решение – оборачиваем все вызовы нативки в try/catch и корректно обрабатываем обе ветки – и успешную, и упавшую. Даже если сейчас, в условной версии X, ничего не падает, нет гарантии, что тот же самый метод в том же контексте не решит плеваться исключениями в версии X+1. Суть тут в том, что доверять SDK нельзя. Каждый раз при использовании сторонней библиотеки мы должны рассматривать два варианта исхода – всё прошло ок, и библиотечка приказала долго жить.

3. Поддержка

Всё заимплементили, надо поддерживать. Для этого внимательно следим за последними изменениями в чейнжлогах и максимально часто обновляем SDK. Да, я знаю, Unity-разработчики обновляют нативку только когда уже другого выхода нет – например, стор начинает отвергать билд со старой либой, потому что она начинает нарушать новую политику магазина. И нас можно понять – неизвестно, какие программистские изыски решили в этот раз опробовать создатели SDK и в какое неожиданное место подложили очередной невероятный баг. Однако, этот подход неприемлем по ряду причин:

  • Во-первых, накопление дельты изменений внутри SDK между нашей старой версией и той, на которую обновляемся. Тратим время на погружение, на починку обратной совместимости, на внедрение нового API;

  • Во-вторых, SDK-разрабы иногда делают такие полезные штуки как багофиксы. И часто об этом никак невозможно узнать, если не пустить новую версию SDK в продакшен. Или хотя бы не потрогать её руками. Чем больше у нас вариантов для сравнения, тем выше шанс найти оптимальную текущую версию конкретного SDK; 

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

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

В идеале, если мы достаточно обеспеченная компания, лучше воздержаться от использования SDK-шных Unity-оберток над нативными плагинами везде, где это возможно, и заменить их своими вропперами. Особенно это касается рекламных сеток. Как правило, нативные библиотеки сделаны лучше и логичнее чем их C# обертка. Оно и понятно – мобильный разработчик целится в конкретную платформу, обеспечивает корректную работу именно на этой платформе еще и по канонам этой платформы. Затем уже другой программист делает то же самое но совсем по-другому для второй платформы. Следом приходит местный «малтитэлентед девелопер» и наспех заворачивает все это в статический C# класс, выпячивая все, что следует спрятать, и запечатывая то, что должно торчать наружу. И наша задача тут определить, что дешевле – приседать с мятой оберткой от SDK-разработчика или каждый апдейт нативки подтюнивать свой авторский вроппер.

Если к этому моменту вы подумали, что я буду учить жизни только клиентских программистов, то вы не угадали. Страдания Unity-разработчиков можно было бы кратно уменьшить, если бы создатели SDK придерживались простых правил.

Как (не)нужно делать SDK

Не переусложнять. Проще == лучше. Например, в iOS плагинах рекламных сеток часто встречается модный key value observer, еще и сокрытый в проприетарных кусках. Чертовски неподходящий паттерн для разработки SDK. Ведь какая наиболее частая ошибка сыпется из этих кусков? Правильно – подписались, отписаться забыли, наблюдатель уже выгружен из памяти, а ему шлют нотифы. Краш летит наружу и роняет игру. Не надо так. Используйте простые паттерны. Key Value Observer безусловно хорош для расширяемости, когда программист докидывает в мапку новый айтем – и вот за пару минут появился новый ивент с драфтом обработчика. Если это взаимосвязанная цепочка ивентов, то и обработчик не нужен. Но в SDK это абсолютно бесполезно, потому что клиентский код в душе не чает про ваш обзёрвер, и на каждый ивент приходится руками делать отдельный клиентский метод. И вот вся ваша великолепная архитектурная оптимизация вместо экономии времени вылилась в жуткий пик на графике крашей. Не надо так. Используйте простые паттерны.

Не пускать наружу исключения, ловить их все внутри. Этому можно посвятить отдельную статью, но тут в двух словах. У нас не токарный станок, который без экстренной остановки отпилит токарю руку. У нас игра, которая логинится в Facebook. Зачем ей падать замертво, если залогиниться не получилось? Кажется, это чересчур. Наш язык программирования – не Java, где бросабельность исключения прописывается прямо в сигнатуру метода. У нас C#, в котором никогда не знаешь откуда выстрелит. И было бы неплохо сделать так, чтобы вообще не стреляло.

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

Quality Assurance. Замокать Unity-приложение, взять десяток настоящих кейсов использования, автоматизировать их, и перед релизом новой версии SDK прогонять эти кейсы на пяти реальных девайсах. Невероятно, но это позволит избежать львиной доли проблем в конечных продуктах. 

И наконец, Customer Driven Development для API – высовываем наружу только то, что действительно нужно клиентскому коду, а не как получится. Это очень больная тема любых SDK, при кажущейся простоте, тут очень надо постараться, чтобы не наделать ерунды, но оно того стоит. Очень полезно, если разработчики SDK сами пользуются своим SDK, и естественным путём накапливают кейсы использования. Но это большая редкость, поэтому следует периодически опрашивать своих клиентов, просить присылать куски кода, узнавать о больных местах и итеративно улучшать свой API.

Заключение

Дописывая этот опус, я представляю мир, где все эти правила соблюдаются. Unity-разработчики сидят в обнимку с создателями SDK, и со счастливыми улыбками на умных лицах программируют новый агрегатор рекламных сеток. Ответственные за техническую стабильность на проектах забыли как в Google консоли открывать Android Vitals, все рекламные креативы досматриваются до великолепной кульминации, телефоны довольно урчат хаптик-вибрацией в руках крепко платящих китов, а проекты каждый квартал сыто отчитываются топ-менеджменту, что несмотря на найм Android-разраба и iOS-программиста в поддержку Unity разработчикам, EBITDA выросла и останавливаться даже не думает.

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

Комментарии (1)


  1. HexGrimm
    16.09.2024 10:51
    +2

    Хочется дополнительно немного кинуть камней в сторону Unity, как фасилитатора кривого апи и некрасивой инициализации. Если не продумать модульность встраивания то каждой команде придётся писать её самой. Это речь и про порядок инициализации, это и про колбеки, которые почему то должны быть на префабах и про Unity врапперы у которых приватные конструкторы и сходи попробуй написать юнит-тесты. Было бы неплохо в движке легаси почистить, и вариантов выстрелить в чью то ногу у разработчика враппера станет меньше.