feature flags что это такое

Как ускорить телефон Android с помощью секретных настроек

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такое

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

↑ Настройки разработчика и как их разблокировать

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такое

↑ Возможности меню Для разработчиков

Ниже представлены функции, содержащиеся в меню Для разработчиков.

↑ Настройка анимации

С помощью нижеприведенного лайфхака снижается время перехода между окнами. Сразу будет ощутима разница при открытии программ.
Сначала нужно открыть секретное меню под названием «Параметры разработчика».

Также есть еще один метод. Сначала необходимо зайти в пункт «Отрисовка». Все показатели выбраны по умолчанию. Значение для шкалы анимации окна, переходного масштаба и длительности устанавливается на 1X. Рекомендуется поставить 0,5Х.

↑ Улучшение графики

Спецификации современных смартфонов быстро масштабируются за счет интеграции большого объема оперативной памяти и SoC, достойных настольных систем среднего и высокого уровня. В Playstore много новых игр и приложений, в том числе бесплатных, позволяющих раскрыть технический потенциал смартфона.

Чтобы улучшить графическую производительность игр и время отклика можно активировать опцию MSAA 4X. Это простой фильтр сглаживания, который обрезает изображения, улучшая качество. Он подойдет не для всех игр, а только для тех, которые используют OpenGL ES 2.0 API.

Один немаловажный минус этой процедуры – небольшое повышение расхода оперативной памяти при взаимодействии с приложениями. Но обычно этот недостаток считается несущественным.

↑ Дополнительные хитрости

Есть несколько других параметров, которые можно настроить для ускорения работы.

↑ Очистить лишние файлы

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

↑ Удалить или отключить приложения вручную

Чтобы сделать это необходимо посетить «Настройки», а затем «Приложения». Некоторое ПО, установленное по умолчанию, нельзя стереть, но разрешено выключить.

↑ Сменить лаунчер

↑ Разгон на 100%

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

Являюсь руководителем проекта, люблю андроид. Слежу за новинками, новостями, а так же помогаю решить проблему возникшую у пользователей в андроид смартфонах!

Для того чтобы воспользоваться полными возможностями сайта, пожалуйста авторизуйтесь с помощью социальных сетей.

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

Информация

На данный момент комментариев нет 🙁

Источник

Feature flags

Фиче-флаги — это настройки, которые позволяют не трогая программистов менять поведение программы, к примеру включать и выключать фичи. Типа вот этому пользователю показываем общих в списке друзей, а тому — нет.

Чаще всего набор фиче-флагов формирует фронтенд, и отсылает на бекенд в момент каждого запроса. Так можно легко ставить a\b тесты — просто выбираем две когорты, одной добавляем фичу, а другой — нет, и смотрим на поведение.

Пример реализации — GitHub, который передает фиче-флаги в HTTP-заголовках. Прямо сейчас в API гитхаба таким образом включается-выключается одновременно 30 фич.

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такое

Есть ещё одно очень полезное применение фиче-флагов — полное отключение функций приложения в зависимости от среды. К примеру, у нас ЦРМ есть фича — уведомлять пользователя по СМС о статусе заказа. Но я не хочу, чтобы СМС уходили с тестовых стендов или из CI, даже если кому-то хватит ума прописать боевые ключи на них. Поэтому я делаю фиче-флаг ENABLE_NOTIFICATIONS и включаю его только в переменных окружения на проде. По умолчанию флаг выключен, поэтому где мы ни развернули мой бекенд — он никогда не пошлет сообдщений живым людям, если его явно об этом не попросить.

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такое

Меня зовут Федя Борщёв. Пишу для программистов в телеграме — 3 поста в неделю об управлении сложными проектами, хорошем коде и профессиональном развитии. А ещё я в прямом эфире пишу код на ютубе и выкладываю всякое в фейсбук. Подписывайтесь!

Источник

Как портал Feature Flags помогает бизнесу управлять ИТ-продуктом

В нашей первой статье про Feature Flags мы говорили, как этот инструмент помогает ускорить запуск новых фич, повысить конкурентоспособность продукта и в целом упростить процессы в команде.

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

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

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

