You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fragment中协程生命周期作用域下无限循环导致内存泄漏问题

应用性能下降与内存泄漏问题排查与优化请求

描述

应用运行时间越长性能越差,怀疑是内存泄漏导致。

场景

  • 主屏幕包含两个Fragment:一个用无限循环模拟车辆速度,另一个提供多功能
  • 左侧Fragment始终处于打开状态,运行在主线程
  • 右侧通过导航图加载另外6个Fragment

问题

应用长时间运行后整体性能逐渐下降,担心左侧Fragment的无限循环是诱因,可能引发内存泄漏。

请求

需要以下方面的建议:

  1. Fragment中处理无限循环,避免性能下降和内存泄漏的方案
  2. 主线程性能优化技巧,尤其是一侧Fragment始终保持活跃的场景
  3. 使用导航图加载多Fragment时,内存高效利用与防泄漏策略

XML布局代码

<?xml version="1.0" encoding="utf-8"?>
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/constraintMain"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/frameLayout"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="5dp"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/rvBottomMenu"
        app:layout_constraintEnd_toStartOf="@+id/guidelineCenter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_speed" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelineCenter"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toTopOf="@+id/rvBottomMenu"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintGuide_begin="@dimen/guide_begin_main"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btnExpand"
        android:layout_width="60dp"
        android:layout_height="150dp"
        android:layout_marginStart="16dp"
        android:background="@drawable/bg_btn_expand_shrink"
        app:layout_constraintTop_toBottomOf="@+id/frameLayout"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btnMic"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginEnd="16dp"
        android:background="@drawable/ic_mic"
        app:layout_constraintTop_toBottomOf="@+id/frameLayout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clViewPager"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:padding="5dp"
        android:background="@drawable/bg_border_main"
        android:layout_marginStart="5dp"
        android:layout_marginEnd="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/guidelineCenter"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/rvBottomMenu">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/nav_host_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_fragments" />
    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvBottomMenu"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:background="@color/black"
        app:itemIconSize="@dimen/tab_icon_size"
        app:tabIndicatorColor="#EF0C4C"
        app:tabIndicatorHeight="0dp"
        app:tabIndicatorFullWidth="true"
        android:orientation="horizontal"
        tools:listitem="@layout/items_bottom_menu"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Kotlin速度模拟代码

@SuppressLint("SetTextI18n", "UseCompatLoadingForDrawables")
private suspend fun readSpeed(){
    var speedValue = 1
    while (true){
        speedValue += 1
        binding.tvSpeed.text = ""
        binding.tvSpeed.text = speedValue.toString()
        delay(200)
        if (speedValue == 140){
            speedValue = 1
        }
        if (speedValue <= 45){
            val newDrawable = getApplicationContext().resources.getDrawable(R.drawable.green_bg, null)
            val transitionDrawable = TransitionDrawable(arrayOf(binding.clSpeedometerMain.background, newDrawable))
            binding.clSpeedometerMain.background = transitionDrawable
            transitionDrawable.startTransition(3000)
        }else if(speedValue in 46 .. 80){
            val newDrawable = getApplicationContext().resources.getDrawable(R.drawable.city_bg, null)
            val transitionDrawable = TransitionDrawable(arrayOf(binding.clSpeedometerMain.background, newDrawable))
            binding.clSpeedometerMain.background = transitionDrawable
            transitionDrawable.startTransition(3000)
        }else {
            val newDrawable = getApplicationContext().resources.getDrawable(R.drawable.red_bg, null)
            val transitionDrawable = TransitionDrawable(arrayOf(binding.clSpeedometerMain.background, newDrawable))
            binding.clSpeedometerMain.background = transitionDrawable
            transitionDrawable.startTransition(3000)
        }
    }
}

调用逻辑

private fun getSpeed() {
    this.lifecycleScope.launch(Main) {
        readSpeed()
    }
}

override fun onResume() {
    super.onResume()
    getSpeed()
}

解决方案与优化建议

1. 修复无限循环的性能与泄漏问题

当前readSpeed方法存在两个核心问题:

  • 重复启动循环:每次onResume都会启动新协程,旧协程不会自动停止,导致多个无限循环同时占用主线程资源
  • 资源重复创建:每次循环都新建TransitionDrawable和Drawable,未回收旧资源,引发内存堆积

优化方案:

  • 用lifecycleScope.launchWhenResumed替代手动在onResume启动,协程会在Fragment暂停时自动挂起,恢复时继续,避免重复启动
  • 缓存不同状态的Drawable,避免重复创建
  • 用while(isActive)替代while(true),协程取消时自动退出循环,确保Fragment销毁时协程终止

优化后的代码示例:

// 初始化时缓存Drawable,避免重复创建
private val greenBg by lazy { requireContext().getDrawable(R.drawable.green_bg) }
private val cityBg by lazy { requireContext().getDrawable(R.drawable.city_bg) }
private val redBg by lazy { requireContext().getDrawable(R.drawable.red_bg) }

@SuppressLint("SetTextI18n")
private suspend fun readSpeed(){
    var speedValue = 1
    // 协程活跃时才继续循环,销毁时自动退出
    while (isActive){
        speedValue += 1
        // 直接设置文本,去掉多余的空文本赋值操作
        binding.tvSpeed.text = speedValue.toString()
        delay(200)
        
        if (speedValue == 140){
            speedValue = 1
        }
        
        // 仅当背景状态变化时才切换,避免重复创建TransitionDrawable
        val currentBg = binding.clSpeedometerMain.background
        val targetBg = when(speedValue){
            in 1..45 -> greenBg
            in 46..80 -> cityBg
            else -> redBg
        }
        
        if (currentBg !is TransitionDrawable || currentBg.findDrawableByLayerId(1) != targetBg){
            val transitionDrawable = TransitionDrawable(arrayOf(currentBg, targetBg))
            binding.clSpeedometerMain.background = transitionDrawable
            transitionDrawable.startTransition(3000)
        }
    }
}

// 无需手动在onResume调用,launchWhenResumed会自动感知生命周期
private fun startSpeedSimulation() {
    lifecycleScope.launchWhenResumed {
        readSpeed()
    }
}

2. 主线程性能优化

  • 减少冗余UI操作:删除binding.tvSpeed.text = ""这类无意义的代码,减少UI绘制次数
  • 异步处理非UI逻辑:把速度计算、资源预加载等操作移到IO线程,仅在更新UI时切回主线程
  • 同步屏幕刷新:如果需要更精准的UI更新时机,可借助Choreographer同步到屏幕刷新周期,避免丢帧
  • 规范资源加载:用ContextCompat.getDrawable替代resources.getDrawable,并尽量在后台线程预加载Drawable资源

3. 导航图与多Fragment内存优化

  • 清理回退栈:在导航图中给右侧Fragment添加android:popUpTo和android:popUpToInclusive="true",确保回退栈中只保留必要的Fragment,避免内存堆积
  • 用ViewModel共享状态:避免在Fragment间传递大量数据,用ViewModel存储共享状态,减少Fragment持有不必要的引用
  • 及时释放资源:在Fragment的onDestroyView中清空ViewBinding引用;如果使用了监听器、回调,在onDestroy中取消注册
  • 禁用保留实例:除非必要,不要设置setRetainInstance(true),避免Fragment长期持有Context引用

内容的提问来源于stack exchange,提问作者Tousif Akram

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 05:14:52