Fragment总结

3,504 阅读13分钟

Fragment是Android开发中常用的组件之一,也是最容易出问题的组件,为了更好地使用它,对此进行一个简单的总结。

说明: 由于v4包中的Fragment具有更好的兼容性,且可随时更新到指定版本,所以本文的讨论仅限v4包中的Fragment。

使用Fragment

使用Fragment的方式有2种:

  1. 在布局文件中使用标签,将其中的name属性值设置为需要加载的Fragment的全路径;

当系统在创建Activity时,会实例化布局中指定的Fragment,并调用它的onCreateView方法,以返回的View来替换元素。

  1. 动态创建Fragment对象,通过FragmentTransaction来加载;

在创建Fragment的时候需要注意,Android不推荐使用自定义构造方法的方式来创建Fragment,可使用官方推荐的方式来传参:

不提倡的方式:

public ChatFragment(int id, String name) {
	this.id = id;
	this.name = name;
}

推荐的方式:

public static Fragment newInstance(int id, String name) {
	Fragment fragment = new ChatFragment();
	Bundle bundle = new Bundle();
	bundle.putInt("id", id);
	bundle.putString("name", name);
	fragment.setArguments(bundle);
	return fragment;
}

当然,也可使用Fragment提供的静态初始化方法来构造Fragment:

/**
 * @param context 加载Fragment的Activity实例
 * @param fname 需要加载的Fragment的全路径名 [其本质是通过反射调用的]
 * @param args 需要传递的参数
 * @return
 */
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {

}

// 示例
Bundle bundle = new Bundle();
bundle.putInt("id", id);
bundle.putString("name", name);
Fragment.instantiate(this, "com.sxu.fragment.ChatFragment", bundle);

然后使用FragmentTransaction将Fragment提交给FragmentManager:

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.add(R.id.container_layout, ContentFragment.newInstance(0);
transaction.commit();

Fragment生命周期

相比Activity, Fragment的生命周期要复杂很多,使用官方的一张图来展示:

image

下面对Fragment生周期中几个核心的阶段说明一下:

onAttach/onDetach

onAttach是Activity与Fragment关联时被调用此,可在此方法中保存Activity实例解决getActivity为空的问题。与之对应的是onDetach, 表示与Activity解除绑定;

onCreate

与Activity一样,Fragment也可以使用onSaveInstanceState在退到后台时保存页面数据,但它没有提供onRestoreInstanceState方法,所以可在onCreate中进行恢复操作;

onCreateView/onDestroyView

加载Fragment View的地方,类似于Activity中的setContentView, 它返会Fragment加载的View。从图上可以看出, Fragment从回退栈中返回时,会从此方法开始调用,所以可将Fragment加载的View以成员变量的形式保存,当其为空时进行再加载操作, 从而避免View的多次加载。

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
	if (mContentView == null) {
		mContentView = inflater.inflate(R.layout.fragment_content_layout, container, false);
	}
	
	return mContentView;
}

FragmentView加载完成后onViewCreated会被调用,调用时机先于onActivityCreated,其中的参数View就表示Fragment加载的布局,所以可在此方法中获取布局中的各个View。

与之对应的是onDestroyView, 它会销毁Fragment中的View.

onActivityCreated

Activity的onCreate执行完成后被调用,Fragment中的逻辑通常在此方法中执行。

Fragment生命周期与Activity生命周期的关系

Fragment虽然有完整的生命周期,但仍然需要以Activity为宿主来存在,所以它的生命周期与Activity生命周期有着直接的关系,如图所示:

image

从图上可以看出,Fragment的生命周期和Activity基本保持一致。

与Activity不同的是,Fragment生命周期并总是在页面可见性发生变化时变化。在以下场景中,Fragment的可见性发生变化时,不会调用生命周期的任何方法。

Fragment show/hide:

Fragment在显示或隐藏时会回调onHiddenChanged, 参数hidden为true表示Fragment被隐藏, 否则表示被显示;

ViewPager中已加载的Fragment切换时:

ViewPager中已加载的Fragment在切换时会调用setUserVisibleHint, 参数isVisibleToUser为true表示Fragment被切换到当前页.由于ViewPager中的Fragment在首次加载时,也会调用setUserVisibleHint,导致出现监听重复的问题,所以在setUserVisibleHint需要添加条件判断:

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
	super.setUserVisibleHint(isVisibleToUser);
	if (isResumed() && isVisibleToUser) {
		// 页面被显示
	}
}

所以,监听Fragment页面的显示,除了监听Fragment生命周期中的onPause/onResume方法,还需要监听onHiddenChanged方法和setUserVisibleHint。

