Содержание статьи
Сегодня в выпуске: очередное напоминание, что диалоги запросов полномочий в Android можно подменить, инструкция, как оптимизировать потребление памяти приложением, рассказ об отличиях ArrayMap и SparseArray от HashMap, ультракороткая инструкция по созданию аналога RecyclerView с помощью Jetpack Compose, инструкции по ограничению видимости API библиотек. А также: очередная подборка библиотек для программистов и инструментов для пентеста.
Почитать
Подмена диалогов запроса полномочий
Подменяем Runtime permissions в Android — статья об уязвимости Android, позволяющей подменить системный диалог запроса разрешений своим, чтобы обманом заставить пользователя выдать приложению опасные полномочия.
В целом в статье нет ничего нового, и она просто описывает известный баг, а точнее, очередной design flaw Android, связанный с оверлеями. Если кратко, суть истории в том, что в Android есть специальное разрешение SYSTEM_ALERT_WINDOW, позволяющее «рисовать» поверх любых других окон. Это же разрешение можно использовать в корыстных целях, чтобы нарисовать поверх системного окна запроса полномочий, используемого приложениями для запроса прав на то или иное действие, свое собственное окно, которое предлагает пользователю дать разрешение на другое, более безобидное действие.
Google в курсе этой проблемы и даже «исправила» ее сразу при появлении в Android системы запроса разрешений (Android 6.0), но уже в версии Android 7.0 отказалась от исправления из‑за многочисленных жалоб пользователей софта с функцией SYSTEM_ALERT_WINDOW (экранные фильтры, системы жестовой навигации, различные всплывающие меню и так далее). Система просто блокировала возможность дать разрешение, если на экране находился оверлей.
Напомним, что SYSTEM_ALERT_WINDOW — одна из самых серьезных проблем Android. На ней построена опасная атака Cloak & Dagger, ее используют многие блокировщики экрана и банковские трояны. Однако исправить эту проблему, не сломав совместимость с существующим софтом, невозможно, и Google приходится искать пути минимизации риска. Для этого уже было сделано несколько шагов.
- В Google Play теперь есть белый список приложений, которые могут получить разрешение
SYSTEM_ALERT_WINDOW
без необходимости его запрашивать (раньше все приложения из Google Play получали его автоматически). - Начиная с Android 8 оверлеи не могут перекрывать строку состояния, а сам оверлей можно быстро отключить в панели уведомлений. Это сделано для борьбы с ransomware, показывающими поверх экрана оверлей, который нельзя никаким образом отключить.
- Начиная с Android 10 приложения, установленные не из Google Play, лишаются разрешения на показ оверлеев через 30 секунд после того, как приложение будет завершено или перезапущено. Приложения из Google Play лишатся этого разрешения после перезагрузки.
- В настройках Android теперь есть опция, полностью запрещающая использовать оверлеи поверх окна настроек (по умолчанию отключена).
Разработчику
Как сократить расход памяти приложением
Decrease memory usage of your Android app in half — очередная статья о способах сократить использование оперативной памяти приложением.
Устрани утечки памяти. Это можно сделать, используя инструмент LeakCanary, который будет показывать уведомление каждый раз, когда есть подозрение на утекшую активность, диалог или фрагмент.
Проанализируй использование памяти графическими элементами. Обычные Bitmap’ы, используемые в приложении, могут привести к ошибке OutOfMemoryError, когда приложение завершается из‑за нехватки памяти. Чтобы этого избежать, масштабируй изображения до меньшего размера, применяй кеширование и своевременно удаляй закешированную графику. Вот несколько советов, как сделать это с помощью популярной библиотеки загрузки изображений Glide:
- По умолчанию библиотека использует формат ARGB_8888 для хранения изображений. Изменив его на RGB_565, можно вдвое сократить использование памяти, не сильно потеряв в качестве (можно использовать только на low-end-устройствах):
@GlideModuleclass CustomGlideModuleV4 : AppGlideModule() { override fun applyOptions(context: Context, builder: GlideBuilder) { builder.setDefaultRequestOptions( RequestOptions().format(DecodeFormat.PREFER_RGB_565) ) }}GlideApp.with(view.context) .load("$imgUrl$IMAGE_URL_SIZE_SPEC") .into(view)
- Избежать проблемы можно, очищая кеш изображений при нехватке памяти. Для этого добавь в Application-класс приложения следующие строки:
override fun onTrimMemory(level: Int) { GlideApp.with(applicationContext).onTrimMemory(TRIM_MEMORY_MODERATE) super.onTrimMemory(level)}
- Чтобы уменьшить размер изображения, можно использовать такой код:
Glide .with(context) .load(url) .apply(new RequestOptions().override(600, 200)) .into(imageView);
-
Проверь код навигации приложения. При использовании паттерна «Одна активность», когда в приложении есть всего одна активность (Activity), а все экраны представляют собой фрагменты (Fragment), ты можешь заметить, что при перемещении на новый фрагмент старый фрагмент не уничтожается. Это стандартное поведение фрагментов, поэтому заботиться об освобождении памяти должен ты сам. Для этого достаточно самостоятельно уничтожать все View в методе
onDestroyView(
.)
Другие советы:
- при использовании RecyclerView по возможности используй
notifyItemChanged(
вместо) notifyDataSetChanged(
;) - не создавай дополнительных объектов‑оберток там, где этого можно избежать;
- уменьши размер APK, это приведет к уменьшению памяти, занимаемой приложением;
- не храни объекты «на всякий случай»;
- запускай бенчмарки на релизных билдах;
- избавься от избыточных анимаций.
Kotlin и видимость API
Mastering API Visibility in Kotlin — статья о том, как сделать интерфейсы библиотек как можно более закрытыми, сохранив гибкость, возможности тестирования и возможность взаимодействовать с кодом на Java.
Internal — твой друг. Этот модификатор видимости чем‑то похож на package private в Java, но покрывает не пакет, а целый модуль. Все классы, поля и методы, помеченные этим ключевым словом, будут видны только внутри текущего модуля.
Модификатор
internal
можно использовать совместно с аннотацией@VisibleForTesting
, чтобы тесты могли достучаться до нужных методов и полей:
@VisibleForTesting(otherwise = PRIVATE)internal var state: State
- В Java нет модификатора
internal
, поэтому в байт‑коде все, что помечено этим ключевым словом, станетpublic
, но с одним важным отличием: к его имени прибавится название модуля. Например, методcreateEntity
со стороны Java будет выглядеть какcreateEntity$имяМодуля
. Этого можно избежать с помощью аннотации@JvmName
, позволяющей указать другое имя для использования из Java:
class Repository { @JvmName("pleaseDoNotCallThisMethod") internal fun createEntity() { ... }}
Если же метод не должен быть виден вообще, можно использовать аннотацию @JvmSynthetic
:
class Repository { @JvmSynthetic internal fun createEntity() { ... }}
-
Explicit API mode — твой второй друг. В Kotlin все объявления по умолчанию получают модификатор public. А это значит, что шанс забыть сделать метод
internal
илиprivate
высок. Специально для борьбы с этой проблемой в Kotlin 1.4 появился Explicit API mode, который заставляет добавлять модификатор видимости к любым объявлениям. Чтобы его включить, достаточно добавить три строки в конфиг Gradle:
kotlin { explicitApi()}
- Одно из неожиданных следствий использования
internal
— инлайновые функции не смогут использовать методы, помеченные этим ключевым словом. Так происходит потому, что код инлайновой функции полностью встраивается в вызывающий код, а он не имеет доступа к методам, помеченным какinternal
. Решить эту проблему можно с помощью аннотации@PublishedApi
. Она сделает метод доступным для инлайновых функций, но оставит закрытым для всех остальных:
@PublishedApiinternal fun secretFunction() { println("through the mountains")}public inline fun song() { secretFunction()}fun clientCode() { song() // ok secretFunction() // Нет доступа}
Что такое ArrayMap и SparseArray
All you need to know about ArrayMap & SparseArray — статья об ArrayMap и SparseArray, двух фирменных, но не так хорошо известных коллекциях Android. Обе коллекции по сути аналоги HashMap из Java с тем исключением, что они созданы специально, чтобы минимизировать потребление оперативной памяти.
В отличие от HashMap, который для хранения каждого объекта создает новый объект и сохраняет его в массиве, ArrayMap не создает дополнительный объект, но использует два массива: mHashes для последовательного хранения хешей ключей и mArray для хранения ключей и их значений (друг за другом). Начальный размер первого — четыре, второго — восемь.
При добавлении элемента ArrayMap сначала добавляет его хеш в первый массив, а затем ключ и значение во второй массив, где индекс ключа высчитывается как индекс хеша в массиве mHashes, умноженный на два, а индекс значения как индекс ключа плюс один. В случае коллизии (когда два разных ключа имеют одинаковый хеш) ArrayMap производит линейный поиск ключа в mArray и, если он не найден, добавляет новый хеш в mHashes и новые ключ:значение в mArray. При достижении предельного размера массивов ArrayMap копирует их в новый массив, размер которого высчитывается так: oldSize+(
(4 → 8 → 12 → 18 → 27 → ...).
SparseArray представляет собой тот же ArrayMap, но предназначенный для работы с типами данных, где ключ — это int, а значение может быть либо объектом, либо простым типом данных: int, long, boolean (SparseIntArray, SparseLongArray, SparseBooleanArray). В итоге SparseArray нет необходимости хранить обертки над простыми типами данных.
Благодаря избавлению от необходимости хранить дополнительный объект для каждого элемента, ArrayMap оказывается примерно на 25% экономнее HashMap, а SparseArray почти в два раза экономнее.
В то же время ArrayMap и SparseArray в целом в два раза медленнее HashMap.
Выводы:
- по возможности используй ArrayMap;
- используй SparseArray, если ключи имеют тип int;
- если размер коллекции известен — указывай его в конструкторе.
RecyclerView с помощью Jetpack Compose
How to make a RecyclerView in Jetpack Compose — краткая заметка о том, как создать собственный RecyclerView, используя библиотеку Jetpack Compose.
RecyclerView — известный и очень популярный элемент интерфейса Android, позволяющий создать динамически формируемый (бесконечный) список элементов с ленивой загрузкой и переиспользуемыми элементами UI. Говоря простыми словами: RecyclerView — это быстрый список из произвольного количества элементов, который будет расходовать память только на те элементы, что в данный момент находятся на экране.
RecyclerView — очень мощный и сложный инструмент. Чтобы создать список с его помощью, необходимо создать сам RecyclerView, подключить к нему адаптер, который будет наполнять его элементами, подключить менеджер лейаутов и создать один или несколько viewHolder’ов, которые будут хранить графическое представление элементов списка.
А теперь посмотрим, как создать аналог RecyclerView с использованием фреймворка Jetpack Compose:
data class ItemViewState( val text: String)@Composablefun MyComposeList( modifier: Modifier = Modifier, itemViewStates: List<ItemViewState>) { LazyColumnFor(modifier = modifier, items = itemViewStates) { viewState -> MyListItem(itemViewState = viewState) }}@Composablefun MyListItem(itemViewState: ItemViewState) { Text(text = itemViewState.text)}
Это действительно все.
Валидация форм с помощью Kotlin Flow
Using Flows for Form Validation in Android — короткая заметка о том, как реализовать валидацию форм с помощью Kotlin Flow. Интересна в первую очередь в качестве простой и наглядной демонстрации работы недавно появившегося StateFlow.
Допустим, у нас есть форма с тремя полями: First Name, Password и User Id. Наша задача — сделать так, чтобы кнопка Submit активировалась лишь в том случае, если поле First Name содержит только символы латинского алфавита, поле Password содержит как минимум восемь символов, а поле User Id содержит хотя бы один символ подчеркивания.
Для хранения текущего значения поля будем использовать StateFlow:
private val _firstName = MutableStateFlow("")private val _password = MutableStateFlow("")private val _userID = MutableStateFlow("")
Дополнительно создадим три метода, чтобы записывать значения в эти StateFlow:
fun setFirstName(name: String) { _firstName.value = name}fun setPassword(password: String) { _password.value = password}fun setUserId(id: String) { _userID.value = id}
Теперь объединим все три StateFlow в один Flow, который будет отдавать только значения true или false:
val isSubmitEnabled: Flow<Boolean> = combine(_firstName, _password, _userID) { firstName, password, userId -> val regexString = "[a-zA-Z]+" val isNameCorrect = firstName.matches(regexString.toRegex()) val isPasswordCorrect = password.length > 8 val isUserIdCorrect = userId.contains("_") return@combine isNameCorrect and isPasswordCorrect and isUserIdCorrect}
Этот код будет запускаться каждый раз, когда состояние любого из трех StateFlow изменится.
Теперь осталось только привязать три первых StateFlow к полям ввода:
private fun initListeners() { editText_name.addTextChangedListener { viewModel.setFirstName(it.toString()) } editText_password.addTextChangedListener { viewModel.setPassword(it.toString()) } editText_user.addTextChangedListener { viewModel.setUserId(it.toString()) }}
А состояние кнопки Submit привязать к полученному в результате преобразования Flow:
private fun collectFlow() { lifecycleScope.launch { viewModel.isSubmitEnabled.collect { value -> submit_button.isEnabled = value } }}
Что делает весь этот код? При изменении любого из полей ввода будет автоматически изменено значение одного из трех StateFlow. Это, в свою очередь, повлечет за собой запуск функции combine
, которая в итоге выпустит новое значение в поток isSubmitEnabled
. На это действие среагирует код внутри функции collectFlow(
. В итоге он изменит состояние кнопки.
Инструменты
- Apk-medit — утилита для поиска и изменения данных в памяти (аналог ArtMoney);
- APKLab — плагин VS Code для реверса и пересборки APK;
- RASEv1 — скрипт для рутинга стандартного эмулятора Android.
Библиотеки
- Speedometer — полукруглый прогресс‑бар;
- NoNameBottomBar — очередная панель управления в нижней части экрана;
- Bottom-sheets — коллекция диалогов в нижней части экрана: время, календарь, цвет и прочее;
- libCFSurface — библиотека, позволяющая выводить информацию на экран напрямую, используя права root;
- Strong-frida — патчи для Frida, позволяющие избежать обнаружения фреймворка;
- Circle-menu — круговое меню;
- onboardingflow — подсветка элемента интерфейса для обучающих экранов;
- fingerprint-android — библиотека для фингепринтинга устройств;
- Flower — библиотека на базе Kotlin Flow для организации получения и кеширования данных из сети;
- Kable — библиотека для асинхронной работы с BLE-устройствами;
- Accompanist — функции для разработки приложения на Jetpack Compose;
- Simple Settings — библиотека для создания экранов настроек;
- Belay — библиотека для обработки ошибок.