Категория > Новости > Android: подмена системных диалогов и утечки памяти - «Новости»

Android: подмена системных диалогов и утечки памяти - «Новости»


12-01-2021, 00:00. Автор: Павел

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 для хра­нения клю­чей и их зна­чений (друг за дру­гом). Началь­ный раз­мер пер­вого — четыре, вто­рого — восемь.


Android: подмена системных диалогов и утечки памяти - «Новости»
Ана­томия 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 объ­ектов

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


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 — биб­лиоте­ка для обра­бот­ки оши­бок.


Перейти обратно к новости