在统计Fragment页面的显示时长时,需要综合考虑这几个方法,具体见Android无埋点方案实践项目——Tracker中对Fragment生命周期的监听过程FragmentLifecycleListener

Fragment懒加载

懒加载(或者叫延迟加载),也就是延迟数据的请求过程。常用于ViewPager+Fragment模式中,不同的Fragment可能使用不同的接口,在页面显示的时候,可能会同时请求offscreenPageLimit 个接口,导致页面出现卡顿。为了解决这种问题,可延迟未显示的Fragment的数据请求过程,即在Fragment显示时,再进行网络请求。Fragment中的setUserVisibleHint方法在ViewPager中的Fragment显示时被调用,所以我们可在其中实现数据的请求。

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
	super.setUserVisibleHint(isVisibleToUser);
	if (isVisibleToUser && !dataRequested) {
		requestData();
	}
}

getContext()与getActivity()的区别

关于这两者的区别,可从源码来看:

@Nullable
public Context getContext() {
    return mHost == null ? null : mHost.getContext();
}

 @Nullable
final public FragmentActivity getActivity() {
    return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}

从它们的实现来看,都是直接返回mHost对象中的成员,mHost的类型为FragmentHostCallback,它的构造方法如下:

public FragmentHostCallback(Context context, Handler handler, int windowAnimations) {
    this(context instanceof Activity ? (Activity) context : null, context, handler,
            windowAnimations);
}

FragmentHostCallback(FragmentActivity activity) {
    this(activity, activity /*context*/, activity.mHandler, 0 /*windowAnimations*/);
}

FragmentHostCallback(Activity activity, Context context, Handler handler,
        int windowAnimations) {
    mActivity = activity;
    mContext = context;
    mHandler = handler;
    mWindowAnimations = windowAnimations;
}

通过对源码进行搜索,发现只是FragmentActivity中直接调用了FragmentHostCallback的构造方法:

class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
    public HostCallbacks() {
        super(FragmentActivity.this /*fragmentActivity*/);
    }
    
    ...
}

就目前源码的实现来说,这两者没有任何区别,引用的都是它所在的Activity的实例,但是它提供的公开的构造方法的实现却说明: getContext()为空的可能性能更大。 所以,在Fragment中获取Context实例时最好使用getActivity()。

getActivity返回null

在使用Fragment的过程中,getActivity()为null的异常应该是最常见的。其根本原因:Fragment与之前关联的Activity失去了联系!

使用Fragment时,我们的Activity继承的都是FragmentActivity, FragmentActivity在被异常关闭时会保存已加载的Fragment,具体如下:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    markFragmentsCreated();
    // 保存Fragment
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    ...
}

然后在其onCreate中对保存的Fragment进行了恢复:

protected void onCreate(@Nullable Bundle savedInstanceState) {
    mFragments.attachHost(null /*parent*/);
    super.onCreate(savedInstanceState);
    ...
    if (savedInstanceState != null) {
        Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
        mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
    }
    ...
}

虽然对Fragment进行了恢复,并与其关联了新的Activity实例,但Fragment之前关联的Activity实例已被销毁,如果这些Fragment中有一些延时任务,并使用了getActivity(), 就会出现空指针异常。

解决方案
  1. 在Fragment的onDetach中移除延时任务:

这是从根本上杜绝getActivity为空的方案。日常开发中,使用的延时任务最多的情况莫过于网络请求和Handler。 对于网络请求,封装的时候最好考虑Activity/Fragment的生命周期, 在onDestroy和onDetach取消网络请求,上传/下载等大数据量的网络请求,可引用ApplicationContext,放在后台服务进行执行。对于Handler, 只需要在onDetach中清除任务即可:

@Override
public void onDetach() {
	super.onDetach();
	handler.removeCallbacksAndMessages(null);
}
  1. 在onAttach中保存Activity的引用;

通过保存的Activity实例替代getActivity。Activity虽然重建了,但之前的实例因为Fragment的持有而不会被内存清理,会造成短暂性的内存泄漏。

@Override
public void onAttach(Context context) {
	super.onAttach(context);
	this.mContext = context;
}

具体使用哪种方法,看自己的需求,如果项目框架良好,团队又有良好的编程规范,自然是推荐第一种。否则还是使用第二种方案,虽然会造成短暂性的内存泄漏,倒也不会有什么大的影响。

Fragment页面重叠问题

