Хабрахабр

Секреты API Android-устройств. Доклад Яндекса

Одна из главных сложностей Android-разработки — фрагментация. Практически каждый производитель меняет Android под свои нужды. Разработчик Андрей Макеев перечислил отличия между реализациями вендоров и оригинальным Android Open Source Project. Из доклада можно узнать, как извлечь пользу из индивидуальных особенностей прошивок на разных устройствах.

— Программированием я занимаюсь со школы, под Android разрабатываю года три. Из них год я провел в Яндексе, участвовал в таких проектах, как Лончер и Телефон.

Хочу рассказать о том, как выглядит фрагментация API Android-устройств — как снаружи, со стороны разработчиков приложений, так и изнутри, с точки зрения разработчиков платформы и телефонов.

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

Самая очевидная и простая — фрагментация по версии Android SDK, мы с ней каждый день сталкиваемся, буквально с первых дней разработки под Android. Фрагментация API — один из параметров, по которому можно фрагментировать устройство. Как правило, мы пользуемся различными библиотеками поддержки от Google, чтобы упростить нашу жизнь. Мы знаем, что и в какой версии API появилось, что убрали, что задеприкейтили, но оно еще доступно, и чего уже нет. Многое уже сделано за нас.

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

Минусы всем известны — мы вынуждены держать целый парк девайсов с разными версиями, чтобы хотя бы протестировать наше приложение. Не будем заострять много внимания на этом типе фрагментации. Особенно это неудобно, когда ты только начал разрабатывать под Android и вдруг выясняется: надо поизучать, что там было года два-три назад, чтобы какие-то прежние девайсы поддержать. Кроме того, мы вынуждены писать лишний код. Положительные стороны: API развивается, технологии двигаются вперед, Android приобретает новых пользователей, становится удобнее как для разработчиков, так и для пользователей.

Мы также используем библиотеки и очень радуемся, когда отказываемся от поддержки старых версий. Как мы с этим работаем? Это прямо счастье. Думаю, в жизни у каждого, кто занимается этим больше года, был такой момент. Далее — фрагментация по типу Android. Это очевидный и простой вариант фрагментации. Android TV говорит сам за себя, Android Auto предназначен в основном для автомагнитол, Android Things — для IoT и аналогичных embedded-устройств. Их существует несколько. Мы попробуем рассмотреть, как это выглядит со стороны разработчика. W — это Wear OS, бывший Android Watch, часики для Android. И, что самое интересное, попробуем рассмотреть, как это выглядит изнутри.

Первый — Wear OS. Берем два примера с developer.android.com. Мы добавляем compileOnly зависимость в build.gradle и прописываем в манифесте две дополнительных строки: uses-feature android.hardware.type.watch и uses-library, которая соответствует тому же самому имени пакета, что и библиотека, которую мы подключили. Что нам нужно, чтобы сделать приложение?

Создаем Activity, только в данном случае мы эстендим не стандартную activity, с которой привыкли работать, и даже не компадную, а WearableActivity, и вызываем специфичные для нее методы, в данном случае setAmbientEnabled(). Как мы что-то реализуем? Uses-library, которая, видимо, вынуждает ОС подключить эти классы и этот код нам в runtime на устройстве, и новые классы, которыми мы пользуемся. Итак, у нас compileOnly-зависимость, то есть она не попадает в ход нашего приложения.

Мы не прописываем uses-feature, только uses-library, compileOnly-зависимость. Android Things API практически не отличается.

Мы опрашиваем GPIO и пытаемся залогировать. Создаем activity, в данном случае это уникальный для Android Things API, класс PeripheralManager.

Есть два варианта. Как такое приложение поведет себя на вашем телефоне?

Если мы указали android:required=”false”, то приложение установится, но при попытке обратиться к классу PeripheralManager мы получим NoClassDefFoundError, потому что в стандартном Android такого класса нет. Если мы указали, что uses-library android:required=”true”, то мы не выполнили обязательные требования PackageManager для установки приложения, и он в принципе откажется его устанавливать.

Мы подключаем compileOnly зависимость только для того, чтобы с ней синковаться при сборке, и те классы, которые мы используем, ждут нас на устройстве, подключаются они при помощи определенных строк в манифесте. Какие отсюда выводы? Иногда мы прописываем фичу, которая чаще нужна, чтобы в Google Play различать устройство, которому можно или нельзя раздавать данное приложение.

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

Пишите больше приложений. Как с этим работать? Для часов должно быть меньше, для Android Things практически ничего из того, что на телефон пишется, не подходит, ну и так далее. Общая рекомендация — писать не одну версию приложения на все типы Android, а все-таки делать разные. И пользуйтесь библиотеками, которые нам предоставляют разработчики Android и, иногда, разработчики устройств.

