Site icon AppTractor

Топ-7 утечек памяти Android и как их избежать

Утечки памяти — тихие убийцы производительности приложений. Даже в 2025 году, когда Jetpack Compose доминирует в разработке пользовательских интерфейсов и появляются новые модные инструменты, эти ошибки все еще преследуют разработчиков. Давайте разберем 7 наиболее распространенных виновников и вооружим вас проверенными в бою решениями.

Статический контекст: классическая ловушка

Статические ссылки на Activity или Context подобны неживым призракам — они никогда не умирают, блокируя сборку мусора. Часто встречаются в синглтонах или глобальных утилитах.

Всегда инжектируйте контекст приложения (никогда не активити):

object AuthManager {
    private lateinit var appContext: Context
    
    // Initialize in your Application class
    fun init(application: Application) {
        appContext = application
    }
    
    fun getUser(): User? {
        // Use appContext here
    }
}

// Try to fix using DI like

@Module
@InstallIn(SingletonComponent::class)
class AppModule {
    @Provides
    fun provideContext(@ApplicationContext context: Context): Context = context
}

Внутренние классы и лямбды: молчаливые хранители

Нестатические внутренние классы (например, обработчики) или лямбды Compose скрытно хранят родительские ссылки. Ваша активити остается в памяти после поворота экрана? Вот почему:

// For legacy code
// Static inner class + WeakReference
class MyActivity : AppCompatActivity() {
    private class SafeHandler(
        activity: MyActivity
    ) : Handler(Looper.getMainLooper()) {
        private val activityRef = WeakReference(activity)
        
        override fun handleMessage(msg: Message) {
            activityRef.get()?.updateUI()
        }
    }
}

// For compose code
@Composable
fun LeakProofButton(onClick: () -> Unit) {
    // Captures latest onClick reference
    val currentOnClick by rememberUpdatedState(onClick)
    
    Button(onClick = { currentOnClick() }) {
        Text("I'm Safe!")
    }
}

Зомби потоки и корутины

Фоновые задачи сохраняются после уничтожения активити/фрагмента. Часто встречаются в disposables RxJava или GlobalScope.

Корутины: привяжитесь к жизненному циклу с помощью lifecycleScope:

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        lifecycleScope.launch {
            // Auto-cancels when fragment dies
            fetchData()
        }
    }
}

Забытые слушатели: фантомные наблюдатели

Незарегистрированные наблюдатели LiveData, broadcast ресиверы или слушатели кликов остаются, как неприятные запахи:

@Composable
fun SensorObserver() {
    val sensorManager = remember { getSystemService(SENSOR_SERVICE) as SensorManager }
    
    DisposableEffect(Unit) {
        val listener = SensorEventListener { /* ... */ }
        sensorManager.registerListener(listener, ...)
        
        onDispose {
            sensorManager.unregisterListener(listener) // Clean exit!
        }
    }
}

Спагетти из синглтонов

Синглтоны случайно удерживают ссылки на UI. Видели такую ошибку? MySingleton → MyActivity → MySingleton (циклическая ссылка!).

@Singleton
class AnalyticsService @Inject constructor(
    @ApplicationContext private val context: Context
) { ... }

View вампиры: UI-компоненты, которые не хотят умирать

Кастомные представления или ComposeView, сохраняющие ссылки на Activity после отсоединения:

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    myView.setOnClickListener(null) // Cut the ties!
}

// Compose 
ComposeView(context).apply {
    setViewCompositionStrategy(
        ViewCompositionStrategy.DisposeOnDetachedFromWindow
    )
    setContent { /* ... */ }
}

Ошибки с растровыми изображениями и монстры состояний

Не отпущенные растровые изображения или массивные состояния Compose, вызывающие OutOfMemoryError:

AsyncImage(
    model = "https://example.com/huge_image.jpg",
    contentDescription = null,
    modifier = Modifier.fillMaxWidth()
)

val heavyList by remember { mutableStateOf(List(10_000) { ... }) } // 

// Better: Use lazy lists or derivedStateOf
val visibleItems by remember {
    derivedStateOf { heavyList.filter { it.isVisible } }
}

Ваш набор инструментов для борьбы с утечками 2025

Источник

Exit mobile version