一次Android Fragment内存泄露的bug解决记录|Fragment not attach to an Activity

Bug描述

前些天出现了一个 bug。Activity 页面里放了一个 ViewPager2,其中的每一页是一个 Fragment。其中第一页的 Fragment 实现了一个监听器,当事件发生和首次添加到监听器管理者 listener manager 时,manager 会通知所有监听者,监听器的回调需要用到当前 Activity 实现一些逻辑。但是在调用requireActivity()获取 activity 时,页面偶尔 会发生 crash,报错提示Exeception: Fragment not attach to an Activity。

因为是偶现的,于是排先了两个小时的原因😭,在这里记录下来,或许能给大家提供些经验。

场景复现

页面如图一所示

图二 页面布局

简化代码如下

kotlin 复制代码
class MyActivity:AppCompatActivity(){
    ...
    overide fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        // 添加Fragment
        val myList = mutableListOf<MyFragment>()
        myList.add(MyFragment())
        myList.add(MyFragment())
        myList.add(MyFragment())
        val myAdapter = ViewPagerTwoAdapter() // 一个继承了FragmentStateAdapter
        val viewPager = findViewById(R.id.vp)
        viewPager.adapter = myAdapter
        ...
        
    }
    ...

}

class MyFragment:Fragment(), MyChangeListenr{
    ...
    override fun onStart(){
        super.onStart()
        myChangeListenerManager.add(this) // 当事件发生和首次添加时,manager会通知所有监听者
    }
    
    override fun doOnChange(){
        val act = requireActivity() // 这行代码报错not attach to an activity,偶现
        ...
    }
    ...
    
}

interface MyChangeListenr{
    fun doOnChange()
}

其实看到这里,相信大家已经觉得有些不对劲了,在 Fragment 的 onStart() 函数中,把此 Fragment 作为监听器添加到了 manager 中,但是没有发现相关的 remove,也许就是因为这个,manager 始终持有此 Fragment的 引用,导致发生了内存泄露。但我们还需要一些证据。

分析调试

刚开始遇到这个bug时,我首先想到的:会不会是事件发生的时机正好处在Fragment拿不到Activity的生命周期?于是我再去复习 Fragment 的生命周期。

Fragment的主要生命周期方法依次是onAttach、onCreate、onCreateView、onViewCreated、onStart、onResume、onPause、onStop、onDestroyView、onDestory、onDetach。

在 onAttach 执行之后就可以通过 requireActivity 获取 Fragment 所在的 Activity 了,上面我们的时间监听是在 onStart 里才添加的,onStart之后早已可以获取 Activity。接下来通过添加日志,再次检查事件的触发时机。

kotlin 复制代码
class MyFragment:Fragment(), MyChangeListenr{
    ...
    overide fun onAttach(context: Context){
        super.onAttach(context)
        Log.d("lxl","onAttach")
    }
    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        Log.d("lxl","onCreate")
    }
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?){
        super.onCreateView(inflater, container, savedInstanceState)
        Log.d("lxl","onCreateView")
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?){
        super.onViewCreated(view, savedInstanceState)
        Log.d("lxl","onViewCreated")
    }
    override fun onStart(){
        super.onStart()
        Log.d("lxl","onViewCreated")
        myChangeListenerManager.add(this) // 当事件发生和首次添加时,manager会通知所有监听者
    }
    override fun onResume(){
        super.onResume()
        Log.d("lxl","onResume")
    }
    override fun onPause(){
        super.onPause()
        Log.d("lxl","onPause")
    }
    override fun onDestoryView(){
        super.onDestoryView()
        Log.d("lxl","onDestoryView")
    }
    override fun onDestry(){
        super.onDestry()
        Log.d("lxl","onDestry")
    }
    override fun onDetach(){
        super.onDetach()
        Log.d("lxl","onDetach")
    }
    
    override fun doOnChange(){
        if(isAdd){ // Fragment里的判断是否attach到Activity的方法
            Log.d("lxl","已绑定")
        }else{
            Log.d("lxl","未绑定")
        }
        
        val act = requireActivity() // 这行代码报错not attach to an activity,偶现
        ...
    }
    ...
    
}