Каждый производитель, получив исходный код — в редких случаях это AOSP, чаще он как-то модифицирован разработчиками железа, — вносит в него изменения. Следующий наименее изученный вид фрагментации — фрагментация по производителю. Или мы узнаём это из crash-аналитики, когда вдруг что-то крашится непонятным образом, только на каких-то определенных устройствах. Как правило, о негативных эффектах такого типа фрагментации мы узнаём из не самых лучших каналов — из негативных отзывов в Google Play, потому что у кого-то что-то сломалось. В лучшем случае мы узнаем это от наших QA, когда у них при тестировании на определенном устройстве что-то сломалось.

Нам пришел баг-репорт, где активити не растягивалась на весь экран, а наша любимая дефолтная обоина не отображалась вообще. На эту тему у меня есть замечательная история из нашей разработки Лончера. У нас даже не было устройств, чтобы это воспроизвести. Она не раскодировалась, показывалось пустое окно. С обоиной все вышло намного сложнее. По растягиванию на экран мы все-таки смогли найти low-end-девайс, на котором это не работало, и достаточно легко всё поправили при помощи android:resizeableActivity=”true” в манифесте. В конце концов выяснили, что на ряде устройств либо отсутствует, либо с багами реализован кодек для раскодировки прогрессивных jpeg, когда использовано несколько алгоритмов сжатия на результатах друг друга. Мы порядка двух дней пытались достучаться и получить более детальную информацию. Перекодировали все обоины, повторили ситуацию на бэкенде, который раздает остальные wallpaper sets, и все замечательно работает. В данном случае мы просто написали lint-проверку, которая фейлила билд при сборке приложения, если мы клали в сам apk обоины, закодированные прогрессивным образом. Но это стоило нам порядка двух дней разбирательств.

Неприятно, но к сожалению, вот так. Примерно так это выглядит в коде. Обычно эти строки после длительного дебаггинга появляются.

В первую очередь, есть CDD, где описывается, что можно, а что нельзя менять, что является обратно совместимым, а что нет. Какие нам Google дает гарантии на предмет того, чтобы API не было сломано настолько, чтобы приложения не работали в принципе? Чтобы покрыть больше кейсов, есть CTS, который необходимо пройти, чтобы телефон получил сертификацию от Google и с него можно было пользоваться гугловыми сервисами. Это документ из нескольких десятков страниц с общими рекомендациями, которые, естественно, не покроют все кейсы. Также есть CTS Verifier, обычный APK, который можно поставить на телефон, чтобы провести ряд проверок. Это набор примерно из 350 000 автоматизированных тестов. Кстати, если вы покупаете телефон с рук, можно его так проверять.

Он проверяет API драйверов, которые, начиная с Project Treble, версионируются и тоже подвергаются подобным тестам. С появлением Treble появился проект VTS, это скорее для разработчиков более низких уровней. Отрицательная сторона в том, что мы встречаемся с непредвиденными багами, которые невозможно предсказать, пока приложение не было запущено на устройстве. Кроме того, сами разработчики телефонов — здравые люди, которые хотят, чтобы Android-приложения нормально у них работали, но это так себе надежда. Мы опять вынуждены покупать, помимо того, что разные версии API, еще и дополнительные устройства разных производителей, чтобы проверять на них.

Как минимум, самые часто реализуемые производителями фичи попадают в сам Android. Но положительные стороны есть. Сейчас, если верить XDA Developers, разблокировку при помощи камеры по лицу тоже хотят сделать API Android, но это пока не точно. Кто-то может помнить, что стандартный Fingerprint API появился позже, чем устройства, которые могли разблокировать экран по отпечатку пальцев. Мы скорее всего с вами об этом узнаем.

И если вы этого ни разу не делали, советую пройтись по статистике использования вашего приложения, посмотреть, какие самые популярные производители, и заглянуть на девелопер порталы их сайтов. Кроме того, сами разработчики устройств, когда делают нестандартные API, они могут, и многие публикуют библиотеки для работы с их API для обычных разработчиков. И на первый взгляд кажется диким писать отдельные фичи для отдельных устройств, но помимо устройств также есть производители процессоров, которых еще меньше, которые также реализуют свои API. Думаю, вы приятно удивитесь, у многих есть API либо с интересными хардварными фичами, либо с секьюрити фичами, либо с облачными сервисами, либо еще с чем-то интересным. Например, у Qualcomm есть замечательный hardware acceleration для распознавания образов с камеры, который вы вполне можете задействовать, у них он даже неплохо описан.

Что мы с этим делаем? Таким образом, любой из вас может получить какую-то пользу даже из этого типа фрагментации. Потому что если были сломаны какие-то API, на которые стоило написать CTS тест, то он будет написан — и были такие прецеденты, — и после этого API становилось надежнее. Ни в коем случае не стесняйтесь репортить баги и слать баг-репорты разработчикам устройств и даже разработчикам Android.

Изучайте Android, изучайте то, что предлагают производители, не ругайтесь с ними — работайте с ними.

Как можно реализовать какую-то фичу, которая будет уникальна для нашего телефона, и воспользоваться этим API из обычного Android приложения? Как это выглядит изнутри?

