Android主题切换引发内存泄漏问题求助(附LeakCanary报告)
主题切换触发MainActivity内存泄漏排查分析
问题背景
切换应用主题(调用AppCompatDelegate.setDefaultNightMode)后,LeakCanary检测到MainActivity内存泄漏,复现步骤:进入设置菜单点击主题切换按钮,执行主题变更操作后触发泄漏。
首次LeakCanary泄漏报告
┬─── │ GC Root: 原生代码中的全局变量 │ ├─ android.graphics.animation.RenderNodeAnimator 实例 │ 泄漏状态: 未知 │ 保留内存: 1.1 MB,涉及16344个对象 │ ↓ RenderNodeAnimator.mTarget │ ~~~~~~~ ├─ android.graphics.RenderNode 实例 │ 泄漏状态: 未知 │ 保留内存: 1.1 MB,涉及16274个对象 │ ↓ RenderNode.mHostView │ ~~~~~~~~~ ├─ com.google.android.material.switchmaterial.SwitchMaterial 实例 │ 泄漏状态: 是(View.mContext引用已销毁的Activity) │ 保留内存: 1.1 MB,涉及16272个对象 │ View未加入窗口视图层级 │ View.mAttachInfo 为null(View已分离) │ View.mID = R.id.null │ View.mWindowAttachCount = 1 │ mContext 是 com.flixeron.my_passkeeper.main.MainActivity 实例,且mDestroyed = true │ ↓ View.mContext ╰→ com.flixeron.my_passkeeper.main.MainActivity 实例 泄漏状态: 是(ObjectWatcher监测到该Activity已收到onDestroy回调,且mDestroyed = true) 保留内存: 251.2 kB,涉及5956个对象 key = 3c074833-b927-4971-b2b5-0a77b9e68bde watchDurationMillis = 5216 retainedDurationMillis = 215 mApplication 是 com.flixeron.my_passkeeper.main.MainApplication 实例 mBase 是 androidx.appcompat.view.ContextThemeWrapper 实例 元数据 Build.VERSION.SDK_INT: 31 Build.MANUFACTURER: Xiaomi LeakCanary版本: 2.7 应用进程名: com.flixeron.my_passkeeper 统计信息: LruCache[maxSize=3000,hits=6345,misses=185335,hitRate=3%] RandomAccess[bytes=9694284,reads=185335,travel=115234430872,range=38720254,size=46574102] 堆转储原因: 7个保留对象,应用处于可见状态 分析耗时: 15049 ms
SettingsFragment代码
class SettingsFragment : Fragment() { private val cardViewModel: CardViewModel by viewModel() private var bindingProp: FragmentSettingsBinding? = null private val binding get() = bindingProp!! override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { bindingProp = FragmentSettingsBinding.inflate(inflater, container, false) setTextLockTime() configureScreenShoots() themeSettings() // 切换深色/浅色模式 binding.switchTheme.setOnClickListener { if (!PreferenceProvider.darkMode) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) PreferenceProvider.darkMode = true binding.imageViewTheme.setImageResource(R.drawable.ic_dark_mode) } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) PreferenceProvider.darkMode = false binding.imageViewTheme.setImageResource(R.drawable.ic_light_mode) } } override fun onDestroyView() { super.onDestroyView() bindingProp = null } }
布局XML代码
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/switchTheme" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="30dp" app:layout_constraintBottom_toBottomOf="@+id/textViewChangeTheme" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/textViewChangeTheme"> <com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/switchTheme1" android:layout_width="0dp" android:layout_height="wrap_content" android:clickable="false" android:focusable="false" app:layout_constraintBottom_toBottomOf="@+id/switchTheme" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/switchTheme" android:scaleX="1.3" android:scaleY="1.3"/> </androidx.constraintlayout.widget.ConstraintLayout>
更新后的LeakCanary泄漏报告
┬─── │ GC Root: 系统类 │ ├─ android.view.inputmethod.InputMethodManager 类 │ 泄漏状态: 否(InputMethodManager↓未泄漏,类本身不会泄漏) │ ↓ static InputMethodManager.sInstance ├─ android.view.inputmethod.InputMethodManager 实例 │ 泄漏状态: 否(DecorView↓未泄漏,且InputMethodManager是单例) │ ↓ InputMethodManager.mCurRootView ├─ android.view.ViewRootImpl 实例 │ 泄漏状态: 否(DecorView↓未泄漏) │ mContext 是 com.android.internal.policy.DecorContext 实例,包装了Activity com.flixeron.my_passkeeper.main.MainActivity,且mDestroyed = false │ ViewRootImpl#mView 不为null │ mWindowAttributes.mTitle = "com.flixeron.my_passkeeper/com.flixeron.my_passkeeper.main.MainActivity" │ mWindowAttributes.type = 1 │ ↓ ViewRootImpl.mView ├─ com.android.internal.policy.DecorView 实例 │ 泄漏状态: 否(View已附加) │ View属于窗口视图层级 │ View.mAttachInfo 不为null(View已附加) │ View.mWindowAttachCount = 1 │ mContext 是 com.android.internal.policy.DecorContext 实例,包装了Activity com.flixeron.my_passkeeper.main.MainActivity,且mDestroyed = false │ ↓ View.mResources │ ~~~~~~~~~~ ├─ dev.b3nedikt.app_locale.AppLocaleResources 实例 │ 泄漏状态: 未知 │ 保留内存: 1.9 kB,涉及55个对象 │ context 是 androidx.appcompat.view.ContextThemeWrapper 实例 │ ↓ AppLocaleResources.context │ ~~~~~~~ ├─ androidx.appcompat.view.ContextThemeWrapper 实例 │ 泄漏状态: 未知 │ 保留内存: 274.9 kB,涉及6390个对象 │ mBase 是 io.github.inflationx.viewpump.ViewPumpContextWrapper 实例 │ ContextThemeWrapper未包装已知的Android上下文 │ ↓ ContextWrapper.mBase │ ~~~~~ ├─ io.github.inflationx.viewpump.ViewPumpContextWrapper 实例 │ 泄漏状态: 未知 │ 保留内存: 270.3 kB,涉及6242个对象 │ mBase 是 dev.b3nedikt.app_locale.AppLocaleContextWrapper 实例 │ ViewPumpContextWrapper未包装已知的Android上下文 │ ↓ ContextWrapper.mBase │ ~~~~~ ├─ dev.b3nedikt.app_locale.AppLocaleContextWrapper 实例 │ 泄漏状态: 未知 │ 保留内存: 270.2 kB,涉及6236个对象 │ mBase 是 android.app.ContextImpl 实例 │ AppLocaleContextWrapper未包装已知的Android上下文 │ ↓ ContextWrapper.mBase │ ~~~~~ ├─ android.app.ContextImpl 实例 │ 泄漏状态: 是(ContextImpl.mOuterContext是com.flixeron.my_passkeeper.main.MainActivity实例,且Activity.mDestroyed = true) │ 保留内存: 268.1 kB,涉及6175个对象 │ mAutofillClient 是 com.flixeron.my_passkeeper.main.MainActivity 实例,且mDestroyed = true │ mOuterContext 是 com.flixeron.my_passkeeper.main.MainActivity 实例,且mDestroyed = true │ ↓ ContextImpl.mAutofillClient ╰→ com.flixeron.my_passkeeper.main.MainActivity 实例 泄漏状态: 是(ObjectWatcher监测到该Activity已收到onDestroy回调,且mDestroyed = true) 保留内存: 264.2 kB,涉及6091个对象 key = 44a30066-31c8-4abb-8660-907035d18457 watchDurationMillis = 5229 retainedDurationMillis = 224 mApplication 是 com.flixeron.my_passkeeper.main.MainApplication 实例 mBase 是 androidx.appcompat.view.ContextThemeWrapper 实例 元数据 Build.VERSION.SDK_INT: 31 Build.MANUFACTURER: Xiaomi LeakCanary版本: 2.7 应用进程名: com.flixeron.my_passkeeper 已清理但仍保留的KeyedWeakReference实例数量: 1 统计信息: LruCache[maxSize=3000,hits=3959,misses=160418,hitRate=2%] RandomAccess[bytes=8403631,reads=160418,travel=86543130456,range=36387679,size=44147138] 堆转储原因: 6个保留对象,应用处于可见状态 分析耗时: 11869 ms
排查建议
针对首次报告的SwitchMaterial泄漏
- 动画残留问题:SwitchMaterial被
RenderNodeAnimator持有,说明主题切换时触发的视图属性动画(如缩放动画)未及时取消,导致View被原生动画框架强引用,进而关联已销毁的Activity。解决方案:在Fragment的onDestroyView中调用binding.switchTheme1.clearAnimation(),手动清理所有关联动画。 - 视图引用清理:虽然Fragment在
onDestroyView中置空了bindingProp,但动画框架可能仍持有View引用。可尝试在主题切换后延迟清理视图引用,或使用弱引用持有视图对象。
针对更新报告的ContextWrapper链泄漏
- 第三方库上下文问题:泄漏链涉及
AppLocaleResources、ViewPumpContextWrapper等第三方库的上下文包装类,可能是库版本存在泄漏bug,或未在Activity销毁时正确释放上下文引用。解决方案:升级app_locale和ViewPump到最新稳定版;在MainActivity的onDestroy中调用库的销毁/重置方法(若提供)。 - Autofill客户端引用:
ContextImpl.mAutofillClient持有已销毁的Activity,可能是Autofill服务未及时释放引用。解决方案:在MainActivity的onDestroy中调用setAutofillClient(null)(API 26+),或临时禁用Autofill功能验证泄漏是否消失。
通用排查步骤
- Activity重建引用检查:调用
AppCompatDelegate.setDefaultNightMode会触发Activity重建,需确保所有Fragment、ViewModel未持有旧Activity的强引用,使用requireContext()/requireActivity()时避免长期持有。 - 堆转储手动分析:使用Android Studio Profiler捕获堆转储,搜索MainActivity实例,查看完整引用链,确认是否存在LeakCanary未检测到的隐藏引用。
内容的提问来源于stack exchange,提问作者Milos
相关产品推荐
相关产品推荐

