无聊的分享:Activity销毁导致Fragment重叠问题

758 阅读3分钟

我正在参加「掘金·启航计划」

1.前言

最近发现了一个离谱的问题,当我打开某软件并长时间锁屏后重新打开并切换Tab时发现竟然出现了Fragment的重叠问题,回味一会发现其实是Activity销毁重建后Fragment重新加载所导致的问题,本文章就如何解决上述问题提供一点小小的思路。

2.造成重叠的原因

当我们的应用长期处于后台时,系统回收了主页Activity,当我们重新解锁后,应用处于前台,主页Activity被销毁并重新创建了,并且在销毁之前执行了onSaveInstanceState(Bundle outState)这个方法,该方法会保存当前的视图层,之前被实例化过的 Fragment 依然会出现在 Activity 中,此时的 FragmentTransaction 中的相当于又再次 add 了 fragment 进去的,hide()和show()方法对之前保存的fragment已经失效了,这个时候点击底部导航的话会重新去切换fragment,于是发生了重叠的问题。

3.如何复现

复现方法很简单,进入手机设置 -> 开发者选项 -> 应用 -> 开启不保留活动(离开后即销毁每个活动) 即可达成复现条件:

我们现在模拟主页Acitivty,存在AFramgnet和BFragment。

class FragmentActivity : AppCompatActivity() {
    //用的是DataBinding
    private lateinit var binding: ActivityFragmentBinding
    private var aFragment: AFragment? = null
    private var bFragment: BFragment? = null
    private var currentId = R.id.tv_a //当前id
    ....
 }
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_fragment)
    binding.apply {
        tvA.setOnClickListener(onClickListener)
        tvB.setOnClickListener(onClickListener)
    }
    //初始化AFragment
    changeFragment(currentId)
}

点击事件:

private var onClickListener = OnClickListener {
    changeFragment(it.id)
}

在Activity创建时将通过changeFragment(id: Int)方法将AFragment创建并添加到视图中去。

//切换Fragment
private fun changeFragment(id: Int) {
    supportFragmentManager.beginTransaction().apply {
        hideFragment(this)
        when (id) {
            R.id.tv_a -> {
                aFragment?.let {
                    show(it)
                } ?: run {
                    aFragment = AFragment()
                    add(R.id.fl_container, aFragment!!)
                }
                binding.tvA.isSelected = true
            }

            R.id.tv_b -> {
                bFragment?.let {
                    show(it)
                } ?: run {
                    bFragment = BFragment()
                    add(R.id.fl_container, bFragment!!)
                }
                binding.tvB.isSelected = true
            }
        }
    }.commitNowAllowingStateLoss()

}

切换之前先通过hideFragment(transaction: FragmentTransaction)隐藏所有的Fragment:

private fun hideFragment(transaction: FragmentTransaction) {
    binding.tvA.isSelected = false
    binding.tvB.isSelected = false
    aFragment?.let {
        transaction.hide(it)
    }
    bFragment?.let {
        transaction.hide(it)
    }
}

现在我们将项目跑起来,并且在开发者模式中打开不保留活动,进入后切换B fragment后切入后台再切回前台就可以复现重叠的情况了:

4.如何解决

我们在使用FragmentTransactionadd方法时需要一并存入tag,通过FragmentManagerfindFragmentByTag来初始化Fragment,重写onSaveInstanceState在Activity销毁时存入Tag,再 onCreate中通过Tag查找存入id再次选中Fragment。

更改后的onCreate

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_fragment)
        binding.apply {
            tvA.setOnClickListener(onClickListener)
            tvB.setOnClickListener(onClickListener)
        }
        aFragment = AFragment()
        //通过savedInstanceState来获取销毁时选中的id
        savedInstanceState?.let { bundle ->
            bundle.getString(TAG)?.let {
                currentId = getCurrentId(it)
            }
        }
        changeFragmentSafe(currentId)
    }

更改后的切换Fragment方法changeFragmentSafe(id: Int)

private fun changeFragmentSafe(id: Int) {
    currentId = id
    //获取Fragment
    val fragment = initFragment(id)
    supportFragmentManager.beginTransaction().apply {
        hideFragment(this)
        //add时加入tag
        fragment?.let {
            if (!it.isAdded) {
                add(R.id.fl_container, it, getFragmentTag(id))
            }
            show(it)
        }
        findViewById<TextView>(id).isSelected = true
    }.commitNowAllowingStateLoss()
}

初始化:

private fun initFragment(id: Int): Fragment? {
    //通过id转为tag来找对应的Fragment
    var fragment = supportFragmentManager.findFragmentByTag(getFragmentTag(id))
    when (id) {
        R.id.tv_a -> {
            if (fragment == null) {
                fragment = AFragment.newInstance()
            }
            aFragment = fragment as AFragment
        }

        R.id.tv_b -> {
            if (fragment == null) {
                fragment = BFragment.newInstance()
            }
            bFragment = fragment as BFragment
        }
    }
    return fragment
}

这里用了个比较繁琐的方法来转换当前id和存入的tag:

//tag 转 id
private fun getCurrentId(tag: String): Int {
    return when (tag) {
        "tv_a" -> {
            R.id.tv_a
        }

        "tv_b" -> {
            R.id.tv_b
        }

        else -> {
            R.id.tv_a
        }
    }
}

//id 转 tag
private fun getFragmentTag(id: Int): String {
    return when (id) {
        R.id.tv_a -> {
            "tv_a"
        }

        R.id.tv_b -> {
            "tv_b"
        }

        else -> {
            "tv_a"
        }
    }
}

通过以上的方法,当Activity销毁重建后,会先去从FragmentManager中使用tag获取对应的Fragment对象,如果没有再去重新创建Fragment实例,这样就解决了Fragment的重叠问题:

5.结尾

以上就是关于Activity销毁重建后Fragment重新加载的分析和解决方法,其实除了更改开发者选项来复现之外还可以通过旋转屏幕、使用AndroidStudio工具 杀掉应用程序,模拟内存不足回收或者转跳到其他Activity、打开多任务窗口、使用Home回到主屏幕再返回都可能复现,且上述的解决方案这只是其中的一种方式,如有不足请在评论区或私信指出,如果你们有更多的方法也欢迎在论区或私信留言捏❤️❤️