Fragment是Android开发中常用的组件之一,也是最容易出问题的组件,为了更好地使用它,对此进行一个简单的总结。
说明: 由于v4包中的Fragment具有更好的兼容性,且可随时更新到指定版本,所以本文的讨论仅限v4包中的Fragment。
使用Fragment
使用Fragment的方式有2种:
-
在布局文件中使用标签,将其中的name属性值设置为需要加载的Fragment的全路径;
当系统在创建Activity时,会实例化布局中指定的Fragment,并调用它的onCreateView方法,以返回的View来替换元素。
- 动态创建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的生命周期要复杂很多,使用官方的一张图来展示:
下面对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的多次加载。
@Overridepublic 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生命周期有着直接的关系,如图所示:
从图上可以看出,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需要添加条件判断:
@Overridepublic void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isResumed() && isVisibleToUser) { // 页面被显示 }}复制代码
所以,监听Fragment页面的显示,除了监听Fragment生命周期中的onPause/onResume方法,还需要监听onHiddenChanged方法和setUserVisibleHint。
在统计Fragment页面的显示时长时,需要综合考虑这几个方法,具体见Android无埋点方案实践项目——中对Fragment生命周期的监听过程。
Fragment懒加载
懒加载(或者叫延迟加载),也就是延迟数据的请求过程。常用于ViewPager+Fragment模式中,不同的Fragment可能使用不同的接口,在页面显示的时候,可能会同时请求offscreenPageLimit 个接口,导致页面出现卡顿。为了解决这种问题,可延迟未显示的Fragment的数据请求过程,即在Fragment显示时,再进行网络请求。Fragment中的setUserVisibleHint方法在ViewPager中的Fragment显示时被调用,所以我们可在其中实现数据的请求。
@Overridepublic void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser && !dataRequested) { requestData(); }}复制代码
getContext()与getActivity()的区别
关于这两者的区别,可从源码来看:
@Nullablepublic Context getContext() { return mHost == null ? null : mHost.getContext();} @Nullablefinal 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{ public HostCallbacks() { super(FragmentActivity.this /*fragmentActivity*/); } ...}复制代码
就目前源码的实现来说,这两者没有任何区别,引用的都是它所在的Activity的实例,但是它提供的公开的构造方法的实现却说明: getContext()为空的可能性能更大。 所以,在Fragment中获取Context实例时最好使用getActivity()。
getActivity返回null
在使用Fragment的过程中,getActivity()为null的异常应该是最常见的。其根本原因:Fragment与之前关联的Activity失去了联系!
使用Fragment时,我们的Activity继承的都是FragmentActivity, FragmentActivity在被异常关闭时会保存已加载的Fragment,具体如下:
@Overrideprotected 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(), 就会出现空指针异常。
解决方案
- 在Fragment的onDetach中移除延时任务:
这是从根本上杜绝getActivity为空的方案。日常开发中,使用的延时任务最多的情况莫过于网络请求和Handler。 对于网络请求,封装的时候最好考虑Activity/Fragment的生命周期, 在onDestroy和onDetach取消网络请求,上传/下载等大数据量的网络请求,可引用ApplicationContext,放在后台服务进行执行。对于Handler, 只需要在onDetach中清除任务即可:
@Overridepublic void onDetach() { super.onDetach(); handler.removeCallbacksAndMessages(null);}复制代码
- 在onAttach中保存Activity的引用;
通过保存的Activity实例替代getActivity。Activity虽然重建了,但之前的实例因为Fragment的持有而不会被内存清理,会造成短暂性的内存泄漏。
@Overridepublic void onAttach(Context context) { super.onAttach(context); this.mContext = context;}复制代码
具体使用哪种方法,看自己的需求,如果项目框架良好,团队又有良好的编程规范,自然是推荐第一种。否则还是使用第二种方案,虽然会造成短暂性的内存泄漏,倒也不会有什么大的影响。
Fragment页面重叠问题
FragmentActivity默认情况下在异常销毁时会保存Fragment,并在onCreate中进行恢复,而在重建时又会创建新的Fragment,就会出现页面重叠的问题。同时导致内存中出现n(n+1)/2个Fragment实例,这会大大增加内存消耗。这里可采用以下两种方案进行优化:
-
只在首次或没有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(); } ...复制代码
}
-
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;}@Overridepublic 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中。
@Overridepublic 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)复制代码
注意:
- setCustomAnimations必须在事务操作(add, replace或remove)之前调用才有效;
- 2个参数的setCustomAnimations中的enter参数表示新fragment进入的动画,而exit参数表示当前fragment退出的动画,并不是新添加的fragment的退出动画,所以使用这个方法时,新添加的Fragment退出时没有动画效果;
- 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的一个简单总结,可能还有很多细节未提及,欢迎补充。