基于LeakCanary检测结果修复内存泄漏解决App崩溃问题
修复ExoPlayer音频应用中的内存泄漏问题
问题背景
在Android Studio中对使用ExoPlayer播放MP3的应用进行性能分析时,发现应用频繁崩溃,通过LeakCanary检测到内存泄漏,以下是检测报告及修复方案。
LeakCanary检测日志
┬─── │ GC根节点:Native代码中的输入或输出参数 │ ├─ java.util.TaskQueue实例 │ 泄漏状态:未知 │ 保留528 B,涉及2个对象 │ ↓ TaskQueue.queue │ ~~~~~ ├─ java.util.TimerTask[]数组 │ 泄漏状态:未知 │ 保留512 B,涉及1个对象 │ ↓ TimerTask[0] │ ~~~ ├─ com.github.islamkhsh.CardSliderViewPager$SlidingTask实例 │ 泄漏状态:未知 │ 保留2.0 MB,涉及13670个对象 │ ↓ CardSliderViewPager$SlidingTask.this$0 │ ~~~~~~ ├─ com.github.islamkhsh.CardSliderViewPager实例 │ 泄漏状态:未知 │ 保留2.0 MB,涉及13668个对象 │ View未属于窗口视图层级 │ View.mAttachInfo为null(视图已分离) │ View.mID = R.id.viewPager │ View.mWindowAttachCount = 1 │ mContext为com.pkasemer.mwonyaa.RootActivity实例,mDestroyed = false │ ↓ View.mParent │ ~~~~~~~ ├─ androidx.constraintlayout.widget.ConstraintLayout实例 │ 泄漏状态:未知 │ 保留1.6 MB,涉及11594个对象 │ View未属于窗口视图层级 │ View.mAttachInfo为null(视图已分离) │ View.mID = R.id.playerView │ View.mWindowAttachCount = 1 │ mContext为com.pkasemer.mwonyaa.RootActivity实例,mDestroyed = false │ ↓ View.mParent │ ~~~~~~~ ├─ androidx.recyclerview.widget.RecyclerView实例 │ 泄漏状态:未知 │ 保留1.5 MB,涉及10856个对象 │ View未属于窗口视图层级 │ View.mAttachInfo为null(视图已分离) │ View.mID = R.id.main_recycler │ View.mWindowAttachCount = 1 │ mContext为com.pkasemer.mwonyaa.RootActivity实例,mDestroyed = false │ ↓ View.mParent │ ~~~~~~~ ├─ androidx.swiperefreshlayout.widget.SwipeRefreshLayout实例 │ 泄漏状态:未知 │ 保留3.9 kB,涉及81个对象 │ View未属于窗口视图层级 │ View.mAttachInfo为null(视图已分离) │ View.mID = R.id.main_swiperefresh │ View.mWindowAttachCount = 1 │ mContext为com.pkasemer.mwonyaa.RootActivity实例,mDestroyed = false │ ↓ View.mParent │ ~~~~~~~ ╰→ android.widget.FrameLayout实例 泄漏状态:是(ObjectWatcher监控到该对象,因为com.pkasemer.mwonyaa.Fragments.Search收到Fragment#onDestroyView()回调,应清除视图引用以防止泄漏) 保留2.0 kB,涉及45个对象 key = fe8776a5-8c6c-452c-86ea-f07c94c6c300 watchDurationMillis = 19352 retainedDurationMillis = 14352 View未属于窗口视图层级 View.mAttachInfo为null(视图已分离) View.mWindowAttachCount = 1 mContext为com.pkasemer.mwonyaa.RootActivity实例,mDestroyed = false
元数据
- Build.VERSION.SDK_INT: 29
- Build.MANUFACTURER: INFINIX MOBILITY LIMITED
- LeakCanary版本: 2.10
- 应用进程名称: com.pkasemer.mwonyaa
- 类数量: 16415
- 实例数量: 216004
- 基本类型数组数量: 122153
- 对象数组数量: 34818
- 线程数量: 53
- 堆总字节数: 23265560
- Bitmap数量: 62
- Bitmap总字节数: 17227346
- 大Bitmap数量: 1
- 大Bitmap总字节数: 6553601
- 数据库状态:
- 已打开: /data/user/0/com.pkasemer.mwonyaa/databases/Mwonya(3个实例)
- 已打开: /data/user/0/com.pkasemer.mwonyaa/no_backup/androidx.work.workdb
- 已打开: /data/user/0/com.pkasemer.mwonyaa/databases/com.google.android.datatransport.events
- 已关闭: /data/user/0/com.pkasemer.mwonyaa/databases/google_app_measurement_local.db
- 统计信息: LruCache[maxSize=3000,hits=69898,misses=182744,hitRate=27%]
- 随机访问[bytes=10784339,reads=182744,travel=58867131075,range=27352144,size=34355609]
- 分析时长: 91735 ms
修复方案
1. 解决CardSliderViewPager的TimerTask泄漏
从日志链可以看出,泄漏的根源是CardSliderViewPager$SlidingTask(TimerTask子类)持有ViewPager的强引用,而ViewPager所在Fragment已执行onDestroyView,但TimerTask仍被Native层TaskQueue引用,导致整个视图树无法被GC回收。
- 自定义CardSliderViewPager场景:在View的
onDetachedFromWindow方法中停止Timer并清除引用:@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (timer != null) { timer.cancel(); // 取消所有待执行任务 timer.purge(); // 移除已取消任务 timer = null; } } - 第三方库CardSliderViewPager场景:
- 优先检查库是否提供停止自动滑动的API,在
SearchFragment的onDestroyView中调用; - 若无公开API,通过反射获取Timer实例并取消:
@Override public void onDestroyView() { super.onDestroyView(); CardSliderViewPager viewPager = view.findViewById(R.id.viewPager); try { Field timerField = CardSliderViewPager.class.getDeclaredField("timer"); timerField.setAccessible(true); Timer timer = (Timer) timerField.get(viewPager); if (timer != null) { timer.cancel(); timer.purge(); } } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } }
- 优先检查库是否提供停止自动滑动的API,在
2. 清理Fragment的视图引用
在SearchFragment的onDestroyView方法中,主动清除所有视图变量引用,避免Fragment残留引用导致泄漏:
@Override public void onDestroyView() { super.onDestroyView(); // 清空所有视图对象引用 mainRecycler = null; mainSwipeRefresh = null; playerView = null; viewPager = null; }
3. 确保ExoPlayer资源正确释放
虽本次泄漏直接诱因不是ExoPlayer,但需在Fragment/Activity销毁时释放资源,避免潜在泄漏:
@Override public void onDestroy() { super.onDestroy(); if (exoPlayer != null) { exoPlayer.release(); exoPlayer = null; } }
4. 优化数据库连接管理
元数据显示多个Mwonya数据库实例被打开,需通过单例模式管理数据库,避免重复创建实例,且在操作完成后关闭连接:
- 实现数据库单例类,确保全局仅一个实例;
- 数据库操作完成后,调用
close()方法关闭连接(注意不要在仍有未完成操作时关闭)。
内容的提问来源于stack exchange,提问作者PATRICK OKELLO
相关产品推荐
相关产品推荐

