Activity, Service, BroadcastReceiver и ContentProvider. Каждый объявляется в манифесте и служит точкой входа, через которую система сама может запустить ваше приложение; Application не точка входа, а объект на весь процесс, который создаётся раньше любого из них.
Activity: один экран, с которым взаимодействует пользователь; запускается через Intent.
Service: работа без UI: foreground-сервис, который пользователь видит (музыка, навигация), или bound-сервис, к которому обращается другой код. Отложенную фоновую работу отдают WorkManager.
BroadcastReceiver: реагирует на события системы или приложения, например BOOT_COMPLETED, и должен отрабатывать быстро.
ContentProvider: отдаёт данные другим приложениям или процессам через URI content://; FileProvider входит в их число.
AndroidJunior
Основы Android
Activity, Fragment, жизненный цикл, Intent, Manifest, configuration changes
Закрыто вопросов
0 / 25
- 0Повторить
- 16Часто спрашивают
Вопросы
onCreate → onStart → onResume выводят Activity на экран, onPause → onStop → onDestroy убирают её. onCreate выполняется один раз на экземпляр, поэтому разовую настройку вроде setContentView() делают там. Activity-маршрутизатор, вызвавшая finish() в onCreate, сразу попадает в onDestroy.
**onCreate**: задать контент (setContentView или setContent {} в Compose), создать биндинги, прочитать savedInstanceState. В onResume дерево вью пересоздавалось бы при каждом возвращении пользователя.
**onStart / onStop**: экран виден / больше не виден. Здесь запускают и останавливают UI-подписки, либо используют repeatOnLifecycle(STARTED).
**onResume / onPause**: в фокусе / теряет фокус. onPause должен быть быстрым: onResume следующей Activity ждёт его.
**onSaveInstanceState**: на API 28+ вызывается после onStop, чтобы сохранить небольшое UI-состояние в Bundle.
**onDestroy**: isFinishing отличает настоящее закрытие от смены конфигурации, которая тоже уничтожает экземпляр.
**Только onDestroy**: вызов finish() внутри onCreate (Activity-маршрутизатор, trampoline) сразу ведёт в onDestroy. А когда система убивает фоновый процесс, не вызывается вообще ничего, так что сохранять данные в onDestroy нельзя.
Поворот означает смену конфигурации: Activity уничтожается и создаётся заново с новыми ресурсами, поэтому её поля теряются. ViewModel переживает пересоздание, а saved-state Bundle (onSaveInstanceState, SavedStateHandle, rememberSaveable) переживает ещё и смерть процесса.
Другие смены конфигурации действуют так же: язык, тёмная тема, масштаб шрифта, подключённая клавиатура, ресайз в multi-window.
**ViewModel**: хранится в ViewModelStore между пересозданиями; держит загруженные данные и состояние экрана. Пропадает вместе с процессом.
Saved-state Bundle: небольшое UI-состояние: поисковый запрос, выбранная вкладка, id. SavedStateHandle отдаёт его во ViewModel, а rememberSaveable отдаёт его в composable.
Почему Bundle, а не SharedPreferences: Bundle принадлежит этому экрану в этой задаче и выбрасывается, когда пользователь его закрывает. SharedPreferences хранили бы значение вечно, утекли бы в следующий запуск и стоили бы записи на диск.
**android:configChanges**: отключает пересоздание, и тогда новые ресурсы вы обрабатываете сами в onConfigurationChanged. Годится для видеоплеера, но не как универсальное лекарство от потери состояния.
Context даёт вам доступ к системе: ресурсы, настройки, запуск компонентов. Виды различаются прежде всего временем жизни, и контекст Activity нельзя держать ни в чём, что живёт дольше самой Activity.
Application context: applicationContext, живёт весь процесс. Именно его должны хранить синглтоны, базы данных, DataStore и репозитории.
Activity context: свой у каждого экземпляра Activity, умирает вместе с ним. Несёт тему и окно Activity, поэтому он нужен диалогам, инфлейту разметки и всему, что зависит от темы; диалог, показанный из application context, падает с BadTokenException.
Контексты сервиса и ресивера: короткоживущие; контекст ресивера действителен только во время onReceive.
Классическая утечка: статическое поле, синглтон или неотписанный слушатель, держащий Activity, удерживает в памяти её саму, всё дерево вью и битмапы. Сужайте контекст до applicationContext в конструкторе всего долгоживущего; с Hilt внедряйте @ApplicationContext. ViewModel вообще не должна хранить Context; держите в состоянии id строкового ресурса и превращайте его в текст в UI.
В Application.onCreate(): он выполняется один раз на процесс, на главном потоке, раньше любой Activity, Service или ресивера. Он должен оставаться маленьким, потому что стоит на пути каждого холодного старта, и не должен хранить состояние приложения, потому что убитый процесс уносит с собой все поля.
Процесс стартует не только по нажатию на иконку: бродкаст, задача WorkManager, запрос к ContentProvider. Каждый такой старт выполняет Application.onCreate вообще без UI.
ContentProvider, объявленные в манифесте, создаются после attachBaseContext и до Application.onCreate; так библиотеки незаметно инициализируют себя. androidx.startup объединяет их в один провайдер с явными зависимостями.
Здесь место: графу DI (@HiltAndroidApp), крэш-репортингу, логированию.
Здесь не место: чтению с диска или из сети, жадной инициализации SDK, текущему пользователю или корзине в поле. После смерти процесса пользователь возвращается на тот же экран, а все поля Application и синглтоны object снова в начальном состоянии; долговечное состояние храните в Room, DataStore или SavedStateHandle.
Чтобы отслеживать переход всего приложения в фон и обратно, подпишитесь на ProcessLifecycleOwner, а не считайте Activity вручную.
Манифест сообщает системе, что содержит приложение, ещё до запуска его кода: компоненты, интенты, которые они принимают, нужные разрешения и железо. При сборке Gradle сливает ваш манифест с манифестами всех библиотек и вариантов сборки в один файл.
Компоненты: <activity>, <service>, <receiver>, <provider>. У любого компонента с <intent-filter> должен быть явно указан android:exported (Android 12+).
Разрешения и фичи: <uses-permission>, <uses-feature> (Play ещё и фильтрует по нему устройства), <queries> для видимости пакетов.
Атрибуты приложения: тема, иконка, класс Application, бэкап и конфигурация сетевой безопасности.
Не здесь: applicationId, minSdk, targetSdk и версия берутся из build.gradle.kts и подставляются при сборке.
Слияние: приоритет: вариант сборки, затем main, затем библиотеки. Конфликты решают через tools:replace, а ненужный элемент из библиотеки убирают tools:node="remove". Итоговый файл (build/intermediates/merged_manifests/) и отчёт (build/outputs/logs/manifest-merger-*-report.txt) показывают, откуда взялось неожиданное разрешение; то же показывает вкладка Merged Manifest в Android Studio.
compileSdk определяет, с какими API можно компилироваться, minSdk задаёт самую старую версию Android, на которую ставится приложение, а targetSdk определяет, какие изменения поведения платформы система к вам применяет. Подняв targetSdk, вы разом получаете все изменения поведения до этого уровня.
compileSdk влияет только на сборку; его повышение никогда не меняет поведение в рантайме, а лишь позволяет обращаться к новым API.
minSdk отражает продуктовое решение по вашей аудитории. Вызов более нового API без проверки Build.VERSION.SDK_INT или @RequiresApi даёт lint-ошибку NewApi.
targetSdk обозначает уровень, под который вы протестировались. Для приложений со старым target система сохраняет слой совместимости; поднимите его, и получите новые правила: разрешение на уведомления (33), типы foreground-сервисов (34), edge-to-edge по умолчанию (35) и так далее.
Play требует свежий targetSdk для обновлений, так что отложить повышение навсегда нельзя. Прочитайте список изменений поведения для каждого пропускаемого уровня, проверьте разрешения, фоновую работу и доступ к файлам, и выпускайте повышение отдельным релизом.
Все три значения задаются в build.gradle.kts, а не в манифесте.
Android сравнивает квалификаторы в имени каждой папки с текущей конфигурацией устройства, отбрасывает все папки, которые ей противоречат, и среди оставшихся выбирает победителя по фиксированному порядку приоритета. Папка без квалификаторов служит запасным вариантом.
Квалификаторы живут в именах папок: values-ru, values-night, drawable-xxhdpi, layout-sw600dp, values-v33.
Порядок приоритета фиксирован: MCC/MNC, затем язык и регион, направление разметки, smallest width, доступная ширина и высота, размер экрана, ориентация, ночной режим, плотность и в самом конце версия платформы.
Папка по умолчанию (values/) должна содержать каждый ресурс, иначе устройство, не подошедшее ни под один квалификатор, упадёт с Resources.NotFoundException. Lint предупреждает об отсутствующем значении по умолчанию.
Плотность является единственным квалификатором, который подбирается приблизительно: если точной папки нет, система берёт ближайшую и масштабирует. VectorDrawable вообще избавляет от папок по плотности.
Смена конфигурации запускает поиск заново, поэтому смена языка или тёмной темы пересоздаёт Activity. Под большие экраны разветвляйтесь через sw600dp или, в Compose, через классы размеров окна (window size classes) в коде.
Intent представляет собой сообщение с просьбой к системе запустить компонент. Явный интент называет конкретный класс; неявный называет действие и данные, и система ищет приложение, чей <intent-filter> им соответствует.
Явный: Intent(context, DetailActivity::class.java); используется внутри своего приложения.
Неявный: ACTION_VIEW, ACTION_SEND, ACTION_DIAL. При поиске сопоставляются действие, категории (Activity нужна CATEGORY_DEFAULT), схема данных и MIME-тип. Если совпадений несколько, показывается выбор приложения; если ни одного, летит ActivityNotFoundException, поэтому его нужно ловить. С Android 11 для запросов о других приложениях нужна ещё запись в <queries>.
Extras: данные едут в Bundle, поэтому держите их маленькими: id, а не список объектов.
Результат: Activity Result API: registerForActivityResult(contract) { }, затем launch(). Он заменил устаревшие startActivityForResult/onActivityResult. Регистрировать нужно до состояния STARTED (в поле класса), иначе будет IllegalStateException.
**PendingIntent**: интент, обёрнутый идентичностью вашего приложения, чтобы другое приложение (уведомления, будильники) могло запустить его позже.
Fragment представляет собой переиспользуемый кусок UI со своим жизненным циклом, живущий внутри Activity. Его состояние никогда не опережает состояние хоста, а его вью может быть уничтожена, пока сам объект Fragment жив, поэтому у вью свой жизненный цикл: viewLifecycleOwner.
Колбэки: onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume, затем onPause → onStop → onDestroyView → onDestroy → onDetach.
Два жизненных цикла: когда фрагмент заменяют и кладут в back stack, вызывается только onDestroyView; экземпляр остаётся и получает новую вью при возврате назад. Подписывайте UI на viewLifecycleOwner, иначе слушатель продолжит обновлять мёртвую вью и удержит её в памяти.
ViewBinding: по той же причине установите биндинг в null в onDestroyView.
Когда использовать: View-приложения с одной Activity и многими экранами, планшетные master-detail, переиспользуемые куски UI. В приложении на Compose фрагментов обычно нет: одна Activity хостит composable-экраны.
FragmentManager сам пересоздаёт фрагменты после смены конфигурации или смерти процесса и умеет вызывать только пустой конструктор. Данные должны ехать в Bundle arguments, который система сохраняет и восстанавливает за вас.
При пересоздании FragmentFactory.instantiate вызывает конструктор без аргументов и заново прикрепляет сохранённые arguments. Если есть только конструктор с одним параметром, вылетает Fragment.InstantiationException; поле, заданное через сеттер, молча вернулось бы пустым.
arguments задают до добавления фрагмента, отсюда идиома фабрики newInstance(...) с bundleOf(...).
Кладите в Bundle только идентичность (id, небольшой enum), а не загруженные объекты: он становится частью сохранённого состояния.
SavedStateHandle во ViewModel фрагмента заранее заполнен теми же аргументами, так что ViewModel может прочитать id напрямую. С Navigation этот Bundle за вас собирают Safe Args или типизированные маршруты.
Зависимости (не данные экрана) можно передавать через конструктор только с собственной FragmentFactory, назначенной FragmentManager до super.onCreate. Hilt так не делает: в @AndroidEntryPoint-фрагмент зависимости внедряются в поля.
У каждого разрешения есть уровень защиты. Normal выдаются при установке по одной записи в манифесте, signature выдаются только приложениям, подписанным тем же сертификатом, что и объявившее разрешение, а dangerous нужно ещё и запросить в рантайме, и пользователь может отозвать их в любой момент.
Normal: INTERNET, VIBRATE: низкий риск, достаточно записи в манифесте.
Signature: для компонентов приложений одного разработчика; без диалога.
Dangerous: камера, геолокация, контакты, POST_NOTIFICATIONS (Android 13+). Объявить в манифесте, каждый раз проверять через ContextCompat.checkSelfPermission, запрашивать через ActivityResultContracts.RequestPermission.
Special: оверлеи, точные будильники, доступ ко всем файлам: диалога нет, пользователя отправляют на экран настроек.
«Больше не спрашивать»: с Android 11 после двух отказов система перестаёт показывать диалог и сразу возвращает отказ. shouldShowRequestPermissionRationale подсказывает, когда сначала объяснить причину; после окончательного отказа остаётся только страница приложения в настройках.
Предпочитайте API, которым разрешение вообще не нужно, например Photo Picker.
onPause означает, что Activity потеряла фокус, но может оставаться видимой; onStop приходит, только когда она полностью скрыта. Полупрозрачная или диалоговая Activity сверху, picture-in-picture и, до Android 10, потеря фокуса в split screen дают только onPause.
**AlertDialog или DialogFragment: это не отдельная Activity, а ещё одно ваше окно: никакой колбэк жизненного цикла не вызывается. Классический вопрос с подвохом.
Диалоговая или полупрозрачная Activity сверху**: только onPause; вы остаётесь видны под ней.
Picture-in-picture: при входе в PiP Activity ставится на паузу, но остаётся на экране; onPictureInPictureModeChanged сообщает о переключении. Видео должно играть до onStop, а не до onPause.
Multi-window (Android 10+): все видимые Activity одновременно в состоянии RESUMED. Только одна из них top-resumed, о чём сообщает onTopResumedActivityChanged, и эксклюзивные ресурсы вроде камеры следуют этому колбэку, а не onResume.
Ресайз в split screen: это смена конфигурации, так что Activity пересоздаётся, если не обрабатывает изменение сама.
Какой колбэк означает «на переднем плане»: на Android 10+ это onTopResumedActivityChanged(true); onResume лишь значит, что Activity видна и интерактивна, и возобновлённых Activity может быть несколько.
Extras и сохранённое состояние сплющиваются в Parcel и уходят через Binder; Parcelable даёт явный и быстрый формат, Serializable опирается на рефлексию и медленнее. Буфер Binder занимает около 1 МБ на процесс, поэтому большой Bundle падает с TransactionTooLargeException.
Bundle хранит примитивы, String, массивы и объекты Parcelable или Serializable; всё остальное падает в рантайме.
Parcelable пишите через плагин kotlin-parcelize: @Parcelize генерирует writeToParcel и CREATOR. Рукописные парселы ломаются, когда порядок чтения расходится с порядком записи.
Serializable работает через рефлексию, создаёт много временных объектов и заметно медленнее. Он остаётся для совместимости с Java.
С API 33 нетипизированный getParcelableExtra(name) устарел; используйте IntentCompat.getParcelableExtra(intent, key, Order::class.java) или BundleCompat.
Буфер в 1 МБ общий для всех транзакций в полёте. Список из сотен строк или Bitmap в onSaveInstanceState дают TransactionTooLargeException, часто после onStop, далеко от кода, который их туда положил.
Правило, которое снимает обе проблемы: передавайте id, а следующий экран пусть загрузит объект из своего репозитория.
Когда приложение в фоне, система может убить его процесс, чтобы освободить память. Когда пользователь возвращается, Android восстанавливает задачу и верхнюю Activity из сохранённого состояния, поэтому выживают только saved-state Bundle и данные на диске; ViewModel, синглтоны и статические поля начинают с нуля.
Выживает: back stack, extras интентов, arguments фрагментов, onSaveInstanceState, SavedStateHandle, rememberSaveable, back stack Navigation, Room, DataStore и файлы.
Теряется: экземпляры ViewModel, синглтоны object, поля Application, кэши в памяти, запущенные корутины.
В отличие от поворота: ViewModel теряет только смерть процесса. Смахивание приложения из «Недавних» или Force stop даёт третий случай: задача и её сохранённое состояние выбрасываются, и ничего не восстанавливается.
Типичные симптомы: UninitializedPropertyAccessException при возврате, пустой экран, «приложение само перезапустилось».
Как воспроизвести: уведите приложение в фон и выполните adb shell am kill <package>, который убивает только фоновый процесс, затем откройте приложение из «Недавних». Опция разработчика «Don't keep activities» уничтожает Activity, но сохраняет процесс, поэтому потерянные синглтоны она не ловит.
Разовый результат передают через Fragment Result API (setFragmentResult / setFragmentResultListener); общее, постоянно меняющееся состояние передают через ViewModel, привязанную к их общему владельцу. Держать ссылку на другой фрагмент или приводить Activity к интерфейсу-слушателю считается устаревшим подходом.
Fragment Result API: FragmentManager хранит результат, пока слушатель не будет хотя бы в STARTED, и переживает пересоздание. Соседи используют parentFragmentManager, а родитель, слушающий дочерний фрагмент, использует childFragmentManager.
Общая ViewModel: by activityViewModels() или by navGraphViewModels(R.id.checkout); оба фрагмента наблюдают один StateFlow. Подходит для master-detail и многошаговых форм.
Интерфейс к хосту: context as Listener в onAttach. Работает, но привязывает фрагмент к одной Activity и легко приводит к утечке.
Никогда не храните ссылку на другой фрагмент: после пересоздания она указывает на мёртвый экземпляр.
С Navigation: результаты передаются через previousBackStackEntry?.savedStateHandle, который переживает и смерть процесса.
replace() удаляет фрагменты, которые сейчас в контейнере, и добавляет новый; add() кладёт новый сверху, оставляя старый живым вместе с его вью. addToBackStack() записывает транзакцию, чтобы «Назад» её откатил, а childFragmentManager управляет фрагментами, вложенными во фрагмент.
**add**: старый фрагмент остаётся RESUMED со своей вью: экраны накладываются, нажатия проходят сквозь прозрачный фон, растёт память. Полезно для оверлеев.
**replace: вью старого фрагмента уничтожается; экземпляр выживает только в back stack.
addToBackStack(name)**: без него «Назад» на экране деталей закрывает Activity. popBackStack(name, POP_BACK_STACK_INCLUSIVE) возвращает к именованной точке.
Какой менеджер: supportFragmentManager принадлежит Activity, parentFragmentManager держит этот фрагмент, а childFragmentManager нужен для фрагментов внутри него (вкладки, ViewPager2). Неправильный выбор даёт дочерние фрагменты, которые переживают родителя.
**setReorderingAllowed(true)**: позволяет менеджеру оптимизировать транзакцию; нужен для корректных переходов.
commit() планирует транзакцию на главном потоке, commitNow() выполняет её синхронно, а commitAllowingStateLoss() пропускает проверку сохранённого состояния. Падение означает, что вы закоммитили после того, как Activity сохранила состояние, и при смерти процесса транзакция потерялась бы.
onSaveInstanceState вызывается при остановке Activity. Транзакция, закоммиченная после него, не попадает в сохранённое состояние, поэтому FragmentManager бросает IllegalStateException, а не теряет экран молча.
Обычная причина: колбэк, пришедший, пока приложение в фоне: ответ сети, наблюдатель без привязки к жизненному циклу, диалог, показанный по завершении операции.
**commitAllowingStateLoss()**: превращает падение в экран, который может так и не появиться. Настоящее исправление состоит в том, чтобы доставлять событие только пока UI в STARTED, через repeatOnLifecycle.
**commitNow()**: выполняется сразу, но несовместим с addToBackStack(). executePendingTransactions() выполняет всё, что запланировано через commit().
В приложении на Navigation эти транзакции делает библиотека, но правило жизненного цикла то же.
Режим запуска решает, создаёт ли запуск Activity новый экземпляр или переиспользует существующий. Чтобы из A-B-C-D вернуться к существующей A, запустите A с FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP: B, C и D уничтожаются, а A получает onNewIntent.
**standard: каждый раз новый экземпляр; режим по умолчанию и почти всегда правильный.
singleTop**: если Activity уже наверху, она переиспользуется и получает onNewIntent вместо второй копии. Типично для экрана, открываемого из уведомлений и диплинков.
**singleTask: один экземпляр на задачу; его запуск очищает всё, что выше. Им злоупотребляют, потому что он молча уничтожает экраны сверху.
singleInstance: Activity одна в своей собственной задаче. Встречается редко.
Флаги на один запуск**: CLEAR_TOP без SINGLE_TOP уничтожает и создаёт заново standard-A (singleTop- или singleTask-A получает onNewIntent). Для выхода из аккаунта используйте FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK: старая задача стирается, стартует свежая A.
**taskAffinity**: к какой задаче Activity предпочитает принадлежать; важен в связке с singleTask и NEW_TASK.
Приложение на Compose с одной Activity почти не касается этого: back stack принадлежит NavController, и для диплинков важен только режим главной Activity.
BroadcastReceiver реагирует на системные события и события приложений; ресивер из манифеста может запустить ваш процесс, а зарегистрированный в коде существует, только пока зарегистрирован. Android 8 перестал доставлять большинство неявных бродкастов ресиверам из манифеста, а Android 14 требует флаг экспорта в registerReceiver.
Ресивер в манифесте: работает, даже когда приложение не запущено, но с API 26 только для явных бродкастов и короткого списка исключений (BOOT_COMPLETED, LOCKED_BOOT_COMPLETED, MY_PACKAGE_REPLACED и ещё несколько).
Ресивер в коде: registerReceiver и отписка в зеркальном колбэке жизненного цикла, иначе утечёт Context.
Android 14: приложения с target 34 обязаны передавать RECEIVER_EXPORTED или RECEIVER_NOT_EXPORTED для любого несистемного бродкаста, иначе SecurityException. Для старых версий используйте ContextCompat.registerReceiver.
**onReceive**: выполняется на главном потоке, и процесс гарантированно жив только во время вызова (около 10 с до ANR). Для долгой работы вызовите goAsync() или поставьте задачу в WorkManager.
Замены: вместо CONNECTIVITY_ACTION теперь ConnectivityManager.registerNetworkCallback; LocalBroadcastManager устарел в пользу SharedFlow.
ContentProvider отдаёт данные через URI content:// с интерфейсом query/insert/update/delete, а система проверяет доступ к нему по разрешениям. Он нужен, чтобы делиться данными с другими приложениями или процессами; для собственных экранов достаточно репозитория над Room.
Доступ: клиенты вызывают contentResolver.query(uri, ...); методы провайдера выполняются на Binder-потоках, поэтому должны быть потокобезопасными.
Старт: провайдеры создаются до Application.onCreate, поэтому библиотеки используют их для самоинициализации.
Типичные случаи: контакты, медиа и календарь устроены как провайдеры; свой пишут для виджетов, поисковых подсказок или соседнего приложения.
**FileProvider**: готовый провайдер для обмена файлами. С Android 7 URI file:// в интенте приводит к FileUriExposedException. Объявите провайдер с exported="false", grantUriPermissions="true" и XML-списком папок, которыми можно делиться, затем превратите файл в URI через FileProvider.getUriForFile.
Выдача доступа: принимающее приложение получает доступ только через FLAG_GRANT_READ_URI_PERMISSION в том же интенте; без него оно падает с SecurityException. Доступ временный.
Стройте authority из applicationId: захардкоженный и скопированный из чужого кода конфликтует с другим приложением и блокирует установку.
Код трогает вью из фонового потока, а это CalledFromWrongThreadException: менять иерархию вью может только поток, который её создал, то есть главный. Главный поток крутит Looper, который обрабатывает MessageQueue, а Handler кладёт работу в эту очередь из любого потока.
Как устроен главный поток
**MessageQueue**: сообщения и Runnable, упорядоченные по времени.
**Looper**: Looper.loop() бесконечно берёт следующее сообщение и выполняет его. Один на поток, хранится в ThreadLocal, поэтому Looper.myLooper() его и находит. Главный берут через Looper.getMainLooper(); HandlerThread представляет собой фоновый поток со своим Looper. Бесконечный цикл не означает ANR: при пустой очереди поток спит в nativePollOnce, а ANR означает, что одно сообщение (например, ввод, 5 с) не обработано вовремя.
**Handler**: привязан к Looper; post() и sendMessage() ставят работу в очередь этого потока. view.post {}, runOnUiThread {} и Dispatchers.Main используют его под капотом.
Как исправить сегодня
lifecycleScope.launch и withContext(Dispatchers.IO) для вызова: результат возвращается на главный поток, а работа отменяется вместе с экраном. Голый Thread к тому же удерживает Activity и никогда не отменяется.
Падение нестабильное: проверка живёт в ViewRootImpl, поэтому запись до прикрепления вью проходит. Не доверяйте «один раз же сработало».
Отложенный Runnable никогда не отменяется. Главный Looper выполнит его и после остановки или закрытия Activity, поэтому уход со сплэша всё равно через две секунды открывает MainActivity, а после finish или поворота сообщение в очереди держит уничтоженную Activity в памяти, пока не сработает.
Почему: postDelayed кладёт Message в главную MessageQueue, а очередь ничего не знает о жизненном цикле. Лямбда захватывает this, поэтому цепочка очередь → сообщение → лямбда → Activity держит экземпляр. На Android 12+ Back на launcher-Activity лишь отправляет задачу в фон: Activity останавливается, но не уничтожается, поэтому destroy-колбэки не вызываются. Утечка мёртвого экземпляра относится к finish и повороту, где срабатывают и старый, и новый экземпляр, и в back stack оказываются два экрана MainActivity.
Исправление: планировать в onStart и вызывать removeCallbacks в зеркальном onStop. Это закрывает Back, поворот и Home: таймер перезапускается, когда пользователь возвращается, а не срабатывает в фоне.
Вариант на корутинах: repeatOnLifecycle(STARTED) { delay(2_000); … } даёт ту же пару старт/стоп. Обычный lifecycleScope.launch отменяется только при уничтожении Activity, а Back на корневой launcher-Activity его не вызывает.
Как поймать: lint такое не находит. LeakCanary сообщит об удержанной Activity при длинных задержках; Robolectric-тест может закрыть Activity, продвинуть время главного Looper за пределы задержки (например, через ShadowLooper) и проверить, что ни одна Activity не запущена.
С Android 12 собственную сплэш-Activity заменяет installSplashScreen() из core-splashscreen.
Компилятор Kotlin выдаёт JVM-байткод, D8 (или R8 в релизных сборках) превращает его в DEX-байткод, а исполняет его Android Runtime (ART). С Android 7 ART сочетает интерпретатор, JIT-компилятор и AOT-компиляцию по профилю в фоне.
DEX: компактный регистровый формат, все классы приложения лежат в файлах classes.dex. Один файл вмещает до 65 536 ссылок на методы; сверх этого сборка создаёт несколько файлов (multidex), что поддерживается нативно с Android 5, так что при minSdk 21+ настраивать ничего не нужно.
Dalvik (до Android 4.4): интерпретатор плюс JIT, поэтому код перекомпилировался при каждом запуске.
ART, Android 5–6: полная AOT-компиляция при установке через dex2oat: быстро работает, долго ставится, много места на диске.
ART, Android 7+: гибрид: новый код интерпретируется и компилируется JIT, горячие методы записываются в профиль, а dex2oat компилирует их заранее, пока устройство простаивает на зарядке.
Почему это важно: первые запуски после установки медленнее, пока профиль не скомпилирован. Облачные профили из Play и Baseline Profiles, поставляемые в приложении, закрывают этот разрыв. С Android 12 ART обновляется через системные обновления Play, так что исправления рантайма больше не ждут новой версии ОС.
Да: android:process у компонента запускает его в отдельном процессе со своей памятью, своим экземпляром Application и своими синглтонами. Процессы общаются через Binder, механизм IPC ядра, на котором работают каждый Intent, каждый вызов системного сервиса и каждый bound-сервис.
Несколько процессов в одном приложении
android:process=":remote" создаёт приватный процесс. Application.onCreate выполняется заново в каждом, поэтому инициализацию разделяют по имени процесса (Application.getProcessName(), API 28+).
Память не общая: синглтоны, кэши и инвалидация Room (ей нужен enableMultiInstanceInvalidation()) в каждом процессе свои. SharedPreferences небезопасны между процессами; у DataStore есть MultiProcessDataStoreFactory.
Зачем: изолировать нестабильный SDK или медиаплеер от UI либо получить отдельный бюджет памяти. Каждый процесс стоит времени старта и RAM.
Что даёт Binder
Одно копирование данных между процессами, идентичность вызывающего (Binder.getCallingUid()) для проверки разрешений и уведомления о смерти через linkToDeath.
Вызов блокирует вызывающего до возврата; вызываемая сторона выполняет его в пуле Binder-потоков (до ~16 потоков), а не на главном потоке, поэтому реализация должна быть потокобезопасной. oneway делает вызов асинхронным.
Буфер транзакций занимает около 1 МБ на процесс, отсюда и TransactionTooLargeException.
Способы использования, от простого к сложному
**Messenger**: очередь на основе Handler; вызовы обрабатываются по одному.
AIDL: вы описываете интерфейс, сборка генерирует Stub и Proxy; для типизированных параллельных вызовов.
Bound-сервис: bindService(); сервис возвращает IBinder из onBind, а клиент получает его в onServiceConnected. onServiceDisconnected вызывается, когда удалённый процесс умирает.
**ContentProvider**: когда вы делитесь именно данными с семантикой запросов.