美文网首页android应用框架Android面试相关Android那些事
Fragment全解析系列(一):那些年踩过的坑

Fragment全解析系列(一):那些年踩过的坑

作者: YoKey | 来源:发表于2016-02-27 21:48 被阅读142949次

    Fragment系列文章:
    1、Fragment全解析系列(一):那些年踩过的坑
    2、Fragment全解析系列(二):正确的使用姿势
    3、Fragment之我的解决方案:Fragmentation

    本篇主要介绍一些最常见的Fragment的坑以及官方Fragment库的那些自身的BUG,并给出解决方案;这些BUG在你深度使用时会遇到,比如Fragment嵌套时或者单Activity+多Fragment架构时遇到的坑。


    Fragment是可以让你的app纵享丝滑的设计,如果你的app想在现在基础上性能大幅度提高,并且占用内存降低,同样的界面Activity占用内存比Fragment要多,响应速度Fragment比Activty在中低端手机上快了很多,甚至能达到好几倍!如果你的app当前或以后有移植平板等平台时,可以让你节省大量时间和精力。


    简陋的目录
    1、getActivity()空指针
    2、异常:Can not perform this action after onSaveInstanceState
    3、Fragment重叠异常-----正确使用hide、show的姿势
    4、Fragment嵌套的那些坑
    5、未必靠谱的出栈方法remove()
    6、多个Fragment同时出栈的深坑BUG
    7、深坑 Fragment转场动画


    开始之前

    最新版知乎,单Activity多Fragment的架构,响应可以说非常“丝滑”,非要说缺点的话,就是没有转场动画,并且转场会有类似闪屏现象。我猜测可能和Fragment转场动画的一些BUG有关。(这系列的最后一篇文章我会给出我的解决方案,可以自定义转场动画,并能在各种特殊情况下正常运行。)

    但是!Fragment相比较Activity要难用很多,在多Fragment以及嵌套Fragment的情况下更是如此。
    更重要的是Fragment的坑真的太多了,看Square公司的这篇文章吧,Square:从今天开始抛弃Fragment吧!

    当然,不能说不再用Fragment,Fragment的这些坑都是有解决办法的,官方也在逐步修复一些BUG。
    下面罗列一些,有常见的,也有极度隐蔽的一些坑,也是我在用单Activity多Fragment时遇到的坑,可能有更多坑可以挖掘...

    在这之前为了方便后面文章的介绍,先规定一个“术语”,安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)

    在系统要把app回收之前,系统会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。在“内存重启”后,Activity的恢复是从栈顶逐步恢复,Fragment会在宿主Activity的onCreate方法调用后紧接着恢复(从onAttach生命周期开始)。


    getActivity()空指针

    可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。

    大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()了宿主Activity。
    比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。

    解决办法:
    更"安全"的方法:(对于Fragment已经onDetach这种情况,我们应该避免在这之后再去调用宿主Activity对象,比如取消这些异步任务,但我们的团队可能会有粗心大意的情况,所以下面给出的这个方案会保证安全)

    在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些),即:

    protected Activity mActivity;
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        this.mActivity = activity;
    }
    
    /**
    *  如果你用了support 23的库,上面的方法会提示过时,有强迫症的小伙伴,可以用下面的方法代替
    */
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.mActivity = (Activity)context;
    }
    

    异常:Can not perform this action after onSaveInstanceState

    有很多小伙伴遇到这个异常,这个异常产生的原因是:

    在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你执行Fragment事务,就会抛出该异常!(一般是其他Activity的回调让当前页面执行事务的情况,会引发该问题)

    解决方法:

    • 1、该事务使用commitAllowingStateLoss()方法提交,但是有可能导致该次提交无效!(宿主Activity被强杀时)

    对于popBackStack()没有对应的popBackStackAllowingStateLoss()方法,所以可以在下次可见时提交事务,参考2

    • 2、利用onActivityForResult()/onNewIntent(),可以做到事务的完整性,不会丢失事务

    一个简单的示例代码 :

    // ReceiverActivity 或 其子Fragment:
    void start(){
       startActivityForResult(new Intent(this, SenderActivity.class), 100);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
         if (requestCode == 100 && resultCode == 100) {
             // 执行Fragment事务
         }
     }
    
    // SenderActivity 或 其子Fragment:
    void do() { // 操作ReceiverActivity(或其子Fragment)执行事务
        setResult(100);
        finish();
    }
    

    Fragment重叠异常-----正确使用hide、show的姿势

    在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==nullif(findFragmentByTag(mFragmentTag) == null),导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)

    @Override 
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    // 在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠 ;
        if(saveInstanceState == null){
        // 或者 if(findFragmentByTag(mFragmentTag) == null)
           // 正常情况下去 加载根Fragment 
        } 
    }
    

    详细原因:从源码角度分析,为什么会发生Fragment重叠?

    如果你add()了几个Fragment,使用show()、hide()方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的话,在“内存重启”后回到前台,app的这几个Fragment界面会重叠。

    原因是FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;
    但是因为官方没有保存Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。
    (如果是replace,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,所有使用replace不会造成重叠现象)

    v4-24.0.0+ 开始,官方修复了上述 没有保存mHidden的问题,所以如果你在使用24.0.0+的v4包,下面分析的2个解决方案可以自行跳过...

    这里给出2个解决方案:
    1、是大家比较熟悉的 findFragmentByTag

    即在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。

    下面是个标准恢复写法:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity);
    
        TargetFragment targetFragment;
        HideFragment hideFragment;
      
        if (savedInstanceState != null) {  // “内存重启”时调用
            targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
            hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
            // 解决重叠问题
            getFragmentManager().beginTransaction()
                    .show(targetFragment)
                    .hide(hideFragment)
                    .commit();
        }else{  // 正常时
            targetFragment = TargetFragment.newInstance();
            hideFragment = HideFragment.newInstance();
    
            getFragmentManager().beginTransaction()
                    .add(R.id.container, targetFragment, targetFragment.getClass().getName())
                    .add(R.id,container,hideFragment,hideFragment.getClass().getName())
                    .hide(hideFragment)
                    .commit();
        }
    }
    

    如果你想恢复到用户离开时的那个Fragment的界面,你还需要在onSaveInstanceState(Bundle outState)里保存离开时的那个可见的tag或下标,在onCreate“内存重启”代码块中,取出tag/下标,进行恢复。

    ** 2、我的解决方案,9行代码解决所有情况的Fragment重叠:传送门**


    Fragment嵌套的那些坑

    其实一些小伙伴遇到的很多嵌套的坑,大部分都是由于对嵌套的栈视图产生混乱,只要理清栈视图关系,做好恢复相关工作以及正确选择是使用getFragmentManager()还是getChildFragmentManager()就可以避免这些问题。

    这部分内容是我们感觉Fragment非常难用的一个点,我会在下一篇中,详细介绍使用Fragment嵌套的一些技巧,以及如何清晰分析各个层级的栈视图。

    附:startActivityForResult接收返回问题
    在support 23.2.0以下的支持库中,对于在嵌套子Fragment的startActivityForResult (),会发现无论如何都不能在onActivityResult()中接收到返回值,只有最顶层的父Fragment才能接收到,这是一个support v4库的一个BUG,不过在前两天发布的support 23.2.0库中,已经修复了该问题,嵌套的子Fragment也能正常接收到返回数据了!


    未必靠谱的出栈方法remove()

    如果你想让某一个Fragment出栈,使用remove()在加入回退栈时并不靠谱。

    如果你在add的同时将Fragment加入回退栈:addToBackStack(name)的情况下,它并不能真正将Fragment从栈内移除,如果你在2秒后(确保Fragment事务已经完成)打印getSupportFragmentManager().getFragments(),会发现该Fragment依然存在,并且依然可以返回到被remove的Fragment,而且是空白页面。

    如果你没有将Fragment加入回退栈,remove方法可以正常出栈。

    如果你加入了回退栈,popBackStack()系列方法才能真正出栈,这也就引入下一个深坑,popBackStack(String tag,int flags)等系列方法的BUG。


    多个Fragment同时出栈的深坑BUG

    6月17日更新: 在support-25.4.0版本,google意识到下面的问题,并修复了。 如果你使用25.4.0及以上版本,下面的方法不要再使用,google移除了mAvailIndices属性

    在Fragment库中如下4个方法是可能产生BUG的:

    1、popBackStack(String tag,int flags)
    2、popBackStack(int id,int flags)
    3、popBackStackImmediate(String tag,int flags)
    4、popBackStackImmediate(int id,int flags)

    上面4个方法作用是,出栈到tag/id的fragment,即一次多个Fragment被出栈。

    1、FragmentManager栈中管理fragment下标位置的数组ArrayList<Integer> mAvailIndeices的BUG

    下面的方法FragmentManagerImpl类方法,产生BUG的罪魁祸首是管理Fragment栈下标的mAvailIndeices属性:

    void makeActive(Fragment f) {
          if (f.mIndex >= 0) {
             return;
          } 
          if (mAvailIndices == null || mAvailIndices.size() <= 0) {
               if (mActive == null) {
                  mActive = new ArrayList<Fragment>();
               } 
               f.setIndex(mActive.size(), mParent); 
               mActive.add(f);
           } else {
               f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
               mActive.set(f.mIndex, f);
           } 
          if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
     }
    

    上面代码最终导致了栈内顺序不正确的问题,如下图:


    上面的这个情况,会一次异常,一次正常。带来的问题就是“内存重启”后,各种异常甚至Crash。

    发现这BUG的时候,我一脸懵比,幸好,stackoverflow上有大神给出了解决方案!hack FragmentManagerImplmAvailIndices,对其进行一次Collections.reverseOrder()降序排序,保证栈内Fragment的index的正确。

    public class FragmentTransactionBugFixHack {
    
      public static void reorderIndices(FragmentManager fragmentManager) {
        if (!(fragmentManager instanceof FragmentManagerImpl))
          return;
        FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
        if (fragmentManagerImpl.mAvailIndices != null && fragmentManagerImpl.mAvailIndices.size() > 1) {
          Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder());
        }
      }
    }
    

    使用方法就是通过popBackStackImmediate(tag/id)多个Fragment后,调用

    hanler.post(new Runnable(){
        @Override
         public void run() {
             FragmentTransactionBugFixHack.reorderIndices(fragmentManager));
         }
    });
    

    2、popBackStack的坑
    popBackStackpopBackStackImmediate的区别在于前者是加入到主线队列的末尾,等其它任务完成后才开始出栈,后者是队列内的任务立即执行,再将出栈任务放到队列尾(可以理解为立即出栈)。

    如果你popBackStack多个Fragment后,紧接着beginTransaction() add新的一个Fragment,接着发生了“内存重启”后,你再执行popBackStack(),app就会Crash,解决方案是postDelay出栈动画时间再执行其它事务,但是根据我的观察不是很稳定。
    我的建议是:如果你想出栈多个Fragment,你应尽量使用popBackStackImmediate(tag/id),而不是popBackStack(tag/id),如果你想在出栈后,立刻beginTransaction()开始一项事务,你应该把事务的代码post/postDelay到主线程的消息队列里,下一篇有详细描述。


    深坑 Fragment转场动画(仅分析v4包下的Fragment)

    如果你的Fragment没有转场动画,或者使用setCustomAnimations(enter, exit)的话,那么上面的那些坑解决后,你可以愉快的玩耍了。

    getFragmentManager().beginTransaction()
             .setCustomAnimations(enter, exit)
            // 如果你有通过tag/id同时出栈多个Fragment的情况时,
            // 请谨慎使用.setCustomAnimations(enter, exit, popEnter, popExit)  
            // 在support-25.4.0之前出栈多Fragment时,伴随出栈动画,会在某些情况下发生异常
            // 你需要搭配Fragment的onCreateAnimation()临时取消出栈动画,或者延迟一个动画时间再执行一次上面提到的Hack方法,排序
    

    (注意:如果你想给下一个Fragment设置进栈动画和出栈动画,.setCustomAnimations(enter, exit)只能设置进栈动画,第二个参数并不是设置出栈动画;
    请使用.setCustomAnimations(enter, exit, popEnter, popExit),这个方法的第1个参数对应进栈动画,第4个参数对应出栈动画,所以是.setCustomAnimations(进栈动画, exit, popEnter, 出栈动画))

    总结起来就是Fragment没有出栈动画的话,可以避免很多坑。
    如果想让出栈动画运作正常的话,需要使用Fragment的onCreateAnimation中控制动画。

    @Override
    public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
        // 此处设置动画
    }
    

    但是用代价也是有的,你需要解决出栈动画带来的几个坑。

    1、pop多个Fragment时转场动画 带来的问题

    6月17日更新: 在support-25.4.0版本,google意识到下面动画引起的问题,并修复了。

    在使用 pop(tag/id)出栈多个Fragment的这种情况下,将转场动画临时取消或者延迟一个动画的时间再去执行其他事务;

    原因在于这种情景下,可能会导致栈内顺序错乱(上文有提到),同时如果发生“内存重启”后,因为Fragment转场动画没结束时再执行其他方法,会导致Fragment状态不会被FragmentManager正常保存下来。

    2、进入新的Fragment并立刻关闭当前Fragment 时的一些问题
    (1)如果你想从当前Fragment进入一个新的Fragment,并且同时要关闭当前Fragment。由于数据结构是栈,所以正确做法是先pop,再add,但是转场动画会有覆盖的不正常现象,你需要特殊处理,不然会闪屏!

    Tip:
    如果你遇到Fragment的mNextAnim空指针的异常(通常是在你的Fragment被重启的情况下),那么你首先需要检查是否操作的Fragment是否为null;其次在你的Fragment转场动画还没结束时,你是否就执行了其他事务等方法;解决思路就是延迟一个动画时间再执行事务,或者临时将该Fragment设为无动画

    总结

    看了上面的介绍,你可能会觉得Fragment有点可怕。

    但是我想说,如果你只是浅度使用,比如一个Activity容器包含列表Fragment+详情Fragment这种简单情景下,不涉及到popBackStack/Immediate(tag/id)这些的方法,还是比较轻松使用的,出现的问题,网上都可以找到解决方案。

    但是如果你的Fragment逻辑比较复杂,有特殊需求,或者你的app架构是仅有一个Activity + 多个Fragment,上面说的这些坑,你都应该全部解决。

    下一篇中,介绍了一些非常实用的使用技巧,包括如何解决Fragment嵌套、各种环境、组件下Fragment的使用等技巧,推荐阅读!

    还有一些比较隐蔽的问题,不影响app的正常运行,仅仅是一些显示的BUG,并没有在上面介绍,在本系列的最后一篇,我给出了我的解决方案,一个我封装的Fragmentation库,解决了所有动画问题,非常适合单Activity+多Fragment 或者 多模块Activity+多Fragment的架构。有兴趣的可以看看 :)

    相关文章

      网友评论

      • 1b2b261b4902:2、利用onActivityForResult()/onNewIntent(),可以做到事务的完整性,不会丢失事务

        这个例子没懂望大侠解释一下
      • 在下陈小村:为什么要写MainFragment,而不是直接在MainActivity中加Fragment,没弄明白。您看到,请帮忙解答一下。
      • devstrongzhao:对于单个Activity +多个fragment 在启动主页的时候会黑屏,切换fragment的时候会卡顿俩三秒
        怎么优化啊,难道是我fragment里的代码写多了? 还没有加数据请求 只是化UI 就卡了
      • 57365a30c6e5:我来简书是以为这里的字很少,没想到啊🙂全是大佬
      • sankemao:【进入新的Fragment并立刻关闭当前Fragment 时的一些问题】,我遇到退出fragment动画覆盖在进入fragment上了,这里作者是如何解决的,源码看了,但是没找到关键点。
      • 663a2bcb5ce4:楼主大神遇到过几个fragment切换用replace方法,都加入回退栈,但按返回键是报fragment 重复添加的问题吗?
        d5786e0375fd:遇到过,看我的csdn博客,https://blog.csdn.net/zxd_Android/article/details/82285702这里面有详细解释,简单的说,就是回退栈,本质上是对事物的逆反操作,比如一个transaction依次hide f1,remove f2,add f3,然后按返回键,底层会依次 remove f3,add f2,show f1.
      • 小廖111:您好 请教一个问题 用了您的控件 遇到一个问题 就是 我返回上个页面的时候 getFragmentManager().popBackStackImmediate(); 但是我现在项目有一个倒计时 在每一个fragment里面过了60秒就返回第一个fragment页面 也就是mainfragment 我进入APP也是从mainfragment进来的 整个APP只有一个Activity 在倒计时间结束我之前跳转直接是 start(MainFragment.newInstance()); 但是这样 过了半个小时 一个小时连续操作 我发现我的UI会很慢很卡 于是我尝试着用 List<Fragment> fragments = getFragmentManager().getFragments();
        for(int index=0;index<fragments.size();index++)
        {
        if(fragments.get(index) instanceof MainFragment)
        {
        getFragmentManager().popBackStack(fragments.get(index).getId(),FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        }可是到了时间 没有返回MainFragment去 停在当前页面 请问 遇到这种情况 我应该怎么做?
        YoKey:卡是因为你重复启动了MainFragment, 你可以使用启动模式来解决:start(MainFragment,SINGLETASK)
      • 小周爱吃瓜:写的真好 网上很多不全或者错误 或者作者都不知道自己写的对不对,还有Github的库很好用
      • 采姑娘的小蘑菇_fa4f:看了个开始就已经被折服:+1:
      • bde2c63d26af:写的很好,赞一个?
      • 商生:用的support 25.3.1 依然有重叠问题。。:joy:
        fragment 嵌套 fragment 在切换 外层fragment之后 子fragment偶尔会出现重叠了。
        YoKey:@574428723 应该是重复加载了同一个Fragment的实例
      • 写代码的解先生:求助大神,在使用setCustomAnimations()方法的时候出现了显示异常
        Fragment是通过 show hide形式进行切换的,使用了动画之后 出现问题
        进行切换之后,在一个界面点击自后界面会变成之前切换的界面
      • afb4c97a36a9:正在深坑中
      • 稻草僧:很详细,很有深度,值得详看
      • 81c17c76d562:在activity onStop 时候调用mFragments.dispatchStop() 也会把 fragmentManager的mStateSaved = true,而不仅仅是在onSaveInstanceState中。但是看博主只是在 onSaveInstanceState把mIsSaved = true;这样是否不妥?
        81c17c76d562:忽略我吧,我智障了
      • ramblejoy:开始用fragment就一直被各种坑 导致我一度很反感用fragment:joy:
      • 20501f08caef:想问下博主是怎么模拟“内存重启”的呢
        20501f08caef:@YoKey 非常感谢!
        YoKey:@lovexx 打开 开发者模式 -> 不保留活动
      • 落雨收柴:栈:d,c,b,a; pop(c, true)-----> 栈:null, null, b, a; push(c)---------> 栈:c,null,b,a。
        我pop并且push后,通过getFragments()获取到的List<Fragment>顺序(index从0开始)是:a,b,c,null; 我想查看一下真正栈的顺序,如何查看?是我这么获取不对吗?
      • 994e84007f0c:楼主怎么更新到support 25.4.0版本的,我这现在只能更新到25.3.1
        994e84007f0c:看到73楼了,谢谢
      • 994e84007f0c:博主如此用心负责的博文让人佩服
      • 再见信仰:非常感谢楼主分享
      • 满月写:分享一个自己使用的心得,关于getActivity() == null 的问题,可以用Fragment#isAdd()判断,不需要自己维持一个Activity的实例,如果返回false,后续的UI操作就可以丢弃了。不过像楼主说的,这些异步任务在Fragment销毁时确实应该cancle掉。
      • 增其Mrlu:目前看过最干货的fragment总结,其他都是复制粘贴。。
      • 0599beed3c0c:6月17日更新: 在support-25.4.0版本。。。
        话说,这个support是哪一个包啊?我在compile的时候没有找到25.4.0的v4包,有一个26.0.0-alpha1,但是里面也有“mAvailIndices”这个属性啊?并没有移除额= =!麻烦告知:pray:
        YoKey:@itzhy 从25.4.0开始,google将support代码迁移到maven {
        url "https://maven.google.com&quot;
        }, 所以 repositories里添加如上即可, 26.0.0-a 应该是和25.4.0并行开发的版本,未来会合并的
      • Alex_Code:very good
      • 紫豪:这个,栈内顺序图是怎么做的?
      • shuaizi:感觉作者对fragment的理解很不错
      • sayyid:牛!获益匪浅
      • be94987a43db:看起来好高深啊然而这是什么:sweat_smile:原来我看不懂的这么多
      • 叮宕:坑的意思是我大体记得之前我解决过这个问题,现在除了记得我解决过什么都忘了。坑总是坑,它总是令人容易遗忘碰上又让人难受的。
      • 安卓猿:楼主, 直接让Activity 不保存 所有状态,包括 fragment, 这种做法怎么样? 后台的Activity被回收后再重启,那就所有的view和fragment全部重新加载。

        @Override
        protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (outState != null) {
        if (needRemoveFragmentState()) {
        outState.remove("android:support:fragments");
        outState.clear();
        }
        }
        }
        YoKey:@安卓猿 不推荐, Fragment核心作用就是 状态保持器, 它会根据宿主Activity的状态去帮你保存一些数据、状态, 还是建议科学处理,了解其机制还是很简单的
      • 一个疑问句:很不错,受益良多
      • EastWoodYang:LZ,我也简单写了一个简单的Fragment管理框架OneMulti(https://github.com/EastWoodYang/OneMulti),有空帮我看看有没有问题:grin:
      • 闹气孩子:在重新回到该Activity的时候(onResumeFragments()或onPostResume()),再执行该事务!
        这个场景如何实现呢?如何判断此次这次事物是否提交呢?
        闹气孩子:@YoKey 谢谢~~
        YoKey:比如ActivityA启动了ActivityB, 在B里某个操作需要把A内的Fragment替换(或者其他事务操作),而A此时不可见,此时直接操作事务会导致 Can not perform this action after onSaveInstanceState! ;

        正确做法是,执行事务时判断是否可见,不可见则设置标志位,在下次可见时在onPostResume()里执行事务
      • 0045636e9a9f:楼主啊我也遇到了getActivity()为null的问题,可是引用了你的方法后还是不能解决。我在父类Fragment中的mannger是用getChildFragmentManger。然后在子类布局中mActivity的值依然返回为空,实在是没有思绪,能不能指点我一下
        0045636e9a9f:@YoKey 在无数据的情况下我的UI能正常运行,只要多个子Fragment获取内容就会null,我目前在检查我的adapter是不是除了问题,解决了问题会回复你的:kissing_heart:
        YoKey:没明白,因为用了文中的方法后确实再也没遇到null的情况了
        0045636e9a9f:楼主,我来修正一下我的问题,我只装载一个子fragment的数据的时候,子fragment的数据是可以显示的,可是我一旦设置了第二个子fragment的数据就立马报空指针,作者你遇到过这种情况吗。
        在最后感谢你写的这篇文章,我看了几遍, 感觉很有收获
      • Kemp_C:作者你好,看了你的文章受益匪浅,但是我想问问,为什么fragment+viewpager中使用的getfragment换成getChildfragmentManager他就不会出现切换后fragment为空的现
        YoKey:getfragmentManager 和getChildfragmentManager 是两个不同层级, 一个面向同级的Fragment,一个面向孩子Fragment
      • Alien的小窝:写的非常好,很久前遇到过这个问题,当时恨没有答案,先收藏了
      • 1f88954d1939:感谢作者解惑。同时我有一个问题,作者在这里:

        发现这BUG的时候,我一脸懵比,幸好,stackoverflow上有大神给出了解决方案!hack FragmentManagerImpl的mAvailIndices,对其进行一次Collections.reverseOrder()降序排序,保证栈内Fragment的index的正确。

        说 StackOverflow 大神提供了解决方案,可是我顺链接过去,发现和这里探讨的问题完全不一致,请问大神是否不小心附错了链接?如果是的话可否更正一下?感谢!
      • SevChen:谢谢!收藏了!!!!!
      • 9507bba91a1f:除了没用动画,该
        oooO ↘┏━┓ ↙ Oooo
        ( 踩)→┃你┃ ←(死 )
        \ ( →┃√┃ ← ) /
          \_)↗┗━┛ ↖(_/
        的坑都踩了个遍,早能看到这么好的文章就好了,为了回复立马注册了一发。 :smile:
      • 75437e238974:@YoKey 大神好,有这样的一个场景:我的app主页是类似微信主页,现在也在用你的Fragmentation库,然而用loadMultiple RootFragment 方法把四个fragment放进去之后,现在想在fragment1点击某个按钮切换并根据参数刷新fragment2的内容,我没有思路啊,没有类似以某种模式启动fragment2啊?或者说使用view Pager会好弄一些。写得不是很清楚,大神见谅
        75437e238974:@YoKey 嗯,event bus在用了,先这样用吧,谢谢了
        YoKey:@你是我最简单的快乐 Fragment 1 2是4个TabFragment的吧

        1点击某个按钮时,可以EventBus发送一个Event,父Fragment/Activity 和 2 来监听该Event, 父接收后控制切换到2, 2接收后刷新
      • 相互交流:楼主有一事不解啊,,内存重启,,全部被销毁,重新走生命周期方法,,需要获取到原来的Fragment对象,但是在activity获取的对象getSupportFragmentManager(),或者getFragmentManager()都是新的activity里面的,,和原来的对象不一样,,获取不到内存重启保存原来的Fragment,还是说需要内存重启前,需要保存getFragmentManager()这个对象??我看你在第一次加载的时候用的是getFragmentManager()这个,,还原的时候用的是getSupportFragmentManager()这个,,??不管用的那个,,在内存重启后getSupportFragmentManager()和getFragmentManager()都不是原来的对象,,它怎么能够找到原来的Fragment,,,,难道需要内存重启前保存????
        YoKey:@相互交流 http://www.jianshu.com/p/78ec81b42f92 这里可以解决你的疑问~
      • 相互交流:楼主 ,, 在pop多个Fragment后,FragmentTransactionBugFixHack.reorderIndices(FragmentManager)即可,也可以在add/replace Fragment之前调用一次 :),,FragmentTransactionBugFixHack这个降序的话,,就是FragmentC 、FragmentB、null、null、FragmentB退出的时候,,不会出错吗???
        YoKey:@相互交流 不会啊 因为经过排序 内部的顺序保证是正确的了
      • 4adaac986562:写的非常棒,大神要是出书,我绝对买 :dizzy_face:
      • Fritz_Xu:谢谢博主.这里想请教下:最近我发现app在切换到后台5,6分钟后,再打开app会爆出java.lang.IllegalStateException: Fragment Tab3Fragment not attached to Activity 的bug而闪退。这种也是内存重启导致的吗?
        Fritz_Xu:@YoKey ok,情况确实和我遇到的刚才那个bug时一样.因为使用博主的方法有会内存泄露,我想了下,暂时就先检测到app被干掉了就直接重启吧.对内存泄露有些恐惧 :joy:
        YoKey:@武械 嗯 应该是的
        你可以通过开启 开发者模式->(滑动到最后)不保留最近活动 来模拟内存重启
      • 大傻妹么么哒:谢谢分享
      • f172ff77b0db:楼主你好,最近项目出现了这个错误:Can not perform this action after onSaveInstanceState。
        根据友盟的错误日志看到是在按钮点击之后调用commit()触发的。想请教下这种情况应该怎么处理,谢谢 :blush:
        YoKey:@lanshouxianggu 这个issue在讨论,可以看看 https://github.com/YoKeyword/Fragmentation/issues/130
        f172ff77b0db:@YoKey 看了楼主的解决方案,理论上没啥问题。按道理来说,用户能点击button,说明Activity已经调用过onResume()了,就是不知道为啥还会报错~
        YoKey:@lanshouxianggu
        在离开AC时,比如进入下一个AC,或者回到桌面等,都立即会调用onSaveInstanceState(),在直到该AC重新onResume()期间,调用Fragment事务方法,必报此异常!

        解决方法2个:
        1、(不推荐)该事务使用commitAllowingStateLoss()方法提交,但是有可能导致该次提交无效!
        2、(推荐)在重新回到该Activity的时候(比如onStart里),再执行该事务!
      • 叛逆的青春不回头:我刚遇到重叠问题,fragment我用的是replace,可还是重叠了呀,根据官方资料和楼主分析表示不明白原因啊。。。
        YoKey:@4dce7389c312 在`onCreate`中加载Fragment,并且没有判断`saveInstanceState==null`,导致重复加载了同一个Fragment导致重叠。(PS:`replace`情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)
        叛逆的青春不回头:@YoKey 解决是能解决,可是根据分析不是repalce情况下不会重叠么
        YoKey: @4dce7389c312 看下 9行代码解决重叠 那篇文章的最后 要判断save=null的情况~
      • 7c67f307c431:getSupportFragmentManager().popBackStackImmdiate();
        new Handler().postDelay(new Runnable(){
        @Override
        public void run() {
        // 在这里执行Fragment事务
        }
        }, 你的出栈动画时间);

        popBackStackImmdiate 不是同步的吗,为什么后面还要post
        YoKey:@gavin2008 Fragment出栈时那个动画,会导致FragmentManager持有该Fragment的引用,可能会导致异常情况, delay延迟一个动画时间会保证安全~ :blush:

        即时你的Fragment没有设置动画,我也是建议post()内执行的,在一些特殊情况可以保证安全。
      • Focus19: @Override
        public void onAttach(Context context) {
        super.onAttach(context);
        this.mActivity = (Activity) context;
        }

        我用getActivity()获取上下文程序正常,改成您说的这个就报空指针异常了.
        YoKey:@天道酬勤1919 你确定异常指在这行的吗 =。= 讲道理肯定不会的... :joy:
        Focus19:@YoKey 试了一下,还是空指针啊.
        YoKey:@天道酬勤1919
        V4里的Fragment现在有2个onAttach(): 2种写法略微不同
        @Override
        public void onAttach(Context context) {
        super.onAttach(context);
        if(context instanceof Activity){
        this.mActivity = (Activity) context;
        }else{
        this.mActivity = getActivity();
        }
        }

        或者

        @Override
        public void onAttach(Activity activity) {
        super.onAttach(context);
        this.mActivity = activity;
        }
      • bab25a8c1541:感谢楼主!有个问题想问一下楼主:
        当前的Fragment重新打开一个Fragment然后快速点击回退,会出现当前Fragment中的控件点击无响应的状况,这是啥情况啊!求解
        YoKey:@mokuang_2016 额,这个问题没遇到过,可以从Fragment代码的书写检查看看~ :smile:
      • 伍子hm:正在使用楼主的fragmentation Lib,有些问题想请教楼主,楼主可否告知联系方式
        YoKey:@伍子hm 我的QQ:328903522
      • 失眠侠:楼主写的6啊。 顺便还想问下楼主,当Activity里维护了一个Fragment栈,当按返回键的时候,怎么处理比较合适,可能Activity退出的时候又有一些逻辑,或者还有软键盘什么的。
        YoKey:@失眠侠 可以参照第三篇的Fragmentation库源码里的实现,是类似安卓事件传递机制的原理实现的。 子FG如果消费(return true)了onBackPressSupport ,父FG、Activity就不会接收到这次BACK事件,如果不消费(return false, 也是默认的),则父FG,AC可以接收到
      • Yyyye:不建议使用 getFragments的那种方式
        在App重启时 会导致mNextAnim的空指针,而我换为第一种
        :blush: 原文:
        这里给出3个解决方案:
        1、是大家比较熟悉的 findFragmentByTag:
        就没有这个问题
        YoKey:@Yyyye "在App重启时 会导致mNextAnim的空指针" 这个其实是在重启前,Fragment事务操作已经导致不正常了,导致mNextAnim空指针异常~ getFragments方式在无异常的情况下是安全的
        不过现在还是推荐使用9行代码那个~一劳永逸的方法 :smile:
      • 红发_SHANKS:博主这喜欢数太亮眼了,1111......嘿哈哈哈哈哈哈
        YoKey: @壹分Orz 不要这样…………
      • xingstarx:特来膜拜下
        YoKey:@xingstarx :joy:
      • 玻色子:感谢 作者 很有用
      • 60702c4214f9:关于fragment重叠的解决方法,还有一种是在onAttachFragment(Fragment fragment)方法中,对参数fragment进行类型判断,然后赋值给对应的变量,不知楼主对这个是否有所了解?
        YoKey:@糖心果 这个方法是知道,但是还真不知道能作为恢复Fragment的解决方案,不知道是否完全安全,回头我尝试下哈~ 感谢分享 :smile:
      • duanduan2088:关于getActivity为空,我在项目中使用楼主的方法的确可以避免,但这样写又出现了另外一种bug->java.lang.IllegalStateException: Fragment PhotoFormatFragment not attached to Activity,也就是说虽然activity不为空,但此时可能fragment还没有attach到activity,照样崩,唉...
        YoKey:@android段段 我不确定你在何种情况下产生该异常,不过你使用isAdded() 这个方法判断下就可以避免该问题 :)
      • MeloDev:全部读完感觉我自己使用fragment逻辑是很简单的,这些坑只遇到过一个,不过总结的太好了,学习了
      • 好奇的小刺猬:遇到过几个,总结的这么全面一看就是重度使用者,感谢分享
      • atomic_volatile:对Fragment又爱又恨~
        YoKey:@阿童木online :smiley: 多踩踩,理清楚它的套路后,换来的是快速的UI响应以及低资源占用!
      • 1c5133cee27b:radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(RadioGroup group, int checkedId) {
        switch (checkedId) {
        case R.id.chache:
        getSupportFragmentManager().beginTransaction()
        .add(R.id.frame_content,tracke)
        .hide(accessories).hide(message).hide(mine)
        .commit();
        break;
        case R.id.accessories:
        getSupportFragmentManager().beginTransaction()
        .add(R.id.frame_content,accessories)
        .hide(tracke).hide(message).hide(mine)
        .commit();
        break;
        case R.id.message:
        getSupportFragmentManager().beginTransaction()
        .add(R.id.frame_content,message)
        .hide(tracke).hide(accessories).hide(mine)
        .commit();
        break;
        case R.id.mine:
        getSupportFragmentManager().beginTransaction()
        .add(R.id.frame_content,mine)
        .hide(tracke).hide(message).hide(accessories)
        .commit();
        break;
        }
        }
        });
        1c5133cee27b:已打赏。。谢谢了
        1c5133cee27b:真是太感谢您了,谢谢
        YoKey:@阿尔法猫 这里给你一种简写方法,第一次就把4个Fragment都add到FM中(如果Fragment资源占用比较大时,再考虑切换目标Fragment时才add这样的方式)
        else{ // 正常时
        tracke = TruckFragment.newInstance();
        accessories = AccessoriesFragment.newInstance();
        message=MessageFragment.newInstance();
        mine=MineFragment.newInstance();
        // // 这里add时,tag可传可不传
        getSupportFragmentManager().beginTransaction()
        .add(R.id.frame_content,tracke)
        .add(R.id.frame_content,accessories)
        .add(R.id.frame_content,message)
        .add(R.id.frame_content,mine)
        .hide(accessories).hide(message).hide(mine)
        .commit();
        }

        切换时:
        getSupportFragmentManager().beginTransaction()
        .show(mine) // add换成show
        .hide(tracke).hide(message).hide(accessories)
        .commit();
      • 1c5133cee27b:哥哥,Fragment全解析系列(一)这个的源码有吗?就是Fragment重叠异常问题,我放在底部了,结果只显示第一个的fragment,点击其他的不执行
        1c5133cee27b: if (savedInstanceState != null) { // “内存重启”时调用
        tost.showToast("内存重启");
        List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
        for (Fragment fragment : fragmentList) {
        if (fragment instanceof TruckFragment) {
        tracke = (TruckFragment) fragment;
        } else if (fragment instanceof AccessoriesFragment) {
        accessories = (AccessoriesFragment) fragment;
        } else if (fragment instanceof MineFragment){
        mine= (MineFragment) fragment;
        } else if (fragment instanceof MessageFragment){
        message= (MessageFragment) fragment;
        }
        // 解决重叠问题
        getSupportFragmentManager().beginTransaction()
        .show(tracke)
        .hide(accessories)
        .hide(message)
        .hide(mine)
        .commit();
        }

        }else{
        // 正常时
        tracke = TruckFragment.newInstance();
        accessories = AccessoriesFragment.newInstance();
        message=MessageFragment.newInstance();
        mine=MineFragment.newInstance();
        // // 这里add时,tag可传可不传
        getSupportFragmentManager().beginTransaction()
        .add(R.id.frame_content,tracke)
        .hide(accessories).hide(message).hide(mine)
        .commit();
        }
        YoKey:@阿尔法猫 :scream: 文章中的代码都是核心代码,是我特意写的,源码倒是没有的;你这个问题,点击其他没作用的话,检查下是否确定执行了
        fragmentManager.beginTransaction()
        .show(目标Fragment)
        .hide(当前Fragment)
        .commit(); :smiley:
      • yangyirunning:用心之作,干货十足!博主辛苦了!
      • ClownQiang:博主你好~关于“FragmentManager栈中管理fragment下标位置的数组ArrayList<Integer> mAvailIndeices的BUG”,这里我没太理解问题,还有你给出的这一段源码,我看了一下不太清楚问题在哪里,能否写的更详细一点~~谢谢啦
        YoKey:@ClownQiang
        f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
        mActive.set(f.mIndex, f);
        这两句代码,pop多个Fragment有时会导致mAvailIndices里的下标顺序出问题,导致Fragment的index错误,附带mActive这个管理Fragment的List下标错误
      • Jafir:半夜起来打蚊子,看看干货……看到 内存重启恢复那里,为什么只有俩个 ?一个是targetfragment 一个是hidefragment。 像QQ那样 都是3 4个~ 这里的俩个 中的hide具体指的什么?应该是除了要显示的targetfragment之外的fragment吧~希望楼主指明
        YoKey:@Jafir :joy: 打蚊子... 是的 那里为了阅读方便简写了下 就写了一个hideFragment
      • 1c5133cee27b:楼主,能提供一下代码吗,我菜鸟,复制上去出现了很多没见过的错
        1c5133cee27b:@YoKey 恩恩,谢谢
        YoKey:@阿尔法猫 我的这系列第三篇文章里有github地址包含Demo,不过建议新手避免深入使用Fragment,正常使用即可,可能第二篇更适合你,这一篇大部分内容都是深入使用时才会遇到的坑 :)
      • 孤舟远行:学习啦
      • 纤沫:在csdn 上,看到有推荐这篇文章,看了非常不错。
      • imknown:太棒了, 自己也走了好多 Fragment 的坑. 楼主的 提到的 那一个 兼容包 23.2 的问题 真的是太棒了. 我去 谷歌官网看了一下, The system now calls onActivityResult() for a nested FragmentActivity., 谢谢 哈哈
      • 自己找知己:可能是我看过的关于Fragment系列文章中最好的.
      • 5cf393479386:楼主将这些坑讲解的很透彻,学习了。 :+1: :+1:
      • devYOUK:很好的文章!
      • 滋滋滋Boom:楼主,想问下,为什么使用onAttach(Activity activity)代替getActivity()没有问题,但是使用onAttach(Context context)应用会空指针异常,
        java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.content.Context.getSystemService(java.lang.String)' on a null object reference
        YoKey:@滋滋滋Boom 从异常来看是你的Context是null,检查下你的context。应该和onAttach没什么关系。
        onAttach(Context context)这个方法是新的support包里新加的,是为了更安全的保证有宿主Activity。其源码:
        public void onAttach(Context context) {
        mCalled = true;
        final Activity hostActivity = mHost == null ? null : mHost.getActivity();
        if (hostActivity != null) {
        mCalled = false;
        onAttach(hostActivity);
        }
        }
      • 019ab4193bef:楼主真的好花心思,分析的很好,那些年用fragment遇到的坑,都让楼主写出来了,赞一个!!
      • 240b50cbfcf3:楼主,请教个问题,就是我项目是ViewPager + Fragment +RadioGroup的,但是每个Fragment里面还嵌套有Fragment,这样从Fragment打开子Fragment是空白的,这是什么情况,用的是hide(),add() 。
        YoKey:@T摩天倫 从这个错,我猜可能是你使用了Activity的R.id.container作为containerViewId
        父Fragment.getChildFragmentManager().beginTransaction
        .add(containerViewId,子Fragment)
        ...

        你应该使用父Fragment的FrameLayout容器,作为containerViewId :blush:
        240b50cbfcf3:@YoKey 这个知道,用了getChildFragmentManager的话,app直接闪退,报错:No view found .......
        YoKey:@T摩天倫 需求是Fragment嵌套吗? 是的话 ,add子Fragment时,使用父Fragment的getChildFragmentManager来操作,而不是用getFragmentManager :)
      • coco猫:你好 ,关于栈内顺序不正确,FragmentTransactionBugFixHack要怎么使用,这部分没看明白
        YoKey:作为一个类放入你工程下新建的android.support.v4.app这个包里就可以了,可以参考我第三篇里的Github里的源码 :smile:
        __Berial___:@YoKey 如何拿到 FragmentManagerImpl 这个只有包权限的类?创建相同的包?话说给的那个stackoverflow 的链接里没有这样的回答额。。
        YoKey:@coco猫 在pop多个Fragment后,FragmentTransactionBugFixHack.reorderIndices(FragmentManager)即可,也可以在add/replace Fragment之前调用一次 :)
      • Liu积土成山:总结的挺好的,一些坑自己也走过,就比如重叠问题
      • zyyoona7:楼主遇到过fragment背景透明的时候重叠问题么?
        YoKey:@zyyoona7 那你可以尝试换一个版本的v4包;检查下代码是否有问题;实在不行加一个底色背景吧
        排查下看看 这个我一直没遇到过,快速切换也测过,没遇到重叠问题
        zyyoona7:@YoKeyword 没有内存重启呀,我在应用内快速切换压力测试,就会出现重叠,至今没找到原因
        YoKey:@zyyoona7 这个就是内存重启后 FragmentManager帮你恢复,而你没有进行恢复后的找回以及hide操作引起的, 你可以参照我第二篇文章里 有两种方式可以解决这个问题
      • Alex_Cin:这是我见过,应该是最好的关于fragment的文章了
      • tao:关于“getActivity()空指针”我也遇到过,但是在fragment中持有activity的引用,这样有activity泄漏的风险吧
        5cf393479386:为了避免activity泄露是不是应该在onDetach方法中停掉所有异步任务呢?
        Nickyzhang:@YoKey 做一个为null的判断不是更好吗?最近在学习RxJava ,这个框架规避内存用着怎么样?
        YoKey:@tao 是的 不过相比较空指针crash,这样处理更安全。 内存泄漏风险自己做好处理就行啦,实际中对于异步任务,我使用RxJava来规避内存泄漏问题
      • 修得养得梦得过得:感谢楼主这一套关于Fragment的文章,我平时开发也很喜欢使用Fragment。先mark,等充周末仔细看看。
      • chandarlee:楼主你说的getActivity()在onCreateView()中使用可能会返回null。这点我有点不理解,你能说明在什么场景下吗?因为据我所知,这个方法调用的时候onAttach方法已经调用,host activity 不可能为空才对?
        chandarlee:@YoKeyword 是有可能v4包各个版本实现不一致导致的!Android可能默默地修改了一些Bug的!
        YoKey:@chandarlee 在很久之前解决这个问题时,看到的原因就是说“内存重启”之后,getActivity()会有个BUG,在onCreateView中使用有风险,而我当时确实测过是这样。今天测了下发现并没有这个原因,不知道是当时的库有那种BUG,还是时间较久忘记了一些细节...
        YoKey:@chandarlee 非常感谢你提出的问题,让我又去实践了下,结论就是:我在之前文章所说的原因并不正确,现在已经更新。
        产生null异常的 大部分原因是在Fragment已经detach了宿主Activity,仍然去调用getActivity()时会发生空指针异常。
        比如在pop了Fragment后,仍有异步任务在执行并调用了getActivity,或者在使用FragmentPagerAdapter时一些不恰当用法,以及在“内存重启”过程中,一些Fragment相关代码的不合理等都会导致getActivity空指针问题。
      • 66aa897454a2:了解,已关注楼主,希望有空和您多交流
      • 66aa897454a2:楼主挖掘的好深,佩服。其实我也是蛮讨厌Fragment的,因此我几乎不使用Fragment嵌套在Activity中,一般我的应用只有一个Activity,然后所有的界面切换都通过addView(),removeView()方式添加进Activity容器中,不知楼主对我这个做法有何看法,求指教下。
        YoKey:@K2y 文章开头那片Square的文章,其实也是说明了Fragment坑很多。他们也是用自定义View解决的,肯定完全可行的。相信管理好回退栈、内存的管理等,就没问题了。 :)
        Fragment是官方提供的一个灵活的组件,在官方的app有大量的使用,有官方的支持,兼容性更好点,最重要的是官方会一直维护,任何Activity的新特性,Fragment基本也都会有的。
      • Aegis:应该是关于fragment看过最好的系列了吧😂
        YoKey:@Aegis 😱
      • 陆地蛟龙:成功捕捉三楼。
      • 陆地蛟龙:溜溜溜 不错。
      • Razerdp:收藏了^ω^
      • HelloVass:不用说了,用心之作,先打赏再看文章

      本文标题:Fragment全解析系列(一):那些年踩过的坑

      本文链接:https://www.haomeiwen.com/subject/mtjwkttx.html