Утечки памяти и исправления
Находите и устраняйте утечки
«Утечки памяти и исправления» — бесплатный урок Android Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 4 уроков всего.
Что такое утечка памяти
Утечка памяти происходит, когда больше не нужные объекты нельзя удалить сборщиком мусора, потому что что-то всё ещё хранит ссылку на них. В Android классическими жертвами становятся Activity или Context, которые сохраняются после уничтожения их экрана.
Утечки со временем увеличивают кучу, вызывают более частые сборки мусора (они приостанавливают интерфейс и приводят к подёргиваниям), а в конечном итоге приводят к аварийному завершению приложения с ошибкой OutOfMemoryError. В этом уроке показано, как находить и устранять утечки.
Как сборщик мусора решает, что сохранить
Сборщик мусора Android сохраняет любой объект, достижимый от корня сборки мусора (рабочих потоков, статических полей и т. д.). Всё недостижимое освобождается.
Утечка — это просто нежелательный путь достижимости: долгоживущий объект хранит ссылку на короткоживущий. Пример ниже на чистом Kotlin показывает, как достижимость не даёт объекту быть удалённым.
object GlobalCache {
val items = mutableListOf<ByteArray>()
}
fun cacheSomething() {
// This 1MB array stays alive forever because GlobalCache
// (a static singleton, a GC root) keeps referencing it.
GlobalCache.items.add(ByteArray(1_000_000))
}
fun main() {
repeat(3) { cacheSomething() }
println("Held arrays: ${GlobalCache.items.size}") // never freed
}Классика: утечка Activity
Самая распространённая утечка в Android возникает, когда статический или долгоживущий объект хранит Context. Когда Activity уничтожается (при повороте экрана или переходе на другой экран), система хочет освободить её, но статическая ссылка сохраняет её навсегда.
Приведённый ниже код создаёт утечку: singleton кэширует контекст Activity.
// LEAK: static field holds an Activity Context
object Analytics {
var context: Context? = null // <- keeps Activity alive
fun init(ctx: Context) { context = ctx }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.init(this) // passing the Activity -> leak on rotation
}
}Исправление: используйте контекст приложения
Если Вам необходимо хранить Context в долгоживущем объекте, храните контекст приложения. Он существует на протяжении всего процесса, поэтому его безопасно сохранять.
Храните контекст Activity или View только пока существует соответствующий экран. Это простое правило предотвращает большинство утечек в Android.
object Analytics {
private var appContext: Context? = null
fun init(ctx: Context) {
// applicationContext is process-scoped and safe to keep
appContext = ctx.applicationContext
}
}
// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the ActivityВнутренние классы и Handler
Нестатический внутренний класс (включая большинство слушателей, Runnable и обратных вызовов Handler) неявно содержит ссылку на внешний класс. Если такая работа продолжается дольше экрана, она создаёт утечку этого экрана.
Частая причина утечек — отложенный вызов Handler.postDelayed: ожидающее сообщение сохраняет Activity, пока не будет выполнено.
// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)
// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}Корутин: ограничивайте область работы
Корутин, который продолжает работать после закрытия экрана, создаёт утечки для всего, что он захватил. Решение — структурированная конкурентность: запускайте работу в области, связанной с жизненным циклом, чтобы она автоматически отменялась.
В ViewModel используйте viewModelScope, а в контроллере пользовательского интерфейса — lifecycleScope. Когда владелец уничтожается, область отменяется, а ссылки освобождаются.
class FeedViewModel : ViewModel() {
fun load() {
// Cancelled automatically when the ViewModel is cleared
viewModelScope.launch {
val feed = repository.fetchFeed()
_state.value = feed
}
}
}
// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()DisposableEffect в Compose
В Compose всё, что Вы регистрируете, необходимо отменить, когда компонуемый элемент покидает интерфейс. DisposableEffect предоставляет блок onDispose именно для этого: для слушателей, наблюдателей, датчиков и приёмников широковещательных сообщений.
Если забыть об очистке, это приведёт к утечке обратного вызова и всего, что он захватывает.
@Composable
fun LocationDisplay(manager: LocationManager) {
DisposableEffect(manager) {
val listener = LocationListener { /* update */ }
manager.register(listener)
onDispose { manager.unregister(listener) } // prevents the leak
}
}Поиск утечек с помощью LeakCanary
LeakCanary — стандартный инструмент для автоматического обнаружения утечек в отладочных сборках. Добавьте зависимость, и он будет отслеживать уничтоженные объекты. Если объект не был удалён сборщиком мусора, инструмент создаст дамп кучи и покажет точную цепочку ссылок, из-за которой объект сохраняется.
Эта цепочка указывает, что нужно исправить: она ведёт прямо к проблемному полю или слушателю.
// build.gradle.kts (app module)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
// Analytics.context -> MainActivity (leaked)Дампы кучи в профилировщике
Для утечек, которые не обнаруживает LeakCanary (медленный рост, утечки в нативном коде и большие кэши), используйте профилировщик памяти. Создайте дамп кучи, принудительно запустите сборку мусора и проверьте, у каких классов неожиданно много экземпляров.
Растущее количество экземпляров Activity, Fragment или ViewModel после ухода с экрана — явный признак утечки. Профилировщик также показывает путь к корням сборки мусора, чтобы Вы могли отследить ссылку.
Битмапы и большие выделения памяти
Не каждая проблема с памятью является утечкой ссылок. Большие битмапы и неограниченные кэши могут сами по себе переполнить кучу. Фотография в полном разрешении может занимать в памяти десятки мегабайт.
Попросите библиотеку изображений (Coil/Glide) уменьшать изображение до отображаемого размера, а для любого создаваемого кэша задайте явное максимальное ограничение. В примере показан LRU-кэш с ограниченным размером.
// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.Контрольный список для предотвращения утечек
Выработайте эти привычки, и большинство утечек не возникнет:
- Храните applicationContext в долгоживущих объектах, но никогда не храните Activity или View.
- Запускайте асинхронную работу в корутине, ограниченной жизненным циклом (
viewModelScope,lifecycleScope). - Всегда отменяйте регистрацию слушателей и приёмников (
onDispose,onDestroy). - Отменяйте отложенные вызовы Handler и таймеры.
- Ограничивайте размер кэшей и уменьшайте размер битмапов.
- Оставляйте LeakCanary в отладочных сборках и реагируйте на его трассировки.
Быстрая проверка
Вам нужно, чтобы singleton-объект хранил Context на протяжении всего времени своего существования. Какой контекст он должен хранить, чтобы не создавать утечку экрана?
Итоги: содержите память в порядке
Вы узнали, что такое утечки и как их предотвращать:
- Утечки — это нежелательные пути достижимости, из-за которых уже неиспользуемые объекты продолжают существовать.
- Классическая утечка — статическая или долгоживущая ссылка на Activity/Context. Вместо неё храните applicationContext.
- Слушатели во внутренних классах, отложенные вызовы Handler и корутины без области создают утечку своего владельца — ограничивайте их область и отменяйте.
- В Compose используйте
DisposableEffect, чтобы отменять регистрацию обратных вызовов. - LeakCanary и профилировщик памяти показывают цепочку ссылок.
- Ограничивайте размер кэшей и уменьшайте размер битмапов.
Далее: ускоряем запуск приложения с помощью оптимизации запуска и базовых профилей.
Часто задаваемые вопросы
Урок «Утечки памяти и исправления» бесплатный?
Да — полный текст урока «Утечки памяти и исправления» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Android Academy, подпишись на CoddyKit PRO. Курс Android Academy содержит 4 уроков всего.
Чему я научусь в уроке «Утечки памяти и исправления»?
Находите и устраняйте утечки Ты практикуешь Android Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Android Academy?
Предыдущий опыт не требуется. Android Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Утечки памяти и исправления»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Android Academy?
Да. Каждый урок Android Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Измерение производительности
- Управление рекомпозицией
- Утечки памяти и исправления
- Профили запуска и Baseline Profiles