A/B-исследования и бета-тесты. В логику переключателей можно заложить стратегию деления пользователей – часть аудитории увидит новую функциональность, а остальные будут работать с основной версией.

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

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

В конечном счете переключатели приближают управление продуктом к нуждам бизнеса. Имея под рукой простой переключатель функций, заказчик обеспечивает себе оперативный контроль над состоянием продукта.

MVP-версия нашего портала представляла собой простую веб-страницу, где отображались статусы функций продукта. Настройки хранились в простом конфигурационном файле.

Когда идея прижилась, MVP вырос до компактного продукта из четырёх модулей:

· Микросервис для управления переключателями – содержит в себе всю логику их обработки. Микросервисы получают конфигурацию флагов при старте из configMap для своего namespace и сохраняют в своей локальной памяти. Если происходит включение или включение флагов на страничке портала, то агент меняет все configMaps, а микросервисы через обратную связь обновляют свою локальную конфигурацию.

· Фронтенд-модуль – обеспечивает механику включения и выключения переключателей.

· Агент – обеспечивает консистентность данных в локальном хранилище, которое заменило конфиг-файл с настройками.

· Стартер (опциональный компонент) – позволяет проверить работоспособность каждого отдельного переключателя перед изменением состояния.

Самое главное, наш портал – это Cloud Native продукт, который изначально разрабатывался для использования в микросервисных приложениях в инфраструктуре Kubernetes. Так что он может работать с любыми продуктами, вне зависимости от платформы, на которой он написан.

Источник

Укрощение feature-флагов

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такое

Всем привет! Меня зовут Паша Стрельченко, и я — Android-разработчик в hh.ru. В этой статье расскажу об укрощении feature-флагов. Если больше нравится аудиовизуальный формат, его можно найти на нашем youtube-канале. В статье я расскажу чуть больше технических подробностей, чем в видео, так что должно получиться интересно.

Что такое feature-флаги? Это обычные булевские флажочки, которыми внутри приложения вы заменяете или закрываете какую-либо функциональность. Например, с помощью одного флажка можно изменить цвет кнопки, с помощью другого – включить или выключить мессенджер внутри приложения.

Если у вас маленькая команда и не очень много feature-флагов, то, скорее всего, вы вообще не сталкивались с проблемами. Однако, если команда растёт, и с каждым днем становится всё больше фичетоглов, то неизбежно возникнут определенные сложности. О них мы и поговорим.

Содержание

Способы сборки feature-флагов по всей кодовой базе

Java Reflections API

Немного специфики hh.ru

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

Во-первых, мы называем feature-флаги «экспериментами», потому что постоянно «экспериментируем» над нашими пользователями, пока проводим огромное количество A/B тестов. Таким образом мы связали эти два понятия. То есть, включаем какой-то эксперимент = включаем feature-флаг. На протяжении всей статьи я буду оперировать именно понятием «эксперимент».

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

И вот тут мы столкнулись с определенными проблемами.

Проблемы с feature-флагами

Нашу первую реализацию конфига экспериментов можно было охарактеризовать фразой «Абстракция на абстракции и абстракцией погоняет».

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такоеАбстракция на абстракции, абстракцией погоняет!

Всё было очень сложно. У нас был базовый модуль экспериментов, в котором мы описывали логику походов в сеть за списком экспериментов и его кэшированием. Было два отдельных модуля, которые содержали все эксперименты двух наших приложений: соискательского и работодательского. И уже от этих двух модулей зависело огромное количество feature-модулей.

И мы выделили для себя три основных проблемы.

Merge-конфликты в общем наборе экспериментов

Как я уже сказал, у нас были модули, в которых мы описывали все наши эксперименты для разных приложений. И выглядело это как гигантский enum — один файл, в котором мы строчка за строчкой описывали эксперименты:

И, серьёзно, почти каждый день мы сталкивались с merge-конфликтами: кто-то смёрджил очередную фичу в develop, кто-то разрабатывает следующую и подмёрдживает к себе develop, и вынужден решать конфликты в одних и тех же файлах.

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такоеБесконечные merge-конфликты в одном файле

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

