Уровень Android API, обратная и прямая совместимость
Добрый вечер, друзья. Мы подготовили полезный перевод для будущих студентов курса «Android-разработчик. Продвинутый курс». С радостью делимся с вами данным материалом.
Если вы читаете эту статью, значит вас могут интересовать такие вещи, как:
Все эти понятия связаны друг с другом, и я постараюсь объяснить их вам в этой статье простым, но эффективным способом.
Для этого необходимо понимать разницу между SDK и API и знать что такое уровень API в экосистеме Android.
Это правда, что в Android между SDK и API существует отношение 1:1, и часто эти два термина используются как синонимы, но важно понимать, что это не одно и то же.
Правильнее говорить, что для каждой версии Android есть SDK и эквивалентный API, а также уровень этого API.
Расшифровывается как Software Development Kit (комплект для разработки программного обеспечения). Обратите внимание на слово «kit» (комплект)… он как раз представляет из себя набор различных инструментов, библиотек, документации, примеров, помогающих разработчикам создавать, отлаживать и запускать приложения для Android. API предоставляется вместе с SDK.
Если открыть SDK Manager в Android Studio, можно будет яснее увидеть, из чего состоит Android SDK.
На первой вкладке SDK Platform перечислены SDK каждой версии Android.
Как показано на рисунке ниже, Android 9.0 SDK (также известный как Pie) содержит:
На второй вкладке SDK Tools показаны другие инструменты, которые также являются частью SDK, но не зависят от версии платформы. Это означает, что они могут быть выпущены или обновлены отдельно.
Расшифровывается как Application Programming Interface (программный интерфейс приложения). Это просто интерфейс, уровень абстракции, который обеспечивает связь между двумя разными «частями» программного обеспечения. Он работает как договор между поставщиком (например, библиотекой) и потребителем (например, приложением).
Это набор формальных определений, таких как классы, методы, функции, модули, константы, которые могут использоваться другими разработчиками для написания своего кода. При этом API не включает в себя реализацию.
Уровень API
Уровень API — это целочисленное значение, однозначно идентифицирующее версию API фреймворка, предлагаемую платформой Android.
Обычно обновления API фреймворка платформы разрабатываются таким образом, чтобы новая версия API оставалась совместимой с более ранними версиями, поэтому большинство изменений в новом API являются аддитивными, а старые части API становятся устаревшими, но не удаляются.
И теперь кто-то может задаться вопросом…
если API Android не предоставляет реализацию, а SDK Manager предлагает необязательный загружаемый исходный код API в составе SDK, то где находится соответствующая реализация?
Ответ прост. На устройстве.
Давайте разберемся с этим…
От исходного кода к APK-файлу
Как правило, проект под Android состоит из кода, написанного разработчиками с использованием Android API (модуль приложения), а также некоторых других библиотек/зависимостей (.jar-файлов, AAR, модулей и т.д.) и ресурсов.
Процесс компиляции преобразует код, написанный на Java или Kotlin, включая зависимости (одна из причин уменьшить ваш код!), в байт-код DEX, а затем сжимает все в файл APK вместе с ресурсами. На данном этапе реализация API не включена в итоговый APK!
Процесс сборки — Android Developers
DEX файлы и Android Runtime
Архитектура Android — Android Developers
Android Runtime — это место, где делается вся грязная работа и где выполняются DEX-файлы. Оно состоит из двух основных компонентов:
Версия API, доступная на этом уровне, соответствует версии платформы Android, на которой запущено приложение.
Например, если на фактическом устройстве установлен Android 9 (Pie), доступны все API до 28 уровня.
compileSdkVersion
Настоятельно рекомендуется выполнить компиляцию с последней версией SDK:
Это же приложение может работать на устройстве с Android 9 Pie (API 28 уровня), поскольку метод API xyz() все еще доступен на API 28 уровня.
minSdkVersion
Это значение обозначает минимальный уровень API, на котором приложение может работать. Это минимальное требование. Если не указан, значением по умолчанию является 1.
Разработчики обязаны установить корректное значение и обеспечить правильную работу приложения до этого уровня API. Это называется обратной совместимостью.
Чтобы обеспечить обратную совместимость, разработчики могут во время выполнения проверять версию платформы и использовать новый API в более новых версиях платформы и старый API в более старых версиях или, в зависимости от случая, использовать некоторые статические библиотеки, которые обеспечивают обратную совместимость.
Также важно упомянуть, что Google Play Store использует это значение, чтобы определить, можно ли установить приложение на определенное устройство, сопоставив версию платформы устройства с minSdkVersion приложения.
Разработчики должны быть очень осторожны при выборе этого значения, поскольку обратная совместимость не гарантируется платформой.
Выбор «правильного» значения для проекта также является бизнес-решением, поскольку оно влияет на то, насколько большой будет аудитория приложения. Посмотрите на распределение платформ.
targetSdkVersion
Это значение указывает уровень API, на котором приложение было разработано.
Иногда могут быть некоторые изменения API в базовой системе, которые могут повлиять на поведение приложения при работе в новой среде выполнения.
Целевой уровень приложения включает поведение среды выполнения, которое зависит от конкретной версии платформы. Если приложение не готово к поддержке этих изменений поведения среды выполнения, оно, вероятно, завершится сбоем.
Простым примером является Runtime Permission, которое было представлено в Android 6 Marshmallow (API 23 уровня).
Приложение может быть скомпилировано с использованием API 23 уровня, но иметь целевым API 22 уровня, если оно еще не готово поддержать новую модель разрешений времени выполнения.
Таким образом, приложение может по-прежнему быть совместимым без включения нового поведения среды выполнения.
В любом случае, как уже упоминалось, Google требует, чтобы приложения удовлетворяли новым требованиям целевого уровня API, поэтому всегда следует иметь высокий приоритет для обновления этого значения.
Теперь соединяя все это вместе, мы видим четкое отношение
minSdkVersion ≤ targetSdkVersion ≤ compileSdkVersion
Имейте в виду, что настоятельно рекомендуется выполнить компиляцию в соответствии с последним уровнем API и стараться использовать targetSdkVersion == compileSdkVersion.
Обеспечение обратной совместимости в распределенных системах
По мере того, как наша жизнь становится более распределенной, также есть программное обеспечение, на которое мы полагаемся. То, что мы видим как единый пользовательский интерфейс, обычно питается от серии подключенных служб, каждая из которых имеет определенную работу.
Рассмотрим Netflix. На главной странице мы видим смесь контента: ранее просмотренные шоу, популярные новые названия, управление учетными записями и многое другое.
Но этот экран не генерируется netflix.exe работая где-то на ПК. По состоянию на 2017 год он был оснащен более чем 700 индивидуальными услугами. Это означает, что начальный экран на самом деле представляет собой просто совокупность сотен микросервисов, работающих вместе. Одна служба для управления функциями учетной записи, другая для вынесения рекомендаций и т. д.
Переход к распределенным архитектурам приносит много преимуществ: более легкое тестирование, меньшие развертываемые блоки, более слабая развязка, меньшие поверхности отказа, чтобы назвать несколько. Но это также приносит свой собственный набор проблем.
Одним из них является поддержание обратной совместимости между компонентами. Другими словами, как набор услуг может развиваться вместе таким образом, чтобы не нарушать систему? Сервисы могут работать только вместе, если все они согласны на различные контракты: как обмениваться данными и как выглядит формат данных. Нарушение даже одного контракта может привести к хаосу в вашей системе.
Но как разработчики, мы знаем, что изменение-это единственная константа. Технологические и бизнес-потребности неизбежно меняются с течением времени, и так же должны меняться и наши услуги. Это может происходить различными способами: веб-интерфейсы API, обмен сообщениями, такие как JMS или Kafka, и даже в хранилищах данных.
Ниже мы рассмотрим некоторые рекомендации по созданию распределенных систем, которые позволяют нам изменять службы и интерфейсы таким образом, упрощая обновление.
Web APIs
Со временем web API могут потребоваться изменения. Независимо от того, идет ли речь о смене бизнес-приоритетов или новых стратегиях, мы должны с самого первого дня принять, что наши API, скорее всего, будут изменены.
Давайте рассмотрим некоторые способы, которыми мы можем сделать наши веб-API обратно совместимыми.
Принцип надежности
Чтобы создать веб-интерфейсы API, которые легко эволюционируют, следуйте принципу надежности, обобщенному как «будьте консервативны в том, что вы делаете, будьте либеральны в том, что вы принимаете.”
В контексте веб-API этот принцип может применяться несколькими способами:
Управление версиями
Управление версиями API позволяет нам поддерживать различные функциональные возможности для одного и того же ресурса.
Например, рассмотрим приложение блога, которое предлагает API для управления его основными данными, такими как пользователи, сообщения в блоге, категории и т. д. Предположим, что первая итерация имеет конечную точку, которая создает пользователя со следующими данными: имя, адрес электронной почты и пароль. Шесть месяцев спустя мы решаем, что теперь каждая учетная запись должна включать роль (администратор, редактор, автор и т. д.). Что мы должны делать с существующим API?
По сути, у нас есть два варианта:
С опцией 1 мы обновляем код, и любой запрос, который не включает новый параметр, отклоняется как неверный запрос. Это легко реализовать, но это также нарушает существующие пользователи API.
С вариантом 2 мы реализуем новый API, а также обновляем исходный API, чтобы обеспечить некоторое разумное значение по умолчанию для нового параметра роли. Хотя это определенно больше работы для нас, мы не нарушаем никаких существующих пользователей API.
Следующий вопрос заключается в том, как мы делаем версию API? Эта дискуссия продолжается уже много лет, и нет ни одного правильного ответа. Многое будет зависеть от вашего стека технологий, но в целом, есть три основных способа реализации управления версиями API:
Это самый простой и наиболее распространенный способ и он может быть достигнут с помощью любого пути:
Или с помощью параметров запроса:
URL-адреса удобны, потому что они являются обязательной частью каждого запроса, поэтому ваши потребители должны иметь дело с ними. Большинство платформ регистрируют URL-адреса с каждым запросом, поэтому легко отслеживать, какие потребители используют те или иные версии.
Заголовки
Это можно сделать с помощью настраиваемого имени заголовка, понятного вашим службам:
Или мы можем захватить заголовок «Accept», чтобы включить пользовательские расширения:
Использование заголовков для управления версиями больше соответствует практике RESTful. В конце концов, URL должен представлять ресурс, а не какую-то его версию. Кроме того, заголовки уже отлично справляются с передачей того, что по сути является метаданными между клиентами и серверами, поэтому добавление версии кажется хорошим выбором.
С другой стороны, заголовки громоздки для работы с некоторыми фреймворками, более трудны для тестирования и нецелесообразны для входа в систему для каждого запроса. Некоторые интернет-прокси могут удалять неизвестные заголовки, что означает, что мы потеряем наш пользовательский заголовок, прежде чем он достигнет службы.
Тело сообщения
Мы могли бы обернуть тело сообщения с некоторыми метаданными, которые включают версию:
С точки зрения RESTful, это нарушает идею о том, что тела сообщений являются представлениями ресурсов, а не версией ресурса. Мы также должны обернуть все наши доменные объекты в общий класс-оболочку, что не очень удобно—если этот класс-оболочка когда-либо должен измениться, все наши API потенциально должны измениться вместе с ним.
Одна последняя мысль о версионировании: рассмотрите возможность использования чего-то помимо простой схемы подсчета (v1, v2 и т. д.). Вы можете предоставить пользователям еще несколько контекстов, используя формат даты (например, «201911») или даже семантическое управление версиями.
Документация
Когда мы выпускаем библиотеки на GitHub или Maven, мы предоставляем журналы изменений и документацию. Наши веб-интерфейсы API не должны отличаться.
Журналы изменений необходимы для того, чтобы позволить потребителям API принимать обоснованные решения о том, как и когда они должны обновить своих клиентов. Как минимум, журналы изменений API должны включать следующее:
Эта последняя часть имеет решающее значение для того, чтобы сделать наши API эволюционирующими. Удаление конечной точки явно не является обратно совместимым, поэтому вместо этого мы должны запретить их. Это означает, что мы продолжаем поддерживать его в течение фиксированного периода времени и позволяем нашим потребителям время для изменения их кода вместо неожиданного взлома.
Служба обмена сообщениями
Из-за этого мы должны быть осторожны при обновлении издателя, либо потребителя. Существует несколько стратегий, которые мы можем принять, чтобы предотвратить критические изменения при обновлении наших приложений обмена сообщениями.
Обновление потребителей в первую очередь
Рекомендуется сначала обновить потребительские приложения. Это дает нам возможность обрабатывать новые форматы сообщений, прежде чем мы фактически начнем их публиковать.
Здесь также применяется принцип устойчивости. Производители всегда должны отправлять минимально необходимую полезную нагрузку, а потребители должны потреблять только те поля, которые им небезразличны, и игнорировать все остальное.
Создание новых тем и очередей
Если тела сообщений существенно изменяются или мы полностью вводим новый тип сообщения, мы должны использовать новую тему или очередь. Это позволяет нам публиковать сообщения, не беспокоясь о том, что потребители могут быть не готовы их потреблять. Сообщения будут стоять в очереди в брокерах, и мы можем свободно развернуть нового или обновленного потребителя, когда захотим.
Использование заголовков и фильтров
Большинство шин сообщений предлагают заголовки сообщений. Так же, как и заголовки HTTP, это отличный способ передать метаданные, не загрязняя полезную нагрузку сообщения. Мы можем использовать это в своих интересах несколькими способами. Как и в случае с веб-API, мы можем публиковать сообщения с информацией о версии в заголовке.
Со стороны потребителя мы можем фильтровать сообщения, соответствующие известным нам версиям, игнорируя другие.
Хранилища данных
В настоящей архитектуре микрослужб хранилища данных не являются общими ресурсами. Каждая служба владеет своими данными и контролирует доступ к ним.
Однако в реальном мире это происходит не так часто. Большинство систем представляют собой смесь устаревшего и современного кода, где все получают доступ к хранилищам данных, используя свои собственные методы доступа.
Итак, как мы можем развивать хранилища данных обратно совместимым образом? Поскольку большинство хранилищ данных являются либо реляционными, либо NoSQL-базами данных, мы рассмотрим каждую из них отдельно.
Реляционная база данных
Реляционные базы данных, такие как Oracle, MySQL и PostgreSQL, имеют несколько характеристик, которые могут сделать их обновление сложной задачей:
Изменения в реляционных базах данных можно разделить на три категории.
Добавление новых таблиц
Это, как правило, безопасно сделать и не будет нарушать любые существующие приложения. Мы должны избегать создания ограничений внешнего ключа в существующих таблицах, но в противном случае беспокоиться не о чем.
Добавление новых столбцов
Всегда добавляйте новые столбцы в конец таблиц. Если столбец не допускает значения null, мы должны включить разумное значение по умолчанию для существующих строк.
Кроме того, запросы в наших приложениях всегда должны использовать именованные столбцы вместо числовых индексов. Это самый безопасный способ убедиться, что новые столбцы не нарушают существующие запросы.
Удаление столбцов или таблиц
Эти типы обновлений представляют наибольший риск для обратной совместимости. Нет никакого хорошего способа убедиться, что таблица или столбец существуют, прежде чем запрашивать его. Подслушанная проверка таблицы перед каждым запросом просто не стоит того.
Если это возможно, запросы к базе данных должны корректно обрабатывать сбои. Предполагая, что удаляемая таблица или столбец не являются критическими или частью какой-либо более крупной транзакции, запрос должен продолжать выполнение, если это возможно.
Однако это не будет работать в большинстве случаев. Скорее всего, каждый столбец или таблица в схеме важны, и его неожиданное исчезновение сломает ваши запросы.
Поэтому наиболее практичным подходом к удалению столбцов и таблиц является первое обновление вызывающего их кода. Это означает обновление каждого запроса, ссылающегося на рассматриваемую таблицу, и изменение его поведения. Как только все эти обычаи исчезнут, можно будет безопасно удалить его из базы данных.
Базы данных NoSQL
Такие хранилища данных NoSQL, как MongoDB, ElasticSearch и Cassandra, имеют иные ограничения, чем их реляционные аналоги.
Основное отличие состоит в том, что вместо строк данных, которые все должны соответствовать схеме, документы внутри базы данных NoSQL не имеют такого ограничения. Это означает, что наши приложения уже привыкли иметь дело с документами, которые не имеют единой схемы.
У нас есть дополнительное преимущество в том, что большинство баз данных NoSQL не допускают ограничений между коллекциями, как это делают реляционные базы данных.
В этом контексте добавление новых коллекций и полей обычно не вызывает беспокойства. Здесь снова принцип надежности является нашим руководством: только сохраняйте необходимые поля и игнорируйте любые поля, которые нам не нужны при чтении документа.
С другой стороны, удаление полей и коллекций должно следовать тем же рекомендациям, что и реляционные базы данных. Если это возможно, наши запросы должны корректно обрабатывать сбои и продолжать выполнение. Кроме того, мы должны сначала обновить все запросы, а затем обновить само хранилище данных.
Развертывание программного обеспечения
Независимо от того, какой технический стек мы используем, есть определенные методы, которые мы можем включить в наш жизненный цикл программного обеспечения, которые помогают устранить или свести к минимуму проблемы совместимости.
Имейте в виду, что большинство из них работает только при двух условиях:
Если ваша организация не вписывается в одну из этих категорий, вы вряд ли добьетесь успеха в реализации любого из этих процессов.
Кроме того, ни одна из приведенных ниже практик не должна быть серебряной пулей, которая решит все проблемы развертывания. Вполне возможно, что ни один или многие из них не будут применимы к вашей организации. Оцените, как каждый из них может помочь вам, а может и не помочь.
Canary deployment
Сanary deployment, также известное как развертывание blue/green, red/black или purple/red, представляет собой идею выпуска новой версии приложения и позволяет только небольшому проценту трафика достичь его.
Цель состоит в том, чтобы протестировать новые версии приложений с реальным трафиком, минимизируя при этом последствия любых проблем, которые могут возникнуть. Если новое приложение работает должным образом, то остальные экземпляры могут быть обновлены. Если что-то пойдет не так, один экземпляр может быть возвращен, и только небольшая часть трафика будет затронута.
Это работает только для кластерных служб, где мы запускаем несколько экземпляров. Приложения, работающие как синглеты, не могут быть протестированы таким образом.
Кроме того, для canary deployments требуются сложные сервисные сетки для работы. Большинство архитектур микросервисов уже используют некоторые типы обнаружения служб, но не все они созданы равными. Без сервисной сетки, которая обеспечивает мелкозернистый контроль над потоком трафика, canary deployment невозможно.
The three Ns
The three Ns относятся к идее, что приложение должно поддерживать три версии каждой службы, с которой оно взаимодействует:
Так что же именно это означает? Это действительно просто сводится к тому, чтобы не предполагать, что наши услуги будут модернизированы в каком-либо конкретном порядке.
В качестве примера рассмотрим две службы, A и B, где A делает спокойные звонки на B.
Если нам нужно внести изменения в A, мы не должны предполагать, что B будет обновлен до или после A, или даже вообще. Изменения, которые мы вносим в A или B, должны стоять сами по себе.
И что произойдет, если B придется откатиться назад? Мы не должны возвращать все свои зависимые услуги в этом случае.
Для ясности: принцип трех Ns нелегко достичь, особенно при работе с устаревшими монолитными приложениями. Однако это не так уж и невозможно.
Это требует планирования и предвидения, и оно не придет без растущих болей и неудач на этом пути. Это обычно требует масштабного сдвига в мышлении разработчиков до точки, когда каждый разработчик и команда должны задавать два вопроса, прежде чем они выпустят какой-либо новый код:
На первый вопрос может быть нелегко ответить, но есть много инструментов, которые могут помочь. От статического анализа исходного кода до более сложных инструментов, таких как Zipkin, график зависимостей может помочь вам понять, как взаимодействуют службы.
Второй вопрос должен быть простым для ответа: что изменилось за пределами кода? Это может быть база данных, файлы конфигурации и т. д. Мы должны иметь план отката этих изменений, а не просто скомпилированный код.
Переключатели характеристик
Существует множество инструментов, которые можно использовать для реализации переключателей функций, таких как rollout.io и Optimizely. Независимо от того, какой инструмент мы используем, есть определенные характеристики, которые мы должны искать.
Быстрый
Реализация функциональных переключателей обычно означает добавление большого количества кода, как показано ниже, в наши приложения:
Поэтому проверка состояния переключателя функций должна быть быстрой. Мы не должны полагаться на чтение из базы данных или удаленного файла каждый раз, когда нам нужно проверить это состояние переключения, так как это может очень быстро ухудшить наше приложение.
В идеале, переключаемое состояние должно быть загружено во время запуска приложения и кэшироваться внутри, с некоторым механизмом для обновления этого внутреннего состояния по мере необходимости (messaging bus, JMX, API и т. д.).
Распределенный
Поскольку мы имеем дело с распределенными системами, вполне вероятно, что переключение функций должно быть доступно для нескольких приложений. Поэтому состояние переключателя должно быть распределено таким образом, чтобы каждое приложение видело одно и то же состояние вместе с любыми изменениями.
Элементарный
Изменение состояния переключателя должно быть одной операцией. Если нам нужно обновить несколько источников конфигурации, мы увеличиваем вероятность того, что приложения получат другой вид переключения.
Переключатели имеют тенденцию накапливаться с течением времени в коде. Хотя влияние на производительность проверки большого количества переключателей может быть незначительным, они могут быстро превратиться в технический долг и потребовать периодической очистки. Обязательно планируйте время, чтобы вернуться к ним и очистке по мере необходимости.
Двигайтесь быстро, но не ломайте вещи
В нашем постоянно меняющемся распределенном мире существует множество способов взаимодействия приложений и служб. Что означает, что есть много способов сломать их, поскольку они неизбежно развиваются.
Приведенные выше советы и идеи являются лишь отправной точкой и не охватывают все способы, которыми могут разговаривать наши системы. Такие вещи, как распределенные кэши и транзакции, также могут создавать препятствия для создания обратно совместимого программного обеспечения.
Существуют также другие протоколы, такие как web sockets или gRPC, которые имеют свои собственные функции, которые мы можем использовать для быстрого обновления наших систем.
По мере того, как мы удаляемся от монолитов и переходим к микрослужбам, нам нужно убедиться, что мы фокусируемся столько же на эволюционности наших систем, сколько и на функциональности.