我们尝试运行,第一次进入页面,显示如下

Terminal 复制代码
onAttach
onCreate
onCreateView
onViewCreated
onStart
onResume
已绑定

关闭再打开Activity,第二次进入页面crash,显示如下

Terminal 复制代码
onAttach
onCreate
onCreateView
onViewCreated
onStart
onResume
未绑定
已绑定

关闭再打开Activity,第三次进入页面crash,显示如下

Terminal 复制代码
onAttach
onCreate
onCreateView
onViewCreated
onStart
onResume
未绑定
未绑定
已绑定

可以看出,之后的每一次进入都会打印出来一个未绑定,这说明有多个 Fragment 都收到了监听,那么这些 Fragment 是从哪来的呢?原来是前面退出的 Activity 的 Fragment 没有被释放掉,仍然处在manager 的监听器列表里,这是一种内存泄露。所以我们在不使用 Fragment 时需要移除监听,代码如下

kotlin 复制代码
class MyFragment:Fragment(), MyChangeListenr{
    ...
    override fun onStart(){
        super.onStart()
        myChangeListenerManager.add(this) // 当事件发生和首次添加时,manager会通知所有监听者
    }
    
    override fun onStop(){
        super.onStop()
        myChangeListenerManager.remove(this) // 当Fragment消失,需要移除监听。防止持有引用,仍然被通知,这是一种内存泄露。
    }
    
    override fun doOnChange(){
        val act = requireActivity() // 这行代码报错not attach to an activity,偶现
        ...
    }
    ...
    
}

留下几个疑惑有待分析

  1. Fragment 被 manager 持有引用无法释放,那么 Fragment 会不会持有 Activity 的引用,导致Activity 无法释放?在这里是 ViewPager2 把 Fragment 和 Activity 绑定,理论上 ViewPager2 会把两者的绑定去掉,可以去看 ViewPager2 源码和发生 bug 时任务栈的情况
  2. Fragment 的 Stop 函数的执行时机与可见性的关系

想说的话

  1. 内存泄露是一个新手听了感到棘手的问题,但实际上产生的原因可能很简单,不用生畏。
  2. 解决 bug 可以通过形式上的合规,修改不规范的地方,这样工作效率更快。如果有余力的情况下可以分析一下 bug 产生的原因,从根本上避免能积累更多经验,避免头痛医头脚痛医脚。
  3. 学习 Android / 移动端 很开心,请各位大佬批评指正,也可以交个朋友互相交流!
相关推荐
撩得Android一次心动8 小时前
Jetpack Compose 知识点整理2【个人用】
android·笔记·学习·kotlin·compose
是真的小外套9 小时前
2024年软考中级信息安全工程师|选择题考点+案例分析真题解析(含答案)
android·网络
用户20187928316711 小时前
一文讲透 Android Native Heap 监控原理:一间"只进不出"的仓库,和总部派来的侦探
android
YB137512 小时前
Jetpack Compose 动画animate*AsState
android
高晶12 小时前
Android 通讯录应用开发实战:从零搭建 SQLite 数据持久化完整项目
android·设计模式·前端框架
隐擎fox12 小时前
深入 Android 移动端风控与协议层对抗:内核级虚拟环境检测、网络堆栈特征伪装与住宅 IP 协同实战
android·网络·python·网络协议·tcp/ip
换元不配限14 小时前
从 AsyncTask 到现代并发:Android 异步方案的演进与替代实践
android·asynctask·异步编程·方案对比·asynctask替代方案
用户20187928316714 小时前
你的 App 为什么是「电量刺客」?——从一条 WakeLock 追到 Linux 内核的 Android 省电原理全解
android
mmsx15 小时前
Android 安全校验下沉到原生层:防 patch 与构建期自动化
android·安全·自动化