Пересборка приложения при добавлении эксперимента

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

Когда вы меняете какой-либо публичный интерфейс, доступный другим модулям, вы изменяете ABI модуля, и при изменении ABI будут пересобраны все зависимые от него модули. И поскольку у нас было много feature-модулей, которые зависели от модуля со списком экспериментов, у нас пересобиралось почти всё приложение при добавлении очередного элемента enum-а.

Так быть не должно.

Много кода ради проверки одного флага

Это проблема уже специфичная для android-проекта hh.ru — мы писали очень много кода для проверки одного-единственного эксперимента. Почему так получилось — мы долгое время считали модуль с экспериментами отдельным feature-модулем, который, по правилам нашей кодовой базы, не мог быть напрямую подключен к другому feature-модулю.

Как вы знаете, театр начинается с вешалки (ну или с парковки), в нашем случае, театр начинался с feature-модуля. В нём мы создавали интерфейс, который описывал зависимости этого feature-модуля:

Затем мы шли в application-модуль, там описывали класс-медиатор, в котором реализовывали зависимости нужного feature-модуля. Там обращались к DI и доставали значение флага:

В модуле экспериментов у нас был собственный публичный интерфейс (о нём я упоминал выше), к которому мы и обращались из application-модуля:

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

Ну и, наконец, изменялся enum с экспериментами, туда добавлялись новые элементы с нужными ключами:

Итого — у нас получилась длинная-предлинная церемония добавления и проверки очередного эксперимента. Разумеется, нас это не устраивало.

Какие выводы мы в итоге сделали:

Нам нужны эксперименты в кодовой базе, просто выбросить их мы не можем;

Мы тратим время на merge-конфликты, от них однозначно хотим уйти;

И мы пишем слишком много кода ради одного эксперимента.

Давайте решать эти проблемы!

Решаем проблемы

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

И техника проста — нужно разделить весь добавляемый контент файла на множество файлов.

Для этого мы ввели интерфейс эксперимента, и разделили элементы enum-а на отдельные классы, реализующие этот интерфейс:

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

Затем мы решили упростить процесс проверки эксперимента: для этого создали специальный объект Experiments, куда добавили метод для проверки наличия эксперимента в кэше:

Для большего удобства можно сделать extension-метод на интерфейсе Experiment, тогда код проверки будет ещё короче:

Ах, да, чтобы не тащить код проверок через медиаторы-application-модули и т.п., пусть теперь модуль с базовой логикой экспериментов считается core-модулем, значит его можно подключать напрямую.

Проблему длинных церемоний в коде — тоже решили.

Ну и, наконец, решаем проблему пересборок. Так как теперь каждый эксперимент — это отдельный класс, их можно размещать в РАЗНЫХ модулях и отмечать класс эксперимента модификатором internal.

Добавляя новый эксперимент таким образом мы не будем вызывать пересборку половины приложения.

Но возникает вопрос: а что делать, если эксперимент нужно проверять в нескольких разных модулях? Ответ простой: выносите общие модели экспериментов в core-модули, которые будут подключаться к вашим feature-модулям. Здесь главное не увлекаться и не складывать абсолютно все эксперименты в одну корзину, чтобы снова не страдать от лишних пересборок.

Ура, все три проблемы решены!

Собираем feature-флаги

Но перед нами встала новая интересная задача: а как нам теперь собрать разбросанные по нашей кодовой базе классы экспериментов в единый список?

Зачем нам вообще это нужно: у нас в hh для облегчения тестирования приложений есть специальная debug-панель, которая доступна на debug-сборке и на минифицированной debuggable-сборке (мы называем это preRelease-ом).

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такоеDebug-панель и открытая секция экспериментов

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

Мы ушли от enum-чика, а значит у нас больше нет встроенной в язык возможности получить разом список экспериментов (раньше это делали через ApplicantExperiment.values()). Плюс к этому, наш сервер не присылает нам ВЕСЬ список возможных ключей экспериментов, он нам присылает только тот список, который активен для того или иного пользователя. А значит в debug-панели нельзя просто взять и отобразить ответ сервера.

Что же делать?

