Пресловутая «нативка»
Со дня возникновения мобильного геймдева, разработчики борются с нативными плагинами для 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 выросла и останавливаться даже не думает.
Всё, что для этого нужно – это следование простым правилам, понимание юз-кейсов, и совсем чуть-чуть тактического мышления. Разработка будет дешевле, результат стабильнее, релизы предсказуемее, утопия ближе. Так что идёмте работать и процветать.