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

如何通过LeakCanary日志定位RewardFragment内存泄漏的具体代码行

如何定位RewardFragment内存泄漏的具体代码位置?

先看LeakCanary给出的泄漏追踪链:

ApplicationLeak(className=com.pilotflyingj.plugin.a_android.ui.component.loyalty.RewardFragment, leakTrace= 
┬ 
├─ android.view.inputmethod.InputMethodManager 
│ Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking) 
│ GC Root: System class 
│ ↓ static InputMethodManager.sInstance 
├─ android.view.inputmethod.InputMethodManager 
│ Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton) 
│ ↓ InputMethodManager.mNextServedView 
├─ com.android.internal.policy.DecorView 
│ Leaking: NO (View attached) 
│ mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.pilotflyingj.plugin.a_android.ui.component.main.MainFrameActivity with mDestroyed = false 
│ Parent android.view.ViewRootImpl not a android.view.View 
│ View#mParent is set 
│ View#mAttachInfo is not null (view attached) 
│ View.mWindowAttachCount = 1 
│ ↓ DecorView.mAttachInfo 
│ ~~~~~~~~~~~ 
├─ android.view.View$AttachInfo 
│ Leaking: UNKNOWN 
│ ↓ View$AttachInfo.mTreeObserver 
│ ~~~~~~~~~~~~~ 
├─ android.view.ViewTreeObserver 
│ Leaking: UNKNOWN 
│ ↓ ViewTreeObserver.mOnScrollChangedListeners 
│ ~~~~~~~~~~~~~~~~~~~~~~~~~ 
├─ android.view.ViewTreeObserver$CopyOnWriteArray 
│ Leaking: UNKNOWN 
│ ↓ ViewTreeObserver$CopyOnWriteArray.mData 
│ ~~~~~ 
├─ java.util.ArrayList 
│ Leaking: UNKNOWN 
│ ↓ ArrayList.elementData 
│ ~~~~~~~~~~~ 
├─ java.lang.Object[] 
│ Leaking: UNKNOWN 
│ ↓ array Object[].[2] 
│ ~~~ 
├─ com.pilotflyingj.plugin.a_android.ui.component.loyalty.-$$Lambda$RewardFragment$-e5xQ8M432-uGOrhL3ajFrvOVQg 
│ Leaking: UNKNOWN 
│ ↓ -$$Lambda$RewardFragment$-e5xQ8M432-uGOrhL3ajFrvOVQg.f$0 
│ ~~~ 
╰→ com.pilotflyingj.plugin.a_android.ui.component.loyalty.RewardFragment ​ 
Leaking: YES (Fragment#mFragmentManager is null and ObjectWatcher was watching this) ​ 
key = 11be43e3-0bcf-42fb-a087-025e52576844 ​ 
watchDurationMillis = 8225 ​ 
retainedDurationMillis = 3220 , retainedHeapByteSize=8730107)

从这个追踪链能明确泄漏核心:你的RewardFragment的一个Lambda实例被ViewTreeObserver的滚动监听列表持有,而这个监听链最终关联到了InputMethodManager系统单例,导致Fragment无法被GC回收。

接下来按这几步定位具体代码并修复:

  • 第一步:找到Fragment中注册滚动监听的代码
    搜索RewardFragment里的viewTreeObserver.addOnScrollChangedListener相关调用,重点找Lambda形式的实现(就是trace里那个自动生成的-$$Lambda$RewardFragment$-e5xQ8M432-uGOrhL3ajFrvOVQg对应的逻辑)。
    注意:这个监听可能不是直接在Fragment根View上注册的,也可能是在子View(比如RecyclerView、ScrollView、自定义滚动容器)的ViewTreeObserver上添加的。

  • 第二步:检查监听的移除逻辑是否完整
    问题大概率出在「只注册了监听,没在合适的生命周期移除」:

    • 如果你用的是匿名Lambda注册监听(比如下方示例),每次创建的Lambda都是新实例,后续根本无法准确移除,必须把Lambda赋值给Fragment的成员变量:
      // 错误写法:匿名Lambda无法被移除
      someView.viewTreeObserver.addOnScrollChangedListener {
          // 处理滚动逻辑
      }
      
      // 正确写法:将Lambda赋值给成员变量
      private val scrollChangeListener = ViewTreeObserver.OnScrollChangedListener {
          // 你的滚动处理逻辑
      }
      
    • 然后在Fragment的onDestroyView(推荐,因为View在这里销毁)或者onDestroy方法中,移除这个监听:
      override fun onDestroyView() {
          super.onDestroyView()
          // 先判断ViewTreeObserver是否存活,避免空指针
          if (someView.viewTreeObserver.isAlive) {
              someView.viewTreeObserver.removeOnScrollChangedListener(scrollChangeListener)
          }
      }
      
  • 第三步:验证修复效果
    修复后重新运行App,触发Fragment的销毁流程(比如切换到其他页面、从返回栈弹出),然后:

    • 观察LeakCanary是否还会报告该泄漏;
    • 用Android Studio的Profiler工具,手动触发GC,查看内存堆中是否还有RewardFragment的残留实例。

额外提醒:除了滚动监听,还要检查Fragment中是否有其他注册到全局单例、系统服务的监听/回调,确保在Fragment生命周期结束时都能及时解绑,避免类似的泄漏问题。

内容的提问来源于stack exchange,提问作者bimal chawla

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:01:22