И вот тут начинается магия. Способов оказалось довольно много, и я хочу вам о них рассказать. Я разбил эти способы на несколько групп:

Собрать список вручную

Вспомнить про Java Reflections API

Сгенерировать нужный код

Для демонстрации я подготовил Github-репозиторий. Каждый из способов, который я рассмотрю в статье, можно пощупать руками в отдельных ветках.

Сборка вручную

Этот способ выглядит сомнительно. Мы бы столкнулись всё с той же проблемой merge-конфликтов: просто на этот раз не в enum-е, а в build.gradle-файлах или же при описании списка, в котором мы бы инстанцировали модели экспериментов:

От чего ушли, к тому и пришли. Непорядок.

С другой стороны, у этого способа тоже есть плюсы:

Вам не нужны никакие дополнительные зависимости

И реализация очень простая.

Возможности DI-фреймворка

Оказывается, некоторые DI-фреймворки могут собирать объекты, которые объединены каким-то признаком (реализация интерфейса, наследники абстрактного класса и т.п.) в списки.

В частности, у Dagger-а 2 и у Hilt-а есть такая фича, которая называется Mutlibindings. С её помощью вы можете собирать объекты либо в Set, либо в Map с любыми ключами.

Как это делается?

Во-первых, вы создаёте dagger-модули в нужных feature-модулях, в которых описываете метод, отмеченный аннотациями @Provides и @IntoSet, и в реализации метода описываете создание объекта:

После этого вы готовы заинжектить собранный список в нужное вам место:

Ок, я сказал, что DI-фреймворки умеют собирать объекты в списки. Но что там у Toothpick/Koin? К сожалению, ничего.

Единственное, что есть у этих фреймворков — это issue на гитхабе, в которых разработчики просят добавить эту возможность (issue для Koin-а, issue для Toothpick-а).

Таким образом, если у вас в проекте уже используется Dagger — вам повезло, вы можете пользоваться встроенной фичей для сбора списка экспериментов. Если же используете другой фреймворк — вам подойдут другие способы. У нас в hh используется Toothpick, так что мы продолжили ресёрч.

Java Reflections API

Java Reflections API — это возможность языка Java в рантайме узнавать или изменять поведение приложений. И в этом разделе есть несколько интересных способов сбора множества разрозненных кусочков кода в единый список, о которых я хочу вам рассказать.

ClassLoader + DexFile

Первая связка, о которой я хочу рассказать — это использование ClassLoader-а и android-ного DexFile-а.

В Java, прежде чем использовать какой-то класс, нужно его загрузить, этим и занимается ClassLoader. Dex-файлы — это специальные файлы внутри APK, которые содержат в себе скомпилированный код ваших приложений. Фишка в том, что формат байт-кода, используемый в Android, отличается от стандартного формата в Java, и этот формат называется DEX.

Скомбинировав ClassLoader и DexFile, можно получить то, что нам нужно — список разрозненных экспериментов. Давайте разобьём реализацию на несколько последовательных шагов.

Абстрактный сканер кодовой базы

Первым шагом мы создадим абстрактный сканер нашей кодовой базы, который получит доступ к содержимому DexFile-ов и отфильтрует нужные классы:

Мы получили ClassLoader из объекта текущего потока, Затем открываем DexFile, передав туда package name нашего приложения. Так мы получим доступ к списку всех имён классов, доступных в DEX-файле.

Теперь нам надо как-то отфильтровать полученный список имён. Современные Android-приложения — это довольно сложные системы, которые подключают к себе тонну различных библиотек и работают на основе объёмной кодовой базы. Поэтому список имён классов может быть очень длинным. Если пытаться загружать каждый класс внутри DEX-а и анализировать, является ли он реализацией нужного нам интерфейса, это может быть довольно долгим процессом.

Поэтому сначала мы фильтруем классы по имени с помощью метода isAcceptableClassName и только после этого загружаем класс и проверяем, подходит ли он нам — проверка будет описана в методе isAcceptableClass.

Если класс нам подходит, вызываем метод-аккумулятор — onScanResult.

Реализация конкретного сканера

Опишем конкретный сканер для наших классов-экспериментов, реализовав наследника ClassScanner:

