惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
Jina AI
Jina AI
大猫的无限游戏
大猫的无限游戏
Martin Fowler
Martin Fowler
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
Securelist
S
Security Affairs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LINUX DO - 热门话题
博客园 - 三生石上(FineUI控件)
T
Threatpost
T
The Blog of Author Tim Ferriss
C
CERT Recently Published Vulnerability Notes
IT之家
IT之家
P
Palo Alto Networks Blog
Microsoft Azure Blog
Microsoft Azure Blog
Spread Privacy
Spread Privacy
Cyberwarzone
Cyberwarzone
腾讯CDC
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CXSECURITY Database RSS Feed - CXSecurity.com
GbyAI
GbyAI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
I
Intezer
T
Tor Project blog
AWS News Blog
AWS News Blog
T
Tenable Blog
NISL@THU
NISL@THU
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
MyScale Blog
MyScale Blog
D
DataBreaches.Net
Microsoft Security Blog
Microsoft Security Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
量子位
美团技术团队
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
罗磊的独立博客
The GitHub Blog
The GitHub Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Пять ошибок начинающих Android‑разработчиков при работе с жизненным циклом
artem · 2026-06-23 · via Все публикации подряд на Хабре

Простой

10 мин

8

Привет, Хабр! Android устроен иначе, чем большинство платформ: операционная система может в любой момент уничтожить Activity или Fragment и пересоздать их заново, причём по самым разным причинам (поворот экрана, нехватка памяти, смена темы, языка, dark mode).

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

Разберём пять типичных ошибок, на которых сыпется почти каждый первый Android‑проект, и покажем, какие инструменты появились в современном Android‑стеке для борьбы с этой болью.

Утечка Activity через нестатический внутренний класс или захват в лямбде

Простой сценарий: на экране есть кнопка, по которой через пять секунд должен показаться Toast. Логика выглядит даже безобидно:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        findViewById<Button>(R.id.button).setOnClickListener {
            Handler(Looper.getMainLooper()).postDelayed({
                Toast.makeText(this, "Hello!", Toast.LENGTH_SHORT).show()
            }, 5000)
        }
    }
}

Пользователь нажал кнопку, в течение этих пяти секунд повернул экран. Android уничтожает старую Activity, создаёт новую. Старая Activity физически продолжает существовать в памяти, потому что её удерживает Handler, который держит лямбду, которая через this ссылается на старую Activity. Если за пять секунд пользователь повернул экран десять раз, в памяти живёт одиннадцать копий Activity, и сборщик мусора их не уберёт.

Это утечка памяти. На одной Activity она почти незаметна, на сложных экранах с большим количеством view и подгруженными изображениями каждая утечка стоит десятки мегабайт. После часа использования приложение получает OutOfMemoryError и крашится.

Решение: понять, что Handler с долгим delayed‑task должен быть отменён в onDestroy, либо использовать lifecycleScope, который сам отменяет работу при уничтожении компонента:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        findViewById<Button>(R.id.button).setOnClickListener {
            lifecycleScope.launch {
                delay(5000)
                Toast.makeText(this@MainActivity, "Hello!", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

lifecycleScope это специальный CoroutineScope, привязанный к жизненному циклу Activity (или Fragment). Когда Activity уничтожается, все корутины внутри lifecycleScope автоматически отменяются, ссылок на Activity больше нет, утечка не происходит.

Та же логика работает для AsyncTask (хотя в 2026 году им уже почти никто не пользуется), для Thread, для подписок на RxJava, для любых асинхронных операций. Правило простое: если что‑то долгоживущее держит ссылку на Activity, нужно явно эту ссылку освободить при уничтожении или использовать scope, который сам это сделает.

Утечка через статический Context

Распространённая ошибка при написании первого Singleton:

object NetworkManager {
    private lateinit var context: Context
    
    fun init(ctx: Context) {
        context = ctx
    }
    
    fun makeRequest(url: String) {
        // используем context для чего-нибудь
    }
}

// В Activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        NetworkManager.init(this)
    }
}

Singleton живёт всё время работы процесса. Если в Singleton сохраняется this от Activity, эта Activity никогда не будет собрана сборщиком мусора, потому что Singleton держит на неё ссылку. После пары поворотов экрана в памяти живут все старые Activity, и каждая тянет за собой layout, view, загруженные ресурсы.

Решение: для долгоживущих сервисов всегда передавать Application Context, который существует ровно один на весь процесс и не зависит от Activity:

object NetworkManager {
    private lateinit var context: Context
    
    fun init(applicationContext: Context) {
        context = applicationContext.applicationContext
    }
}

// В Activity
NetworkManager.init(applicationContext)

Метод applicationContext возвращает Context, привязанный к Application, а не к Activity. Application существует столько же, сколько процесс приложения, поэтому хранить его в Singleton безопасно. Activity получает свой контекст через this, но для долгоживущих сервисов это не подходит.

Полезное правило: если объект живёт дольше одной Activity, ему нужен Application Context. Если объект используется внутри одной Activity для UI‑операций (показать диалог, запустить другой Activity), нужен Activity Context. Перепутать их просто: с Application Context невозможно показать диалог, а с Activity Context в долгоживущем сервисе течёт память.

Запуск работы в onCreate без её отмены при уничтожении

Сценарий загрузки данных с сети при открытии экрана:

class ProductListActivity : AppCompatActivity() {
    private lateinit var adapter: ProductAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_product_list)
        adapter = ProductAdapter()
        findViewById<RecyclerView>(R.id.list).adapter = adapter
        
        GlobalScope.launch {
            val products = productRepository.fetchAll()
            withContext(Dispatchers.Main) {
                adapter.submitList(products)
            }
        }
    }
}