FragmentActivity默认情况下在异常销毁时会保存Fragment,并在onCreate中进行恢复,而在重建时又会创建新的Fragment,就会出现页面重叠的问题。同时导致内存中出现n(n+1)/2个Fragment实例,这会大大增加内存消耗。这里可采用以下两种方案进行优化:

  1. 只在首次或没有Fragment实例存在的时候才创建新的Fragment:

    protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);

     FragmentManager fm = getSupportFragmentManager();
     // 重新关联保存的Fragment
     if (savedInstanceState == null || fm.getFragments().size() == 0) {
     	FragmentTransaction transaction = fm.beginTransaction();
     	transaction.add(R.id.container_layout, ContentFragment.newInstance(fm.getFragments().size()));
     	transaction.commit();
     }
     ...
    

    }

  2. Activity被异常关闭时,不要保存Fragment:

    @Override public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { FragmentManager fm = getSupportFragmentManager(); List fragmentList = fm.getFragments(); if (fragmentList.size() == 0) { super.onSaveInstanceState(outState, outPersistentState); } }

getSupportFragmentManager与getChildFragmentManager的区别

getSupportFragmentManager是V4包中管理Activity中的Fragment的管理器,而getChildFragmentManager是管理Fragment中的Fragment的管理器,也就是Fragment嵌套时应该使用getChildFragmentManager而不是getSupportFragmentManager。

Fragment Commit介绍

动态创建Fragment时,需要事务的配合,事务添加完成后需要提交,FragmentTransaction中提供了多个提交方法:

commit();       // 异步提交
commitNow();    // 同步提交
commitAllowingStateLoss();      // 异步提交,允许状态丢失
commitNowAllowingStateLoss();   // 同步提交,允许状态丢失

commit()和commitNow()不允许在onSaveInstanceState后调用,否则会抛出java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState异常,因为onSaveInstanceState就是用来保存Fragment的状态,onSaveInstanceState后面再次提交事务,与这些事务关联的Fragment的状态就会丢失,所以抛出了异常。在这种情况下,如果确定状态丢失不会产生影响,可使用commitAllowingStateLoss()或commitNowAllowingStateLoss()。

Fragment的回退栈

与Activity类似,Fragment也有回退栈的概念,使用addBackToStack方法即可将Fragment添加到回退栈中(注意:需要在commit之前调用),加入到回退栈中的Fragment在执行Actiivty的onBackPressed()方法时,会逐渐出栈。

执行replace操作时,如果添加到回退栈中,则被替换的Fragment的onDestroy()和onDetach()不会被调用,按返回键可返回到之前的Fragment,并从onCreateView()开始调用。如果不添加到回退栈中,则会调用被替换Fragment的onDestroy()和onDetach()方法。

==建议:== Activity中的第一个Fragment不要添加到回退栈中,否则返回时需要多执行一次onBackPressed()(具体UI表现:第一个Fragment出栈后出现空白页面);

Fragment与View的关系

与其将Fragment与Activity比较,倒不如将其与View进行比较,毕竟它们都是不可单独存在的元素,都需要Activity作为宿主。Fragment与View很像,可以动态创建,也可以在布局文件中定义,所以可视为是加入了生命周期的View. 只不过它的管理需要借助于FragmentManager和FragmentTransaction.

Fragment UI问题

在使用Fragment过程中,经常会遇到一些UI问题:

Fragment背景透明

Fragment不像Activity,没有主题的概念,如果其中加载的布局没有设置背景,默认就是透明的。如果Activity只加载了一个Fragment,看起来背景就是主题的背景,当添加多个Fragment的时候,就会发现页面出现重叠,因为背景是透明的,此时需要为Fragment中的View设置背景。

解决办法

基于减少页面重绘的原则,可使用以下方案:

  • 对于Activity中只有一个Fragment的情况,不要为Fragment中的View设置背景,直接设置Activity主题的背景;
  • 对于Activity需要多个Fragment的情况,在添加新的Fragment的时候可将底层的Fragment先隐藏;

事件穿透

Fragment中的View默认情况下是不可点击的,所以不会拦截事件。通常需要将Fragment中的根布局View的clickable属性设置为true,以屏蔽事件穿透。

页面内容丢失

有时候将当前页面切换到后台,然后恢复到前台时会发现页面内容丢失。出现这个问题,原因主要出现在onSaveInstanceState方法,有时候可能不需要保存Fragment的状态,所以在super.onSaveInstanceState之前清空了FragmentManager中的Fragment,当页面被切换到前台时,就会出现页面为空的问题。至于如何正确保存/恢复Fragment的状态,前面的页面重叠部分已提供了解决方案。

ViewPager+Fragment的正确实现

ViewPager+Fragment是一个经典组合,基本上每个APP中都会使用它。但在使用过程中有一些细节需要关注。下面以一个实例来说明一下:

fragmentList.add(new FirstFragment());
fragmentList.add(new SecondFragment());
fragmentList.add(new ThirdFragment());
viewPager.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));

public static class MyFragmentPagerAdapter extends FragmentPagerAdapter {