Чтобы быстро отфильтровать эксперименты по имени, мы вводим соглашение по их именованию — каждый класс эксперимента должен иметь суффикс Experiment. В методе isAcceptableClass мы проверяем, что класс является реализацией интерфейса Experiment, и что проверяемый класс не является самим интерфейсом — это нужно для того, чтобы мы могли создать инстанс класса через clazz.newInstance.

Метод-аккумулятор просто складывает инстанцированные эксперименты в список.

Использование сканера

Запускаем сканер, получаем из него список:

Но возникает вопрос: что отобразится в debug-панели при минифицированной сборке?

Включаем Proguard и обнаруживаем, что в списке экспериментов пусто.

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такоеВключили Proguard и теперь грустим

После этого всё заработает.

Выводы по ClassLoader-у и DexFile-у

Это рабочий способ, несмотря на то, что класс DexFile стал deprecated с API 26.

Но минусов вагон и маленькая тележка:

Способ требует соглашения по неймингу классов-экспериментов. Ко мне часто обращались коллеги, которые не находили свой эксперимент в debug-панели, потому что забывали о необходимом суффиксе Experiment;

Если вы хотите в minified-сборках получить доступ к списку экспериментов, придётся включать keep имён классов-экспериментов. Чем это плохо: потенциальные конкуренты могут посмотреть содержимое вашего APK-файла, увидеть там все названия классов-экспериментов, по именам понять, о чём этот эксперимент, и скопировать логику к себе;

Год спустя я уже не понимаю, почему мы выбрали именно этот способ работы с нашими экспериментами, ведь есть способы проще и лучше =)

З.Ы. По поводу «конкурентов» — чтобы затруднить реверс логики экспериментов в приложении, одной обфускации названий классов недостаточно. По-хорошему, ключами экспериментов должны быть какие-то чиселки, по которым нельзя отдалённо понять суть эксперимента, в релизном APK не должно быть никаких строковых литералов-описаний экспериментов.

ServiceLoader + META-INF/services

ServiceLoader — это специальный утилитный класс в Java, который существует там с незапамятных времён. Этот класс позволяет загружать список нужных объектов (сервисов) с помощью service provider-ов.

Service provider обычно представляет собой текстовый файл, который лежит по адресу/src/resources/META-INF/services/fqn.YourClass. Здесь fqn.YourClass — это полное имя с пакетом вашего общего интерфейса/абстрактного класса. В файле на каждой строчке описывается provider — полное имя класса, который реализует нужный вам интерфейс:

Кстати, совершенно неважно, в каком именно модуле вы создадите такой файлик, Gradle подхватит его, и добавит в итоговый APK.

Описав такой файлик, вы сможете воспользоваться методом ServiceLoader.load(YourClass::class.java), который отдаст вам Iterable с объектами. Главное, чтобы у ваших классов был дефолтный конструктор без параметров:

И вроде бы всё хорошо, но камнем преткновения становится как раз этот конфигурационный файл с описанием provider-ов, ведь мы снова приходим к merge-конфликтам, на этот раз — в файле META-INF/services.

Ещё один минус этого файла — он не поддерживает авторефакторинг. Если вы захотите изменить имя класса-эксперимента, перенести в другой пакет, то нужно будет не забыть подправить его имя в META-INF.

Делаем выводы:

Из плюсов: никаких внешних дополнительных зависимостей и простая реализация

Из минусов: проблема ручного заполнения файла META-INF.

Генерация META-INF/services файла

Довольно логичным кажется попробовать автоматически сгенерировать файл META-INF/services, чтобы не страдать от merge-конфликтов. Для этого, конечно же, уже есть несколько готовых библиотек, в частности:

Первая немного устарела, она начинает писать в билд-лог приложения довольно много всяких warning-ов, связанных с версией языка Java.

Таких warning-ов нет, если воспользоваться библиотечкой ClassIndex. Как с ней работать:

Подключаем её через compileOnly + kapt;

Навешиваем аннотацию @IndexSubclasses на нужный базовый интерфейс:

Подключаем библиотеку к каждому feature-модулю, где есть модельки экспериментов, для их индексации:

