我踩到的关于Fragment 状态的保存和恢复的坑

Posted 碎格子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我踩到的关于Fragment 状态的保存和恢复的坑相关的知识,希望对你有一定的参考价值。

在进行项目开发的时候遇到了一个奇怪的坑,在Activity和Fragment传递对象的时候已经对对象进行了判空处理,但是在Fabric统计上还是出现了“NullPointException”,我们在代码中的具体实现是在Activity里将对象注入Fragment中:

public void loadData()
    if(product != null)
        ProductDetailBottomLayoutFragment bottomLayoutFragment = new ProductDetailBottomLayoutFragment();
        Bundle bundle = new Bundle();
        bundle.putSerializable("product", product);
        bottomLayoutFragment.setArguments(bundle);
    transaction.replace(R.id.product_detail_bottom_fragment, bottomLayoutFragment).commit();
    

然后在Fragment的方法里获取传入的对象:

@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) 
        super.onActivityCreated(savedInstanceState);
        product = (TProductExtension) getArguments().getSerializable("product");
        if (getView() != null) 
            ...
            getIsFavorite(product.productUrl);
            ...
        
    

看起来似乎没有什么问题,我在Activity中已经判了product是否为空了,不为空的时候就创建Fragment的对象,然后将product对象传进去。但是我在Fragment里面使用product.productUrl时Fabric上却报出TProductExtension对象是空,虽然这个出错的概率在Fabric上显示占比比较小,但是我们还是要将这个bug给fix掉。

最开始想的办法是在onActivityCreated中获取product对象时再一次进行判空处理,即:

if (product != null && getView() != null) 
    ...
    getIsFavorite(product.productUrl);
    ...

这样虽然能使这段代码不再抛出”NullPointException”,但是我们并没有找到错误的根源在哪里,并且这样的判断也会引出其他的问题,例如当product再一次是空的时候,Fragment连View都不会进行显示。所以这个方法行不通。

那么,问题到底在哪呢?既然我们传入的方式是没有问题的,那么问题会不会出现在Fragment或者Activity自身呢?我们开始试着从Fragment生命周期下手。我们在Fragment里重写了onResume方法,打上了断点进行调试,然后神奇的事情发生了,当我从当前Activity跳转到下一个Activity再返回回来时,Fragment里的onResume方法居然被调用了两次。然后我去Activity看了创建Fragment对象的方法,loadData是在Activity的onCreate方法里,我在ProductDetailBottomLayoutFragment bottomLayoutFragment = new ProductDetailBottomLayoutFragment(); 这段代码打上断点调试,却发现这段代码却只运行了一次,那么另外一次onResume是谁调用的,又是在哪调用的呢?

最后发现,原来罪魁祸首就是onCreate的参数Bundle savedInstanceState, 没错就是他。我们都知道,Activity有自己的一套状态保存机制。

当用户要离开Activity并在Activity意外销毁时向其传递将保存的 Bundle 对象时,系统会调用此方法。
如果系统必须稍后重新创建Activity实例,它会将相同的 Bundle 对象同时传递给 onRestoreInstanceState()和 onCreate() 方法。

以上是来自android官方开发文档解释。

而为了证实到底是不是这个原因,我重写了onResume、onSaveInstanceState、onRestoreInstanceState、onPause方法进行断点调试从当前Activity跳转到下一个Activity再返回,得出的这几个方法的调用顺序分别是:onPause、onSaveInstanceState、onStop、onCreate、onRestoreInstanceState、onResume,而在onSaveInstanceState方法里会将我当前Activity内的Fragment存入HashMap中再存到Bundle对象中,然后当我Activity被销毁后返回回来时,系统会从onCreate方法和onRestoreInstanceState方法中将我们之前存入的Fragment取出来进行显示。

这是断点调试onCreate方法时获取到的Bundle对象,在mActive这个map中就存有我的Fragment等信息。

这样也就导致了我返回Activity后在onCreate方法里调用loadData方法new了一个Fragment,但Bundle对象中还保存了我的Fragment对象,在onCreate方法被调用时被取出,所以Fragment的onResume方法被调用了两次。而导致空指针异常的情况是,恢复的Fragment并没有走数据传递那一步,导致了我在Fragment里取到的product对象是空。

知道了这个原因,那问题就好解决了。只要onCreate方法调用的loadData里判断onSaveInstanceState是否为空,如果为空的话再创建Fragment的对象,如果不为空的话就让系统帮我进行Fragment的显示。

具体代码如下:

public void loadData(Bundle savedInstanceState)
    if(product != null)
        if(savedInstanceState == null)
            ProductDetailBottomLayoutFragment bottomLayoutFragment = new ProductDetailBottomLayoutFragment();
            Bundle bundle = new Bundle();
            bundle.putSerializable("product", product);
            bottomLayoutFragment.setArguments(bundle);
    transaction.replace(R.id.product_detail_bottom_fragment, bottomLayoutFragment).commit();
        
    

一般来说我们都应该注意Activity销毁时将必要的数据进行保存,保存数据写在onSaveInstanceState方法里,取出数据则写在onRestoreInstanceState方法中。

ps:以上都是在开启Android开发者选项的“不保留活动”这个选项后实践的,如果没有开启这个选项很难保证跳转下一个Activity时,上一个Activity被销毁了。

以上是关于我踩到的关于Fragment 状态的保存和恢复的坑的主要内容,如果未能解决你的问题,请参考以下文章

关于iview中tabs踩到的坑

MySQL Server-id踩到的坑

总结开发过程踩到的坑

Laravel 会话使用 Memcached 踩到的坑

聊聊因不恰当使用alibaba sentinel而踩到的坑

使用element ui tabs组件切换时踩到的坑