	public MyFragmentPagerAdapter(FragmentManager fm) {
		super(fm);
	}

	@Override
	public Fragment getItem(int position) {
		return fragmentList.get(position);
	}

	@Override
	public int getCount() {
		return fragmentList.size();
	}
}

这种写法应该很熟悉,通常情况下不会出现什么问题,但当Activity重建时,就会发现,也不会有大的问题,只是多创建了几个Fragment而已,下面看一下FragmentPagerAdapter的实现:

public Object instantiateItem(ViewGroup container, int position) {
    final long itemId = getItemId(position);
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    
    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    ...
    mCurTransaction.detach((Fragment)object);
}

可以看到Adapter在初始化Item的时候,会先查看该position是否已存在Fragment,如果存在就重新attach, 没有的时候才会调用getItem创建新的Fragment。而destroyItem的实现说明已加载的Fragment,如果不在mOffscreenPageLimit范围内,也只是detach掉了,其Fragment实例仍然是存在的,也就是说,每个position上的Fragment仅会创建一次(每个position上的getItem只被调用一次),即便是Activity被重建。也就是说,FragmentPagerAdapter内置了Fragment重新关联的功能。

再回到上面的问题,如果在onCreate中创建Fragment, 那么每次Activity重建时,都会创建新的Fragment,然而这些新创建的Fragment并没有什么用,因为FragmentPagerAdapter关联的还是之前存在的Fragment。所以推荐将Fragment的构造写在getItem中。

@Override
public Fragment getItem(int position) {
	return ContentFragment.newInstance(position) ;
}

Fragment转场动画

Fragment设置转场动画使用setCustomAnimations方法,它有2个重载方法,详细介绍如下:

/**
 * @param entry 新fragment进入的动画,只对add和replace操作有效
 * @param exit 当前fragment退出的动画,只对replace和remove操作有效
 * @return
 */
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int entry, @AnimatorRes @AnimRes int exit);

/**
 * @param enter 新页面进入的动画 
 * @param exit 当前页面退出的动画
 * @param popEnter 当前页面进入的动画
 * @param popExit 新页面退出的动画
 * @return
 */
public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter, @AnimatorRes @AnimRes int exit,
       @AnimatorRes @AnimRes int popEnter, @AnimatorRes @AnimRes int popExit)

注意:

  1. setCustomAnimations必须在事务操作(add, replace或remove)之前调用才有效;
  2. 2个参数的setCustomAnimations中的enter参数表示新fragment进入的动画,而exit参数表示当前fragment退出的动画,并不是新添加的fragment的退出动画,所以使用这个方法时,新添加的Fragment退出时没有动画效果;
  3. 4个参数的setCustomAnimations前两个参数表示页面进入时2个页面的动画效果,而后两个参数表示从回退栈中返回时2个页面的动画效果;

除了setCustomAnimations方法,Android在5.0中还扩展了转场动画,可使用以下方法实现:

/**
 * 新Fragment进入时的动画
 * @param transition
 */
public void setEnterTransition(@Nullable Object transition);

/**
 * 新Fragment退出时的动画
 * @param transition
 */
public void setReturnTransition(@Nullable Object transition);

/**
 * 新Fragment进入时当前Fragment的退出动画, 需在当前Fragment对象中设置
 * @param transition
 */
public void setExitTransition(@Nullable Object transition);

/**
 * 新Fragment退出时原Fragment的进入动画,需在原Fragment对象中设置
 * @param transition
 */
public void setReenterTransition(@Nullable Object transition);

其中的参数,Android提供了几种实现:

  • Explode: 扩散动画
  • Fade: 渐变动画
  • Slide: 平移动画

示例:

FragmentManager fm = mContext.getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragment = ContentFragment.newInstance(fm.getFragments().size());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
	fragment.setEnterTransition(new Slide(Gravity.RIGHT));
	fragment.setReturnTransition(new Slide(Gravity.RIGHT));
}
transaction.add(R.id.container_layout, fragment);
transaction.addToBackStack(null);
transaction.commit();

Fragment与Activity如何取舍

这是一个很有争议性的话题,有人认为一个APP只需要一个Activity即可,也有人认为Fragment只需要使用在需复用的页面即可。关于这个问题,可从以下几方面来探讨:

  • 复用性;
  • 开发效率;
  • 业务耦合度;

基于这三点,我们可在单个业务模块中使用单Activity+多Fragment的模式来实现,如登录模块,只需要一个LoginActivity, 其他的功能如注册,找回密码等都使用Fragment实现。一方面,Fragment比Activity更轻量,另一方面,使模块化在组件层面有了明显的区分。

总结

这只是Fragment的一个简单总结,可能还有很多细节未提及,欢迎补充。