Fragment的填坑之旅

前言

目前主流的应用中,多是采用单Actvity多Fragment的方式实现的。随着应用功能越来越多,界面越来越复杂,我们会利用Fragment对Activity的界面进行模块化编程。Fragment有着种种优点吸引着我们,如比Activity有着更好的性能,能够轻量切换,开销比Activity小等等。

作为 view 界面的一部分,Fragment 的存在必须依附于 FragmentActivit使用,并且与 FragmentActivit 一样,拥有自己的独立的生命周期,同时处理用户的交互动作。同一个 FragmentActivit 可以有一个或多个 Fragment 作为界面内容,同样Fragment也可以拥有多个子Fragment,并且可以动态添加、删除 Fragment,让UI的重复利用率和易修改性得以提升,同样可以用来解决部分屏幕适配问题。另一方面,support v4 包中也提供了 Fragment,兼容 Android 3.0 之前的系统,使用兼容包需要注意两点:
宿主Activity 必须继承自 FragmentActivity;
使用getSupportFragmentManager() 方法获取 FragmentManager 对象;

尽管Fragment带来许多便利,让我们脱离了重复的构建Activity中,但是我们还是无法忽视它给我们带来的许多麻烦。下面我将列出在使用Fragment时,可能出现的问题,并且会分析其原因和提供解决方案。

当app长时间运行在系统后台时,如果出现系统资源紧张时会导致将该app的资源全部回收,这时如果将app再从后台返回前台运行时,app会重新启动。这种情况称为“内存重启”。在系统要把app回收之前,会先调用方法onSaveInstanceState 将Activity的状态保存下来,Activity的FragmentManager负责把Activity的Fragment信息保存起来。在“内存重启”后,Activity的恢复是从栈底到栈顶逐步恢复,Fragment会在宿主Activity的onCreate方法调用后紧接着恢复(从onAttach生命周期开始)。

getActivity() 为空的异常

在出现上述“内存重启”之后,调用getActivity()时,有时会返回null,提示空指针异常。
原因:
出现这种情况,在调用getActivity()时,当前的Fragment已经onDetach了宿主Activity。但是Fragment还执行了异步任务,结束后调用该方法,这样就会出现空指针。
再次打开该Activity时, 在onCreate方法里取出bundle里的fragment状态, 但这时fragment对应的Activity早就不在了, 所以getActivity为空。

解决方法:
在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是相比空指针闪退,这种做法“安全”些),即:
@Override
public void onAttach(Context context) {
super.onAttach(context);
activity = (Activity) context;
}

Fragment重叠异常

当App的页面中,add了多个Fragment时,在“内存重启”之后,回到前台时,app的这几个Fragment会出现界面重叠的现象。
原因:
FragmentManager帮我们管理Fragment,发生“内存重启”之后,它会从栈底向栈顶的顺序一次性恢复所有保存的Fragment;但是并没有保存每个Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。
(如果是replace,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,所有使用replace不会造成重叠现象)。
解决方法:
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.通过getSupportFragmentManager().getFragments()恢复
通过getFragments()可以获取到当前FragmentManager管理的栈内所有Fragment。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);

TargetFragment targetFragment;
HideFragment hideFragment;

if (savedInstanceState != null) {  // “内存重启”时调用
    List<Fragment> fragmentList = getSupportFragmentManager().getFragments();

//此处遍历时,不能获取isHidden的状态,因为没有保存
for (Fragment fragment : fragmentList) {
if(fragment instanceof TartgetFragment){
targetFragment = (TargetFragment)fragment;
}else if(fragment instanceof HideFragment){
hideFragment = (HideFragment)fragment;
}

// 解决重叠问题
getFragmentManager().beginTransaction()
.show(targetFragment)
.hide(hideFragment)
.commit();
}else{ // 正常时
targetFragment = TargetFragment.newInstance();
hideFragment = HideFragment.newInstance();

    // 这里add时,tag可传可不传
    getFragmentManager().beginTransaction()
            .add(R.id.container)
            .add(R.id,container,hideFragment)
            .hide(hideFragment)
            .commit();
}

}

3.自己保存Fragment的Hidden的状态
发生Fragment重叠的根本原因在于FragmentState没有保存Fragment的显示状态,即mHidden,导致页面重启后,该值为默认的false,即show状态,所以导致了Fragment的重叠。此方案由 由Fragment自己来管理自己的Hidden状态!
基类Fragment代码:
public class BaseFragment extends Fragment {
private static final String STATE_SAVE_IS_HIDDEN = “STATE_SAVE_IS_HIDDEN”;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
...
if (savedInstanceState != null) {
    boolean isSupportHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN);

    FragmentTransaction ft = getFragmentManager().beginTransaction();
    if (isSupportHidden) {
        ft.hide(this);
    } else {
        ft.show(this);
    }
    ft.commit();
}

@Override
public void onSaveInstanceState(Bundle outState) {
    ...
    outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden());
}

}
优点:不管多深的嵌套Fragment、同级Fragment等场景,全都可以正常工作,不会发生重叠!
缺点:其实也不算缺点,就是需要在加载根Fragment时判断savedInstanceState是否为null才初始化fragment,否则重复加载,导致重叠,判断如下:

public class MainActivity … {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    // 这里一定要在save为null时才加载Fragment,Fragment中onCreateView等生命周里加载根子Fragment同理
    // 因为在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠
    if(saveInstanceState == null){
          // 这里加载根Fragment
    }
}

}
4.手动修改不保存所有记录
在Activity中重写onSaveInstanceState方法:
@Override
protected void onSaveInstanceState(Bundle outState) {
//super.onSaveInstanceState(outState); //注释掉该方法, 即不保存状态
}
这样做虽然比较随意,但是相比页面重叠,用户体验来说要好上不少。

异常:Can not perform this action after onSaveInstanceState
在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你使用commit()提交了Fragment事务,就会抛出该异常!
解决办法:
该事务使用commitAllowingStateLoss()方法提交,但是有可能导致该次提交无效!(在此次离开时恰巧Activity被强杀时)。

关于onHiddenChanged和setUserVisibleHint

onHiddenChanged的回调时机
当使用add()+show(),hide()跳转新的Fragment时,旧的Fragment回调onHiddenChanged(),不会回调setUserVisibleHint和onStop()等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged(),这点要切记。(第一次加载时,不会调用该方法);
使用FragmentPagerAdapter+ViewPager时,切换回上一个Fragment页面时(已经初始化完毕),不会回调任何生命周期方法以及onHiddenChanged(),只有setUserVisibleHint(boolean isVisibleToUser)会被回调,所以如果你想进行一些懒加载,需要在这里处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值