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

前言

本文转载自 Yokey,分享了Fragment 那些年走过的坑,很是精彩,相信对你们有所帮助。java

Yokey的博客地址程序员

https://www.jianshu.com/u/6b372d09b617web

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


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


简陋的目录 
一、getActivity()空指针 
二、异常:Can not perform this action after onSaveInstanceState 
三、Fragment重叠异常-----正确使用hide、show的姿式 
四、Fragment嵌套的那些坑 
五、未必靠谱的出栈方法remove() 
六、多个Fragment同时出栈的深坑BUG 
七、深坑 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,这种作法“安全”些),即:

 1protected Activity mActivity;
2@Override
3public void onAttach(Activity activity) {
4    super.onAttach(activity);
5    this.mActivity = activity;
6}
7
8/**
9*  若是你用了support 23的库,上面的方法会提示过期,有强迫症的小伙伴,能够用下面的方法代替
10*/

11@Override
12public void onAttach(Context context) {
13    super.onAttach(context);
14    this.mActivity = (Activity)context;
15}

异常:Can not perform this action after onSaveInstanceState

有不少小伙伴遇到这个异常,这个异常产生的缘由是:

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

解决方法:

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

>
对于popBackStack()没有对应的popBackStackAllowingStateLoss()方法,因此能够在下次可见时提交事务,参考2

  • 二、利用`onActivityForResult()`/`onNewIntent()`,能够作到事务的完整性,不会丢失事务

一个简单的示例代码 :

 1// ReceiverActivity 或 其子Fragment:
2void start(){
3   startActivityForResult(new Intent(this, SenderActivity.class), 100);
4}
5
6@Override
7protected void onActivityResult(int requestCode, int resultCode, Intent data) {
8     super.onActivityResult(requestCode, resultCode, data);
9     if (requestCode == 100 && resultCode == 100) {
10         // 执行Fragment事务
11     }
12 }
13
14// SenderActivity 或 其子Fragment:
15void do() // 操做ReceiverActivity(或其子Fragment)执行事务
16    setResult(100);
17    finish();
18}

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

在类onCreate()的方法加载Fragment,而且没有判断saveInstanceState==nullif(findFragmentByTag(mFragmentTag) ==null),致使重复加载了同一个Fragment致使重叠。(PS:replace状况下,若是没有加入回退栈,则不判断也不会形成重叠,但建议仍是统一判断下)

 1```
2@Override 
3protected void onCreate(@Nullable Bundle savedInstanceState) {
4// 在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,致使重叠 ;
5    if(saveInstanceState == null){
6    // 或者 if(findFragmentByTag(mFragmentTag) == null)
7       // 正常状况下去 加载根Fragment 
8    } 
9}
10```

详细缘由:从源码角度分析,为何会发生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个解决方案: 
一、是你们比较熟悉的findFragmentByTag

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

下面是个标准恢复写法:

 1@Override
2protected void onCreate(Bundle savedInstanceState
{
3    super.onCreate(savedInstanceState);
4    setContentView(R.layout.activity);
5
6    TargetFragment targetFragment;
7    HideFragment hideFragment;
8
9    if (savedInstanceState != null) {  // “内存重启”时调用
10        targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
11        hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
12        // 解决重叠问题
13        getFragmentManager().beginTransaction()
14                .show(targetFragment)
15                .hide(hideFragment)
16                .commit();
17    }else{  // 正常时
18        targetFragment = TargetFragment.newInstance();
19        hideFragment = HideFragment.newInstance();
20
21        getFragmentManager().beginTransaction()
22                .add(R.id.container, targetFragment, targetFragment.getClass().getName())
23                .add(R.id,container,hideFragment,hideFragment.getClass().getName())
24                .hide(hideFragment)
25                .commit();
26    }
27}

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

**
二、个人解决方案,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的:

一、popBackStack(String tag,int flags) 
二、popBackStack(int id,int flags) 
三、popBackStackImmediate(String tag,int flags) 
四、popBackStackImmediate(int id,int flags)

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

一、FragmentManager栈中管理fragment下标位置的数组ArrayList  mAvailIndeices的BUG

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

 1void makeActive(Fragment f{
2      if (f.mIndex >= 0) {
3         return;
4      } 
5      if (mAvailIndices == null || mAvailIndices.size() <= 0) {
6           if (mActive == null) {
7              mActive = new ArrayList<Fragment>();
8           } 
9           f.setIndex(mActive.size(), mParent); 
10           mActive.add(f);
11       } else {
12           f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
13           mActive.set(f.mIndex, f);
14       } 
15      if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
16 }

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

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

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

 1public class FragmentTransactionBugFixHack {
2
3  public static void reorderIndices(FragmentManager fragmentManager) {
4    if (!(fragmentManager instanceof FragmentManagerImpl))
5      return;
6    FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager;
7    if (fragmentManagerImpl.mAvailIndices != null && fragmentManagerImpl.mAvailIndices.size() > 1) {
8      Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder());
9    }
10  }
11}

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

1hanler.post(new Runnable(){
2    @Override
3     public void run() {
4         FragmentTransactionBugFixHack.reorderIndices(fragmentManager));
5     }
6});

二、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)的话,那么上面的那些坑解决后,你能够愉快的玩耍了。

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

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

总结起来就是Fragment没有出栈动画的话,能够避免不少坑。 
若是想让出栈动画运做正常的话,须要使用Fragment的onCreateAnimation中控制动画。

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

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

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

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

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

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

二、进入新的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 的架构。有兴趣的能够看看 :)

推荐阅读

Android 面试必备 - http 与 https 协议

Android 面试必备 - 计算机网络基本知识(TCP,UDP,Http,https)

Android 面试必备 - 线程

Android 面试必备 - JVM 及 类加载机制

Android 面试必备 - 系统、App、Activity 启动过程

致刚入职场的你 - 程序员的成长笔记

干起来,你就超过了 50% 的人

一个程序员的五年总结,给你不同的角度


stormjun94

扫一扫,欢迎关注个人公众号 stormjun94。若是你有好的文章,也欢迎你的投稿。


本文分享自微信公众号 - 徐公码字(stormjun94)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索