После этого мы спокойно используем метод ServiceLoader.load, получаем список экспериментов, радуемся.

Но есть, как говорится, нюансы.

Приключение с ClassIndex

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

Однако при использовании этого способа есть интересный момент.

Одним из недостатков способа с ClassLoader-ом я назвал необходимость сохранять имена классов-экспериментов в минифицированном APK. К чему это может привести? К тому, что злоумышленник-конкурент может легко обнаружить все классы-тоглы внутри вашего приложения, по названиям классов понять логику экспериментов, скопировать её, ну и так далее.

Разумеется, хотелось бы избежать аналогичной проблемы при использовании ClassIndex-а. Но вот незадача — ClassIndex по итогу своей работы генерирует файл META-INF/services/fqn.of.your.class.ClassName, который попадает в конечный APK. В этом файле аккуратно перечислены все имена классов-экспериментов, причём при обработке кода R8 будут перечислены уже минифицированные имена классов, что сильно упростит работу злоумышленникам. Что же делать?

Я отыскал только один надёжный способ выключения генерации META-INF-файла: не добавлять в library-модули, в которых, в основном, и объявляют классы-эксперименты, зависимости для ClassIndex-а. Из-за этого annotation processor не видит, что какой-то класс является наследником индексируемого интерфейса — и не добавляет его описание в итоговый META-INF-сервис.

Для этого я добавил простой Gradle-параметр, в зависимости от которого либо добавляем ClassIndex к library-модулю, либо нет:

В каждом модуле такое писать неудобно, поэтому это можно вынести в convention-плагин и подключать уже его.

На локальную разработку это никак не влияет — продуктовые разработчики не должны добавлять никаких дополнительных флагов или вообще как-то менять свой привычный флоу работы. А для сборки release APK на CI мы можем легко пробросить параметр:

Таким образом, мы избавились от двух проблем способа с ClassLoader-ом:

Нам не нужно иметь какое-то специальное соглашение по именованию классов-экспериментов;

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

Profit.

А где приключения-то?

А приключения были, пока я искал рабочий способ отключения генерации META-INF.

Первым делом я решил попробовать воспользоваться возможностью AGP убирать какие-либо файлы из итогового APK. Это можно сделать при помощи объекта PackagingOptions, к которому можно получить доступ через метод DSL-а packagingOptions. Мы настраиваем наши application-модули через convention-плагины, так что я дописал небольшой блок кода туда:

Но даже если написать такой exclude, то в конечном APK указанного файл всё равно будет присутствовать. Я до конца не понимаю почему так происходит, ведь annotation processor должен отрабатывать раньше, чем начинается запаковка файлов в итоговый APK.

Объект packagingOptions почему-то не помог нам в решении ситуации. Я подумал, что может быть это из-за того, что я указываю не минифицированное имя интерфейса-эксперимента. Добавил proguard-правило, которое сохраняло имя интерфейса Experiment, ничего не изменилось;

Пробовал удалять временные файлы META-INF, которые появлялись в разных модулях, в течение работы annotation processor-а. Я заметил, что в каждом модуле, куда я подключал kapt и зависимость для ClassIndex-а, в папке build/tmp/kapt3/classes/release/META-INF/services появлялся файл для ServiceLoader-а, в котором был записан FQN класса-эксперимента. Я пробовал удалять эти временные файлы до сборки итогового APK (через TaskProvider-ы, доступные в android.applicationVariants), но это не помогало;

Гипотетически, форкнув ClassIndex, и добавив в него флаг для annotation processor-а, можно было бы выключать создание META-INF файла для релизного APK, но это уже немного другая история.

Выводы по ServiceLoader-у и META-INF

Это самый простой способ из найденных мною;

Не требуется никаких зависимостей в runtime.

Плюс-минус: вам может потребоваться дополнительная работа kapt-а, правда, совсем незначительная, поэтому серьёзного импакта на скорость сборки быть не должно.

Reflections / Scannoation / Classgraph / etc

Третий способ внутри направления Java Reflections API — это использование различных библиотек. Когда вы попробуете загуглить что-то вроде «java Collect classes with annotation across the codebase», вы обнаружите огромное количество библиотек, связанных с Reflections API.

