Одна из главных сложностей Android-разработки — фрагментация. Практически каждый производитель меняет Android под свои нужды. Разработчик Андрей Макеев перечислил отличия между реализациями вендоров и оригинальным Android Open Source Project. Из доклада можно узнать, как извлечь пользу из индивидуальных особенностей прошивок на разных устройствах.
— Программированием я занимаюсь со школы, под Android разрабатываю года три. Из них год я провел в Яндексе, участвовал в таких проектах, как Лончер и Телефон.
Хочу рассказать о том, как выглядит фрагментация API Android-устройств — как снаружи, со стороны разработчиков приложений, так и изнутри, с точки зрения разработчиков платформы и телефонов.
Мой доклад состоит из двух частей. Сначала поговорим о том, как API фрагментируется внешне. Потом пройдем по коду — узнаем, как реализуется уникальная фича абстрактного телефона, как строится разработка.
Фрагментация API — один из параметров, по которому можно фрагментировать устройство. Самая очевидная и простая — фрагментация по версии Android SDK, мы с ней каждый день сталкиваемся, буквально с первых дней разработки под Android. Мы знаем, что и в какой версии API появилось, что убрали, что задеприкейтили, но оно еще доступно, и чего уже нет. Как правило, мы пользуемся различными библиотеками поддержки от Google, чтобы упростить нашу жизнь. Многое уже сделано за нас.
В нашем коде это выглядит как-то так: мы какую-то функциональность включаем, какую-то выключаем — в зависимости от того, в какой версии SDK мы сейчас находимся. Если вы используете либы, то как правило, они делают то же самое, но внутри.
Не будем заострять много внимания на этом типе фрагментации. Минусы всем известны — мы вынуждены держать целый парк девайсов с разными версиями, чтобы хотя бы протестировать наше приложение. Кроме того, мы вынуждены писать лишний код. Особенно это неудобно, когда ты только начал разрабатывать под Android и вдруг выясняется: надо поизучать, что там было года два-три назад, чтобы какие-то прежние девайсы поддержать. Положительные стороны: API развивается, технологии двигаются вперед, Android приобретает новых пользователей, становится удобнее как для разработчиков, так и для пользователей.
Как мы с этим работаем? Мы также используем библиотеки и очень радуемся, когда отказываемся от поддержки старых версий. Думаю, в жизни у каждого, кто занимается этим больше года, был такой момент. Это прямо счастье. Это очевидный и простой вариант фрагментации. Далее — фрагментация по типу Android. Их существует несколько. Android TV говорит сам за себя, Android Auto предназначен в основном для автомагнитол, Android Things — для IoT и аналогичных embedded-устройств. W — это Wear OS, бывший Android Watch, часики для Android. Мы попробуем рассмотреть, как это выглядит со стороны разработчика. И, что самое интересное, попробуем рассмотреть, как это выглядит изнутри.
Берем два примера с developer.android.com. Первый — Wear OS. Что нам нужно, чтобы сделать приложение? Мы добавляем compileOnly зависимость в build.gradle и прописываем в манифесте две дополнительных строки: uses-feature android.hardware.type.watch и uses-library, которая соответствует тому же самому имени пакета, что и библиотека, которую мы подключили.
Как мы что-то реализуем? Создаем Activity, только в данном случае мы эстендим не стандартную activity, с которой привыкли работать, и даже не компадную, а WearableActivity, и вызываем специфичные для нее методы, в данном случае setAmbientEnabled(). Итак, у нас compileOnly-зависимость, то есть она не попадает в ход нашего приложения. Uses-library, которая, видимо, вынуждает ОС подключить эти классы и этот код нам в runtime на устройстве, и новые классы, которыми мы пользуемся.
Android Things API практически не отличается. Мы не прописываем uses-feature, только uses-library, compileOnly-зависимость.
Создаем activity, в данном случае это уникальный для Android Things API, класс PeripheralManager. Мы опрашиваем GPIO и пытаемся залогировать.
Как такое приложение поведет себя на вашем телефоне? Есть два варианта.
Если мы указали, что uses-library android:required=”true”, то мы не выполнили обязательные требования PackageManager для установки приложения, и он в принципе откажется его устанавливать. Если мы указали android:required=”false”, то приложение установится, но при попытке обратиться к классу PeripheralManager мы получим NoClassDefFoundError, потому что в стандартном Android такого класса нет.
Какие отсюда выводы? Мы подключаем compileOnly зависимость только для того, чтобы с ней синковаться при сборке, и те классы, которые мы используем, ждут нас на устройстве, подключаются они при помощи определенных строк в манифесте. Иногда мы прописываем фичу, которая чаще нужна, чтобы в Google Play различать устройство, которому можно или нельзя раздавать данное приложение.
Негативных сторон такого вида фрагментации я не смог выделить. Разве что те, кто разрабатывал, расскажут много историй про то, как встретили совершенно непонятные незнакомые баги, с которыми на телефоне не сталкивались. Положительные стороны, что это дополнительные рынки, дополнительные пользователи, дополнительный опыт, это всегда хорошо.
Как с этим работать? Пишите больше приложений. Общая рекомендация — писать не одну версию приложения на все типы Android, а все-таки делать разные. Для часов должно быть меньше, для Android Things практически ничего из того, что на телефон пишется, не подходит, ну и так далее. И пользуйтесь библиотеками, которые нам предоставляют разработчики Android и, иногда, разработчики устройств.
Следующий наименее изученный вид фрагментации — фрагментация по производителю. Каждый производитель, получив исходный код — в редких случаях это AOSP, чаще он как-то модифицирован разработчиками железа, — вносит в него изменения. Как правило, о негативных эффектах такого типа фрагментации мы узнаём из не самых лучших каналов — из негативных отзывов в Google Play, потому что у кого-то что-то сломалось. Или мы узнаём это из crash-аналитики, когда вдруг что-то крашится непонятным образом, только на каких-то определенных устройствах. В лучшем случае мы узнаем это от наших QA, когда у них при тестировании на определенном устройстве что-то сломалось.
На эту тему у меня есть замечательная история из нашей разработки Лончера. Нам пришел баг-репорт, где активити не растягивалась на весь экран, а наша любимая дефолтная обоина не отображалась вообще. Она не раскодировалась, показывалось пустое окно. У нас даже не было устройств, чтобы это воспроизвести. По растягиванию на экран мы все-таки смогли найти low-end-девайс, на котором это не работало, и достаточно легко всё поправили при помощи android:resizeableActivity=”true” в манифесте. С обоиной все вышло намного сложнее. Мы порядка двух дней пытались достучаться и получить более детальную информацию. В конце концов выяснили, что на ряде устройств либо отсутствует, либо с багами реализован кодек для раскодировки прогрессивных jpeg, когда использовано несколько алгоритмов сжатия на результатах друг друга. В данном случае мы просто написали lint-проверку, которая фейлила билд при сборке приложения, если мы клали в сам apk обоины, закодированные прогрессивным образом. Перекодировали все обоины, повторили ситуацию на бэкенде, который раздает остальные wallpaper sets, и все замечательно работает. Но это стоило нам порядка двух дней разбирательств.
Примерно так это выглядит в коде. Неприятно, но к сожалению, вот так. Обычно эти строки после длительного дебаггинга появляются.
Какие нам Google дает гарантии на предмет того, чтобы API не было сломано настолько, чтобы приложения не работали в принципе? В первую очередь, есть CDD, где описывается, что можно, а что нельзя менять, что является обратно совместимым, а что нет. Это документ из нескольких десятков страниц с общими рекомендациями, которые, естественно, не покроют все кейсы. Чтобы покрыть больше кейсов, есть CTS, который необходимо пройти, чтобы телефон получил сертификацию от Google и с него можно было пользоваться гугловыми сервисами. Это набор примерно из 350 000 автоматизированных тестов. Также есть CTS Verifier, обычный APK, который можно поставить на телефон, чтобы провести ряд проверок. Кстати, если вы покупаете телефон с рук, можно его так проверять.
С появлением Treble появился проект VTS, это скорее для разработчиков более низких уровней. Он проверяет API драйверов, которые, начиная с Project Treble, версионируются и тоже подвергаются подобным тестам. Кроме того, сами разработчики телефонов — здравые люди, которые хотят, чтобы Android-приложения нормально у них работали, но это так себе надежда. Отрицательная сторона в том, что мы встречаемся с непредвиденными багами, которые невозможно предсказать, пока приложение не было запущено на устройстве. Мы опять вынуждены покупать, помимо того, что разные версии API, еще и дополнительные устройства разных производителей, чтобы проверять на них.
Но положительные стороны есть. Как минимум, самые часто реализуемые производителями фичи попадают в сам Android. Кто-то может помнить, что стандартный Fingerprint API появился позже, чем устройства, которые могли разблокировать экран по отпечатку пальцев. Сейчас, если верить XDA Developers, разблокировку при помощи камеры по лицу тоже хотят сделать API Android, но это пока не точно. Мы скорее всего с вами об этом узнаем.
Кроме того, сами разработчики устройств, когда делают нестандартные API, они могут, и многие публикуют библиотеки для работы с их API для обычных разработчиков. И если вы этого ни разу не делали, советую пройтись по статистике использования вашего приложения, посмотреть, какие самые популярные производители, и заглянуть на девелопер порталы их сайтов. Думаю, вы приятно удивитесь, у многих есть API либо с интересными хардварными фичами, либо с секьюрити фичами, либо с облачными сервисами, либо еще с чем-то интересным. И на первый взгляд кажется диким писать отдельные фичи для отдельных устройств, но помимо устройств также есть производители процессоров, которых еще меньше, которые также реализуют свои API. Например, у Qualcomm есть замечательный hardware acceleration для распознавания образов с камеры, который вы вполне можете задействовать, у них он даже неплохо описан.
Таким образом, любой из вас может получить какую-то пользу даже из этого типа фрагментации. Что мы с этим делаем? Ни в коем случае не стесняйтесь репортить баги и слать баг-репорты разработчикам устройств и даже разработчикам Android. Потому что если были сломаны какие-то API, на которые стоило написать CTS тест, то он будет написан — и были такие прецеденты, — и после этого API становилось надежнее.
Изучайте Android, изучайте то, что предлагают производители, не ругайтесь с ними — работайте с ними.
Как это выглядит изнутри? Как можно реализовать какую-то фичу, которая будет уникальна для нашего телефона, и воспользоваться этим API из обычного Android приложения?
Немного теории. Как сами разработчики Android описывают внутреннее устройство AOSP? Верхний слой — приложение, которое написали либо вы, либо разработчики самого телефона, которое не обладает никакими высокими правами, просто использует стандартные API. Это фреймворк, это те классы, которые не входят в состав вашего приложения, такие как Activity, Parcelable, Bundle — они являются частью системы, их относят к фреймворку. Классы, которые вам доступны на устройстве. Далее идут системные сервисы. Это то, что связывает вас с системой: ActivityManagerService, WindowManagerService, PackageManagerService, которые реализуют внутреннюю сторону взаимодействия с системой.
Далее идут Hardware Abstraction Layer, это верхний слой драйверов, в которых заложена вся логика для вывода на экран, для взаимодействия с Bluetooth и всем подобным. Ядро — это нижний слой драйверов, менеджмент системы. Тем, кто знает, что такое ядро и сталкивался, не нужно рассказывать, а рассказывать можно долго.
Как это выглядит на устройстве? Наше приложение взаимодействует не только со стандартным фреймворком, но также может взаимодействовать и с кастомным фреймворком производителя. Также через этот фреймворк может связываться с кастомными сервисами. Если это хардварные или низкоуровневые фичи, то для них пишется и HAL, и даже драйвера на уровне ядра, если необходимо.
Как же нам написать свою фичу? План простой: надо написать фреймворк, который не сильно отличается от библиотечек, которые большинство Android разработчиков писали, думаю, вам всем это знакомо. Нужно написать системный сервис, который представляет собой обычное приложение, только с не совсем обычным набором прав в системе. И если нужно, можно написать HAL, но мы это опускаем. Можно написать и свои драйвера на уровне ядра, но также мы это сейчас не будем рассматривать. И написать клиентское приложение, которое всем этим будет пользоваться.
Чтобы мы взаимодействовали с системой, нам нужно написать какой-то контракт, и для этого уже существует хороший механизм AIDL-интерфейсов. Это просто своего рода интерфейс, на основе которого система генерит еще дополнительный класс, который мы можем экстендить, через который осуществляется межпроцессное взаимодействие между вашим приложением и системными сервисами.
Далее пишем фреймворк, нашу библиотеку, которая держит имплементацию этого интерфейса, и проксирует все вызовы к нему. Если вам интересно, то ActivityManager, PackageManager, WindowManager работают примерно так же. Там чуть больше логики, чем мы здесь реализовали, но суть именно такая.
Мы реализовали фреймворк, нам нужно написать системный сервис, который будет со стороны системы получать наши данные, в данном случае мы передаем и считываем integer. Мы создаем класс, он также экстендит интерфейс, который нам сгенерился из AIDL на предыдущем слайде. Мы создаем поле, в которое записываем значения, считываем, пишем сеттером, геттером. Единственное, здесь не хватает локов, но они не очень вмещались на слайде, их стоило бы сделать.
Далее, чтобы этот системный сервис был доступен, мы должны зарегистрировать его в системном сервис-менеджере, и в данном случае это тот самый класс, который не доступен обычным приложениям. Он доступен именно тем, которые находятся в платформе в системных разделах. Мы регистрируем сервис просто в Application.onCreate(), делаем его доступным под именем класса, который мы создали.
Что нам нужно, чтобы onСreate() в принципе запустился и наш сервис оказался загруженным в память? Мы пишем в манифесте в application android:persistent=”true”. Это значит, что это персистентный процесс, он должен находиться в памяти постоянно, потому что он осуществляет системные функции.
Также в самом манифесте можем указать android:sharedUserId, в данном случае system, но это может быть широкий набор различных ID, они позволяют приложению получать более широкие права в системе, взаимодействовать с различными API и сервисами, которые недоступны обычным приложениям.
В данном случае для примера мы ничего такого не использовали.
Мы написали фреймворк, написали системный сервис. Механизмы внутри мы опустим, это несколько сложная тема, она заслуживает отдельного доклада.
Как доставить фреймворк разработчикам приложения? Два формата. Мы можем выдавать полноценные классы и сделать полноценную библиотеку, которую вы вкомплириуете в свое приложение, и вся логика станет частью ваших дексов.
Либо можно распространять фреймворк в виде stub-классов, относительно которых можно только слинковаться во время компиляции и рассчитывать на то, что эти классы будут вас ждать аналогично предыдущим примерам из различных версий Android на самом устройстве. Распространять можно либо через обычный Maven-репозиторий, который всем знаком, либо через Android Studio sdkmanager, аналогично тому, как вы устанавливаете новые версии SDK. Сложно сказать, какой метод удобнее. Maven лично мне удобнее подключать.
Пишем простое приложение. Уже знакомым образом подключаем compileOnly-зависимость, только теперь это наша библиотека. Мы прописываем uses-library, которые мы написали и которые положили на устройство. Мы пишем Activity, получаем доступ к этим классам, взаимодействуем с системой. Так можно было бы реализовывать абсолютно любые фичи: передачу данных на какие-то дополнительные устройства, дополнительные хардварные фичи и т. д.
Тем самым все разработчики делают уникальные особенности устройства доступными для разработчиков. Иногда это приватные API, которые дают только партнерам. Иногда — публичные и те, что вы можете найти на developer-порталах. Существуют и другие способы реализовать подобные вещи, но я описал способ, который считается основным в Google и среди Android-разработчиков.
Не стоит относиться к разработчикам устройств как к людям, которые ломают ваши приложения. Это такие же разработчики, они пишут такой же код, примерно на том же уровне. Пишите баг-репорты, они действительно помогают, я часто их анализирую. Пишите больше приложений и пользуйтесь возможностями, которые вам предоставляет как Android, так и само устройство. У меня всё.
Автор: Leono