GlobalScope запускает корутину, которая живёт, пока работает процесс. Если пользователь покинул экран до завершения сетевого запроса, корутина продолжает выполняться, и в момент возврата на главный поток обращается к adapter, который привязан к уничтоженной Activity.

В лучшем случае это утечка памяти (Activity не собирается, потому что её держит корутина), в худшем — IllegalStateException, если внутри adapter обращается к view, которые уже null.

Это та же проблема, что и в первой ошибке, только в другой обёртке. Решение похожее: использовать lifecycleScope для работы, привязанной к экрану, или viewModelScope для работы, которая должна пережить поворот экрана, но завершиться при уходе пользователя с экрана:

class ProductListViewModel : ViewModel() {
    private val _products = MutableStateFlow<List<Product>>(emptyList())
    val products: StateFlow<List<Product>> = _products.asStateFlow()
    
    init {
        viewModelScope.launch {
            _products.value = productRepository.fetchAll()
        }
    }
}

class ProductListActivity : AppCompatActivity() {
    private val viewModel: ProductListViewModel by viewModels()
    private lateinit var adapter: ProductAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_product_list)
        adapter = ProductAdapter()
        findViewById<RecyclerView>(R.id.list).adapter = adapter
        
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.products.collect { products ->
                    adapter.submitList(products)
                }
            }
        }
    }
}

Здесь работа разделена на два слоя. ViewModel и её viewModelScope живут поверх поворотов экрана: запрос на сервер выполняется один раз, при повороте экрана новая Activity получает ту же самую ViewModel и не делает повторный запрос. lifecycleScope в Activity подписывается на поток данных, но активна подписка только когда Activity в состоянии STARTED или RESUMED (через repeatOnLifecycle). Когда Activity уходит в STOPPED, подписка автоматически отменяется, при возвращении на экран подключается заново.

GlobalScope в коде почти всегда как ошибка. Эта идиома существует для очень специфических случаев (фоновые задачи, которые правда должны жить столько же, сколько процесс), но для обычной работы с данными экрана её использовать нельзя.

Потеря данных при повороте экрана

Простой пример формы:

class FeedbackActivity : AppCompatActivity() {
    private lateinit var editText: EditText
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_feedback)
        editText = findViewById(R.id.feedback_input)
    }
}

Пользователь набрал длинный текст, повернул экран. Android уничтожил Activity, создал заново, текст пропал. Точнее, текст в EditText сохраняется автоматически системой, потому что у EditText есть android:id и встроенный механизм сохранения View state. Но если бы это была не EditText, а кастомное состояние (например, текущий шаг wizard‑формы, выбранный таб, состояние загрузки), оно бы пропало.

Решение через onSaveInstanceState:

class WizardActivity : AppCompatActivity() {
    private var currentStep = 0
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_wizard)
        
        currentStep = savedInstanceState?.getInt(KEY_CURRENT_STEP) ?: 0
        showStep(currentStep)
    }
    
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(KEY_CURRENT_STEP, currentStep)
    }
    
    companion object {
        private const val KEY_CURRENT_STEP = "current_step"
    }
}

onSaveInstanceState вызывается системой перед уничтожением Activity. Туда складываются примитивы и Parcelable‑объекты, которые потом восстанавливаются в onCreate или onRestoreInstanceState. Bundle ограничен размером (около 1 MB на API 28+, меньше на старых версиях), поэтому большие данные туда класть нельзя.