Я не стал рассматривать абсолютно все, а посмотрел на те, по которым была хорошая документация и много информации на StackOverflow — это Reflections и ClassGraph.

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

С другой стороны, в репозиториях на Github-е этих библиотек можно отыскать issues, где разработчики пытаются найти способ завести их под Android. Чётких инструкций вы там не найдёте, но там описывается способ, которым, теоретически, можно воспользоваться — список нужных вам классов можно собрать на момент компиляции ваших приложений. Когда компиляция полностью пройдет, вы в build-скриптах можете воспользоваться библиотекой, сгенерируете промежуточный файл, а потом в рантайме его прочитаете и всё будет офигенно!

Нет. Ну, по крайней мере, у меня не получилось это сделать за разумное время, возможно, вам повезёт больше.

Codegen / Bytecode patching

Это последнее направление, о котором я хотел рассказать — возможности кодогенерации.

В решении нашей задачи могут помочь два способа:

Написать свой собственный annotation processor;

Писать свой annotation processor можно, но зачем, если уже есть тот же ClassIndex, который сделает ровно то, что нужно. А вот вторая возможность выглядит интересно.

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

Коллеги из компании Joom написали библиотеку Colonist, которая идеально подходила под наши нужды. Забавный факт: статья про Colonist вышла буквально на следующий день после начала моего ресёрча.

Несмотря на то, что библиотека так и не вышла из alpha-статуса, ей можно пользоваться. Как это сделать:

Во-первых, нужно будет подключить библиотеку к вашему приложению. Подробнее про это можно почитать на Github-е:

Во-вторых, мы объявляем аннотацию-«колонию». Колония — это место, куда мы будем направлять «поселенцев», то есть, некоторый аккумулятор, который будет собирать нужные нам классы по правилам, которые мы задаём при помощи тонны аннотаций:

Мы описали аннотацию-колонию ExperimentsColony, в которую будем пускать определённых поселенцев — наследников класса Experiment (не оговорился, именно абстрактного класса Experiment). Также мы говорим Colonist-у, что поселенцев мы будем обрабатывать через специальный callback.

Остаётся описать класс-колонию / наш коллектор:

Отмечаем его аннотацией колонии (@ExperimentsColony), добавляем метод для приёма поселенцев (отмечен аннотацией @OnAcceptSettler), готово!

Что происходит под капотом: если компиляции APK посмотреть на него с помощью утилиты jadx-gui, то можно увидеть, что код коллектора отличается от того, что вы написали. Произошла магия изменения байт-кода, и теперь есть специальный кусочек кода, который собирает наследников нужного класса и складывает их в определенный метод.

feature flags что это такое. Смотреть фото feature flags что это такое. Смотреть картинку feature flags что это такое. Картинка про feature flags что это такое. Фото feature flags что это такоеСмотрим итоговый байт-код через jadx-gui

    Остаётся только использовать колонию

    Выводы по Colonist

    Это рабочее решение, хоть и используется альфа-версия библиотеки;

    Вы не можете отдебажить метод, который взаимодействует с поселенцами, потому что IDE ничего не знает про ваши манипуляции с байт-кодом;

    У библиотеки магия под капотом. Если обычный annotation processor — это ещё более-менее понятная вещь, вы можете пощупать сгенерированный код, то здесь происходит гораздо больше волшебства (если захотите разобраться с ASM — вот томик документации);

    Подведём итоги

    Во-первых, необязательно страдать от merge-конфликтов, ведь можно от них уйти;

    Есть множество способов собрать разбросанные по кодовой базе модели в единый список;

    Если бы у нас был Dagger, этой статьи, возможно, не было =)

    Совет для вас — не создавайте лишних абстракций, упрощайте жизнь себе и коллегам.

    Буду рад вопросам и дополнениям в комментариях.

    Маленький опрос для большого исследования

    Мы проводим ежегодное исследование технобренда крупнейших IT-компаний России. Нам очень нужно знать, что вы думаете про популярных (и не очень) работодателей. Опрос займет всего 10 минут.

    Источник

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

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