Fragment中协程生命周期作用域下无限循环导致内存泄漏问题
应用性能下降与内存泄漏问题排查与优化请求
描述
应用运行时间越长性能越差,怀疑是内存泄漏导致。
场景
- 主屏幕包含两个Fragment:一个用无限循环模拟车辆速度,另一个提供多功能
- 左侧Fragment始终处于打开状态,运行在主线程
- 右侧通过导航图加载另外6个Fragment
问题
应用长时间运行后整体性能逐渐下降,担心左侧Fragment的无限循环是诱因,可能引发内存泄漏。
请求
需要以下方面的建议:
- Fragment中处理无限循环,避免性能下降和内存泄漏的方案
- 主线程性能优化技巧,尤其是一侧Fragment始终保持活跃的场景
- 使用导航图加载多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
相关产品推荐
相关产品推荐