Современный подход для серьёзных приложений: ViewModel плюс SavedStateHandle. ViewModel переживает повороты экрана сама по себе, без участия пользователя:

class WizardViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    var currentStep: Int by savedStateHandle.saveable(initialValue = 0)
}

SavedStateHandle сохраняет данные не только при повороте, но и при уничтожении процесса операционной системой (например, когда приложение в фоне и система освобождает память). При возвращении в приложение через час пользователь оказывается на том же шаге, на котором ушёл.

Эта связка ViewModel плюс SavedStateHandle сейчас идиоматичный подход. Для совсем простых случаев onSaveInstanceState достаточно, для сложных экранов с состоянием лучше сразу выносить логику в ViewModel.

Доступ к View после onDestroyView в Fragment

Самая частая ошибка с Fragment. Сценарий:

class ProductFragment : Fragment(R.layout.fragment_product) {
    private var _binding: FragmentProductBinding? = null
    private val binding get() = _binding!!
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentProductBinding.bind(view)
        
        lifecycleScope.launch {
            val product = productRepository.fetch()
            binding.titleTextView.text = product.title
        }
    }
}

Пользователь открыл экран, fragment запустил загрузку, пользователь сразу нажал назад. Fragment проходит через onDestroyView, ViewBinding нужно освободить, но в коде про это забыли. Когда корутина завершается, обращается к binding.titleTextView и получает либо null pointer (если ViewBinding обнулили), либо IllegalStateException (если View уже detached).

В Fragment есть две сущности lifecycle: lifecycle самого Fragment и lifecycle его View. Когда фрагмент находится в backstack и не виден, его View уничтожается, но сам Fragment живёт. ViewModel и lifecycleScope Fragment остаются активными, хотя View уже нет. Это и есть источник проблемы.

Решение: использовать viewLifecycleOwner.lifecycleScope для работы, связанной с View, и обнулять binding в onDestroyView:

class ProductFragment : Fragment(R.layout.fragment_product) {
    private var _binding: FragmentProductBinding? = null
    private val binding get() = _binding!!
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentProductBinding.bind(view)
        
        viewLifecycleOwner.lifecycleScope.launch {
            val product = productRepository.fetch()
            binding.titleTextView.text = product.title
        }
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

viewLifecycleOwner.lifecycleScope отменяется при уничтожении View, а не Fragment. Корутина не дойдёт до обращения к binding, потому что её отменят в onDestroyView. Обнуление _binding нужно, чтобы Fragment, лежащий в backstack, не держал ссылку на старый View и не мешал сборщику мусора.

Для большего удобства принята идиома property delegate, который автоматизирует освобождение binding:

class FragmentViewBindingDelegate<T : ViewBinding>(
    private val fragment: Fragment,
    private val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {

    private var binding: T? = null

    init {
        fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
            viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
                override fun onDestroy(owner: LifecycleOwner) {
                    binding = null
                }
            })
        }
    }

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        return binding ?: viewBindingFactory(thisRef.requireView()).also { binding = it }
    }
}

fun <T : ViewBinding> Fragment.viewBinding(factory: (View) -> T) =
    FragmentViewBindingDelegate(this, factory)

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

class ProductFragment : Fragment(R.layout.fragment_product) {
    private val binding by viewBinding(FragmentProductBinding::bind)
}

Никакого ручного освобождения и никакого _binding со знаком вопроса. Delegate сам подписывается на lifecycle View и обнуляет ссылку. Эта идиома используется почти во всех современных Android‑проектах.


Все пять ошибок выше связаны с непониманием жизненного цикла компонентов: что когда создаётся, что когда уничтожается, кто кого может держать.

В debug‑сборках обязательно стоит подключить LeakCanary: библиотека автоматически находит утечки и показывает цепочку объектов, которая держит Activity или Fragment в памяти. Подключается за пять минут, окупается на первой же находке.

Хотите глубже разобраться, как Android‑приложение живёт после запуска — от архитектуры экрана до асинхронных операций?

Присмотритесь к открытым урокам:

  • 2 июля в 20:00 — «От API до экрана: создаём Android‑приложение на рекомендуемой архитектуре». Записаться
    Покажем, как связать серверное API, сетевой слой и экран приложения в единую рабочую архитектуру.

  • 20 июля в 20:00 — «Асинхронное программирование в Android». Записаться
    Объясним, зачем Android‑приложению несколько потоков, как тяжёлые операции влияют на отклик интерфейса и как с этим работать.

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