Как сами разработчики Android описывают внутреннее устройство AOSP? Немного теории. Это фреймворк, это те классы, которые не входят в состав вашего приложения, такие как Activity, Parcelable, Bundle — они являются частью системы, их относят к фреймворку. Верхний слой — приложение, которое написали либо вы, либо разработчики самого телефона, которое не обладает никакими высокими правами, просто использует стандартные API. Далее идут системные сервисы. Классы, которые вам доступны на устройстве. Это то, что связывает вас с системой: ActivityManagerService, WindowManagerService, PackageManagerService, которые реализуют внутреннюю сторону взаимодействия с системой.

Ядро — это нижний слой драйверов, менеджмент системы. Далее идут Hardware Abstraction Layer, это верхний слой драйверов, в которых заложена вся логика для вывода на экран, для взаимодействия с Bluetooth и всем подобным. Тем, кто знает, что такое ядро и сталкивался, не нужно рассказывать, а рассказывать можно долго.

Наше приложение взаимодействует не только со стандартным фреймворком, но также может взаимодействовать и с кастомным фреймворком производителя. Как это выглядит на устройстве? Если это хардварные или низкоуровневые фичи, то для них пишется и HAL, и даже драйвера на уровне ядра, если необходимо. Также через этот фреймворк может связываться с кастомными сервисами.

План простой: надо написать фреймворк, который не сильно отличается от библиотечек, которые большинство Android разработчиков писали, думаю, вам всем это знакомо. Как же нам написать свою фичу? И если нужно, можно написать HAL, но мы это опускаем. Нужно написать системный сервис, который представляет собой обычное приложение, только с не совсем обычным набором прав в системе. И написать клиентское приложение, которое всем этим будет пользоваться. Можно написать и свои драйвера на уровне ядра, но также мы это сейчас не будем рассматривать.

Это просто своего рода интерфейс, на основе которого система генерит еще дополнительный класс, который мы можем экстендить, через который осуществляется межпроцессное взаимодействие между вашим приложением и системными сервисами. Чтобы мы взаимодействовали с системой, нам нужно написать какой-то контракт, и для этого уже существует хороший механизм AIDL-интерфейсов.

Если вам интересно, то ActivityManager, PackageManager, WindowManager работают примерно так же. Далее пишем фреймворк, нашу библиотеку, которая держит имплементацию этого интерфейса, и проксирует все вызовы к нему. Там чуть больше логики, чем мы здесь реализовали, но суть именно такая.

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

Он доступен именно тем, которые находятся в платформе в системных разделах. Далее, чтобы этот системный сервис был доступен, мы должны зарегистрировать его в системном сервис-менеджере, и в данном случае это тот самый класс, который не доступен обычным приложениям. Мы регистрируем сервис просто в Application.onCreate(), делаем его доступным под именем класса, который мы создали.

Мы пишем в манифесте в application android:persistent=”true”. Что нам нужно, чтобы onСreate() в принципе запустился и наш сервис оказался загруженным в память? Это значит, что это персистентный процесс, он должен находиться в памяти постоянно, потому что он осуществляет системные функции.

Также в самом манифесте можем указать android:sharedUserId, в данном случае system, но это может быть широкий набор различных ID, они позволяют приложению получать более широкие права в системе, взаимодействовать с различными API и сервисами, которые недоступны обычным приложениям.

В данном случае для примера мы ничего такого не использовали.

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

Два формата. Как доставить фреймворк разработчикам приложения? Мы можем выдавать полноценные классы и сделать полноценную библиотеку, которую вы вкомплириуете в свое приложение, и вся логика станет частью ваших дексов.

Распространять можно либо через обычный Maven-репозиторий, который всем знаком, либо через Android Studio sdkmanager, аналогично тому, как вы устанавливаете новые версии SDK. Либо можно распространять фреймворк в виде stub-классов, относительно которых можно только слинковаться во время компиляции и рассчитывать на то, что эти классы будут вас ждать аналогично предыдущим примерам из различных версий Android на самом устройстве. Maven лично мне удобнее подключать. Сложно сказать, какой метод удобнее.

Уже знакомым образом подключаем compileOnly-зависимость, только теперь это наша библиотека. Пишем простое приложение. Мы пишем Activity, получаем доступ к этим классам, взаимодействуем с системой. Мы прописываем uses-library, которые мы написали и которые положили на устройство. д. Так можно было бы реализовывать абсолютно любые фичи: передачу данных на какие-то дополнительные устройства, дополнительные хардварные фичи и т.

Иногда это приватные API, которые дают только партнерам. Тем самым все разработчики делают уникальные особенности устройства доступными для разработчиков. Существуют и другие способы реализовать подобные вещи, но я описал способ, который считается основным в Google и среди Android-разработчиков. Иногда — публичные и те, что вы можете найти на developer-порталах.

Это такие же разработчики, они пишут такой же код, примерно на том же уровне. Не стоит относиться к разработчикам устройств как к людям, которые ломают ваши приложения. Пишите больше приложений и пользуйтесь возможностями, которые вам предоставляет как Android, так и само устройство. Пишите баг-репорты, они действительно помогают, я часто их анализирую. У меня всё.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть