前言
目前主流的应用中,多是采用单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)会被回调,所以如果你想进行一些懒加载,需要在这里处理。