Се­год­ня в выпус­ке: оче­ред­ное напоми­нание, что диало­ги зап­росов пол­номочий в 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 при­ходит­ся искать пути миними­зации рис­ка. Для это­го уже было сде­лано нес­коль­ко шагов.

  1. В Google Play теперь есть белый спи­сок при­ложе­ний, которые могут получить раз­решение SYSTEM_ALERT_WINDOW без необ­ходимос­ти его зап­рашивать (рань­ше все при­ложе­ния из Google Play получа­ли его авто­мати­чес­ки).
  2. На­чиная с Android 8 овер­леи не могут перек­рывать стро­ку сос­тояния, а сам овер­лей мож­но быс­тро отклю­чить в панели уве­дом­лений. Это сде­лано для борь­бы с ransomware, показы­вающи­ми поверх экра­на овер­лей, который нель­зя никаким обра­зом отклю­чить.
  3. На­чиная с Android 10 при­ложе­ния, уста­нов­ленные не из Google Play, лиша­ются раз­решения на показ овер­леев через 30 секунд пос­ле того, как при­ложе­ние будет завер­шено или переза­пуще­но. При­ложе­ния из Google Play лишат­ся это­го раз­решения пос­ле перезаг­рузки.
  4. В нас­трой­ках Android теперь есть опция, пол­ностью зап­реща­ющая исполь­зовать овер­леи поверх окна нас­тро­ек (по умол­чанию отклю­чена).
 

Разработчику

 

Как сократить расход памяти приложением

Decrease memory usage of your Android app in half — оче­ред­ная статья о спо­собах сок­ратить исполь­зование опе­ратив­ной памяти при­ложе­нием.

1. Устра­ни утеч­ки памяти. Это мож­но сде­лать, исполь­зуя инс­тру­мент LeakCanary, который будет показы­вать уве­дом­ление каж­дый раз, ког­да есть подоз­рение на утек­шую активность, диалог или фраг­мент.
2. Про­ана­лизи­руй исполь­зование памяти гра­фичес­кими эле­мен­тами. Обыч­ные Bitmap’ы, исполь­зуемые в при­ложе­нии, могут при­вес­ти к ошиб­ке OutOfMemoryError, ког­да при­ложе­ние завер­шает­ся из‑за нех­ватки памяти. Что­бы это­го избе­жать, мас­шта­бируй изоб­ражения до мень­шего раз­мера, при­меняй кеширо­вание и сво­евре­мен­но уда­ляй закеши­рован­ную гра­фику. Вот нес­коль­ко советов, как сде­лать это с помощью популяр­ной биб­лиоте­ки заг­рузки изоб­ражений Glide.

  • По умол­чанию биб­лиоте­ка исполь­зует фор­мат ARGB_8888 для хра­нения изоб­ражений. Изме­нив его на RGB_565, мож­но вдвое сок­ратить исполь­зование памяти, не силь­но потеряв в качес­тве (мож­но исполь­зовать толь­ко на low-end-устрой­ствах):

    @GlideModule
    class 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);

3. Про­верь код навига­ции при­ложе­ния. При исполь­зовании пат­терна «Одна активность», ког­да в при­ложе­нии есть все­го одна активность (Activity), а все экра­ны пред­став­ляют собой фраг­менты (Fragment), ты можешь заметить, что при переме­щении на новый фраг­мент ста­рый фраг­мент не унич­тожа­ется. Это стан­дар­тное поведе­ние фраг­ментов, поэто­му заботить­ся об осво­бож­дении памяти дол­жен ты сам. Для это­го дос­таточ­но самос­тоятель­но унич­тожать все View в методе onDestroyView().

Дру­гие советы:

  • при исполь­зовании RecyclerView по воз­можнос­ти исполь­зуй notifyItemChanged() вмес­то notifyDataSetChanged();
  • не соз­давай допол­нитель­ных объ­ектов‑обер­ток там, где это­го мож­но избе­жать;
  • умень­ши раз­мер APK, это при­ведет к умень­шению памяти, занима­емой при­ложе­нием;
  • не хра­ни объ­екты «на вся­кий слу­чай»;
  • за­пус­кай бен­чмар­ки на релиз­ных бил­дах;
  • из­бавь­ся от избы­точ­ных ани­маций.
 

Kotlin и видимость API

Mastering API Visibility in Kotlin — статья о том, как сде­лать интерфей­сы биб­лиотек как мож­но более зак­рытыми, сох­ранив гиб­кость, воз­можнос­ти тес­тирова­ния и воз­можность вза­имо­дей­ство­вать с кодом на Java.

  1. Internal — твой друг. Этот модифи­катор видимос­ти чем‑то похож на package private в Java, но пок­рыва­ет не пакет, а целый модуль. Все клас­сы, поля и методы, помечен­ные этим клю­чевым сло­вом, будут вид­ны толь­ко внут­ри текуще­го модуля.

  2. Мо­дифи­катор internal мож­но исполь­зовать сов­мес­тно с анно­таци­ей @VisibleForTesting, что­бы тес­ты мог­ли дос­тучать­ся до нуж­ных методов и полей:

    @VisibleForTesting(otherwise = PRIVATE)
    internal var state: State
  3. В Java нет модифи­като­ра internal, поэто­му в байт‑коде все, что помече­но этим клю­чевым сло­вом, ста­нет public, но с одним важ­ным отли­чием: к его име­ни при­бавит­ся наз­вание модуля. Нап­ример, метод createEntity со сто­роны Java будет выг­лядеть как createEntity$имяМодуля. Это­го мож­но избе­жать с помощью анно­тации @JvmName, поз­воля­ющей ука­зать дру­гое имя для исполь­зования из Java:

    class Repository {
    @JvmName("pleaseDoNotCallThisMethod")
    internal fun createEntity() { ... }
    }

    Ес­ли же метод не дол­жен быть виден вооб­ще, мож­но исполь­зовать анно­тацию @JvmSynthetic:

    class Repository {
    @JvmSynthetic
    internal fun createEntity() { ... }
    }
  4. Explicit API mode — твой вто­рой друг. В Kotlin все объ­явле­ния по умол­чанию получа­ют модифи­катор public. А это зна­чит, что шанс забыть сде­лать метод internal или private высок. Спе­циаль­но для борь­бы с этой проб­лемой в Kotlin 1.4 появил­ся Explicit API mode, который зас­тавля­ет добав­лять модифи­катор видимос­ти к любым объ­явле­ниям. Что­бы его вклю­чить, дос­таточ­но добавить три стро­ки в кон­фиг Gradle:

    kotlin {
    explicitApi()
    }
  5. Од­но из неожи­дан­ных следс­твий исполь­зования internal — инлай­новые фун­кции не смо­гут исполь­зовать методы, помечен­ные этим клю­чевым сло­вом. Так про­исхо­дит потому, что код инлай­новой фун­кции пол­ностью встра­ивает­ся в вызыва­ющий код, а он не име­ет дос­тупа к методам, помечен­ным как internal. Решить эту проб­лему мож­но с помощью анно­тации @PublishedApi. Она сде­лает метод дос­тупным для инлай­новых фун­кций, но оста­вит зак­рытым для всех осталь­ных:

    @PublishedApi
    internal 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
Ана­томия ArrayMap

При добав­лении эле­мен­та ArrayMap сна­чала добав­ляет его хеш в пер­вый мас­сив, а затем ключ и зна­чение во вто­рой мас­сив, где индекс клю­ча выс­читыва­ется как индекс хеша в мас­сиве mHashes, умно­жен­ный на два, а индекс зна­чения как индекс клю­ча плюс один. В слу­чае кол­лизии (ког­да два раз­ных клю­ча име­ют оди­нако­вый хеш) ArrayMap про­изво­дит линей­ный поиск клю­ча в mArray и, если он не най­ден, добав­ляет новый хеш в mHashes и новые ключ:зна­чение в mArray. При дос­тижении пре­дель­ного раз­мера мас­сивов ArrayMap копиру­ет их в новый мас­сив, раз­мер которо­го выс­читыва­ется так: oldSize+(oldSize>>1) (4 → 8 → 12 → 18 → 27 → ...).

SparseArray пред­став­ляет собой тот же ArrayMap, но пред­назна­чен­ный для работы с типами дан­ных, где ключ — это int, а зна­чение может быть либо объ­ектом, либо прос­тым типом дан­ных: int, long, boolean (SparseIntArray, SparseLongArray, SparseBooleanArray). В ито­ге SparseArray нет необ­ходимос­ти хра­нить обер­тки над прос­тыми типами дан­ных.

Бла­года­ря избавле­нию от необ­ходимос­ти хра­нить допол­нитель­ный объ­ект для каж­дого эле­мен­та, ArrayMap ока­зыва­ется при­мер­но на 25% эко­ном­нее HashMap, а SparseArray поч­ти в два раза эко­ном­нее.

HashMap vs ArrayMap vs SparseArray: использование памяти для 1000 объектов
HashMap vs ArrayMap vs SparseArray: исполь­зование памяти для 1000 объ­ектов

В то же вре­мя ArrayMap и SparseArray в целом в два раза мед­леннее HashMap.

HashMap vs ArrayMap vs SparseArray: рандомные операции чтения
HashMap vs ArrayMap vs SparseArray: ран­домные опе­рации чте­ния

Вы­воды:

  • по воз­можнос­ти исполь­зуй 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
)
@Composable
fun MyComposeList(
modifier: Modifier = Modifier,
itemViewStates: List<ItemViewState>
) {
LazyColumnFor(modifier = modifier, items = itemViewStates) { viewState ->
MyListItem(itemViewState = viewState)
}
}
@Composable
fun 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 — биб­лиоте­ка для обра­бот­ки оши­бок.

1 комментарий

  1. Аватар

    Garta

    11.01.2021 в 23:09

    андроиды с превьюхи как мистеры мисиксы

Оставить мнение