Android开发-Fragment的生命周期和重建问题解决方案
来源:互联网 发布:百视通网络机顶盒破解 编辑:程序博客网 时间:2024/05/20 18:16
介绍
众所周知在Android开发中Fragment的生命周期非常复杂,复杂得甚至让Square公司提出了我为什么主张反对使用Android Fragment转而提倡使用自定义View组合替代Fragment。但是没办法公司项目还是使用了很多Fragment嵌套。遇到问题还是需要自己去处理的。
这里以Fragment的状态保存和恢复(即重建)来讨论一些关于Fragment的生命周期问题。
有隐患的代码
不知道各位有没有写过下面,类似的代码。
public class TabActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tab); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); ViewPager mViewPager = (ViewPager) findViewById(R.id.container); mViewPager.setAdapter(mSectionsPagerAdapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(mViewPager); } //交互的接口定义 public interface OnInteractionListener { void action(String action); } public static class PlaceholderFragment extends Fragment { private static final String ARG_SECTION_NUMBER = "section_number"; public PlaceholderFragment() { } private OnInteractionListener listener; public void setListener(OnInteractionListener listener) { this.listener = listener; } public static PlaceholderFragment newInstance(int sectionNumber) { PlaceholderFragment fragment = new PlaceholderFragment(); Bundle args = new Bundle(); args.putInt(ARG_SECTION_NUMBER, sectionNumber); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_tab, container, false); TextView textView = (TextView) rootView.findViewById(R.id.section_label); textView.setText( getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER))); //使用接口 和Activity交互 listener.action("fragment response view is build"); return rootView; } } public class SectionsPagerAdapter extends FragmentPagerAdapter { public SectionsPagerAdapter(FragmentManager fm) { super(fm); } /** * 在get方法中 返回Fragment实例 并设置接口回调 * @param position * @return */ @Override public Fragment getItem(int position) { Logger.d("position = "+position); PlaceholderFragment fragment = PlaceholderFragment.newInstance(position + 1); //注入接口实例 打印输出 fragment.setListener(new OnInteractionListener() { @Override public void action(String action) { Logger.d(action); } }); return fragment; } @Override public int getCount() { return 3; } @Override public CharSequence getPageTitle(int position) { switch (position) { case 0: return "SECTION 1"; case 1: return "SECTION 2"; case 2: return "SECTION 3"; } return null; } }}
基本思路是使用ViewPager显示Fragment,使用FragmentPagerAdapter管理Fragment,并在创建的地方加入外部接口实例注入过程。
这段代码看起来没什么问题,跑起来也没有问题。但是有很大隐患。
首先代码是使用Android Studio自动生成的,操作如下。我在原有基础上加上了Fragment和Activity的通过接口交互的操作(有注释的部分代码)。
如果按上一篇提到的开发者选项->开启不保留活动
测试切换后的Activity恢复重建。然后程序就崩溃了!!,原因竟然是NullPointerException空指针,因为交互的接口实例为空。
分析源码
因为接口实例的注入在FragmentPagerAdapter
的getItem
中完成,我们发现问题的入口就是FragmentPagerAdapter。
FragmentPagerAdapter:只贴出相关源码
public abstract class FragmentPagerAdapter extends PagerAdapter { private final FragmentManager mFragmentManager; public FragmentPagerAdapter(FragmentManager fm) { mFragmentManager = fm; } /** * Return the Fragment associated with a specified position. */ public abstract Fragment getItem(int position); @Override public Object instantiateItem(ViewGroup container, int position) { if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } final long itemId = getItemId(position); // Do we already have this fragment? String name = makeFragmentName(container.getId(), itemId); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } else { fragment = getItem(position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); } return fragment; }private static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; } }
从源码中发现instantiateItem
中调用getItem(int
position)是有条件的。只有当上一步尝试在FragmentManager
抽象类中查找不到特定的Fragment时才会调用,去子类中获取Fragment实例。
而FragmentManager
是由外部注入的,注入的是抽象定义没有实现代码,且findFragmentByTag
方法是怎么样根据name去查找Fragment的。
我们得到以下两个问题:
1:FragmentManager的实例是什么?
2:findFragmentByTag查找的是什么?
FragmentActivity管理Fragment
查看源码我们来到FragmentActivity,它的主要功能就是对嵌套在他内部的Fragment进行管理。
FragmentActivity:
public class FragmentActivity {final FragmentController mFragments = FragmentController.createController(new HostCallbacks());//FragmentActivity对内部状态的保存操作@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Parcelable p = mFragments.saveAllState(); if (p != null) { outState.putParcelable(FRAGMENTS_TAG, p); } } }/** * Return the FragmentManager for interacting with fragments associated * with this activity. */ public FragmentManager getSupportFragmentManager() { return mFragments.getSupportFragmentManager(); }
由以上这些代码,我们可以知道对于Fragment的控制,FragmentActivity其实是通过FragmentController
实现的
FragmentController
public class FragmentController { /** * Returns a {@link FragmentManager} for this controller. */ public FragmentManager getSupportFragmentManager() { return mHost.getFragmentManagerImpl(); } }
通过这行代码我们最终来到了目的地
FragmentManagerImpl:它是FragmentManager抽象类的具体实现类
final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory { ArrayList<Fragment> mActive; ArrayList<Fragment> mAdded;@Override public Fragment findFragmentByTag(String tag) { if (mAdded != null && tag != null) { // First look through added fragments. for (int i=mAdded.size()-1; i>=0; i--) { Fragment f = mAdded.get(i); if (f != null && tag.equals(f.mTag)) { return f; } } } if (mActive != null && tag != null) { // Now for any known fragment. for (int i=mActive.size()-1; i>=0; i--) { Fragment f = mActive.get(i); if (f != null && tag.equals(f.mTag)) { return f; } } } return null; }}
findFragmentByTag:是从内部持有的Fragment集合中根据tag名称查找的。
Fragment的备忘录模式应用
我们通过源码回答了前面提出的两个问题,但是还是没有弄清楚崩溃的原因。
但是我们发现ArrayList<Fragment> mActive
内部变量。根据对备忘录模式的理解,肯定存在对该变量的备忘录封装操作。
回到前面的部分FragmentActivity对内部状态的保存操作
我们看看FragmentManagerImpl关于备忘录模式的实现,篇幅有限只看保存操作。
FragmentManagerImpl:
Parcelable saveAllState() { //省略其他操作 只看关键代码 // First collect all active fragments. int N = mActive.size(); FragmentState[] active = new FragmentState[N]; for (int i=0; i<N; i++) { Fragment f = mActive.get(i); FragmentState fs = new FragmentState(f); active[i] = fs; } FragmentManagerState fms = new FragmentManagerState(); fms.mActive = active; fms.mAdded = added; fms.mBackStack = backStack; return fms;}
Fragment的备忘录对象实现:
final class FragmentState implements Parcelable { final String mClassName; final int mIndex; final boolean mFromLayout; final int mFragmentId; final int mContainerId; final String mTag; final boolean mRetainInstance; final boolean mDetached; final Bundle mArguments; final boolean mHidden; Bundle mSavedFragmentState; Fragment mInstance; public FragmentState(Fragment frag) { mClassName = frag.getClass().getName(); mIndex = frag.mIndex; mFromLayout = frag.mFromLayout; mFragmentId = frag.mFragmentId; mContainerId = frag.mContainerId; mTag = frag.mTag; mRetainInstance = frag.mRetainInstance; mDetached = frag.mDetached; mArguments = frag.mArguments; mHidden = frag.mHidden; } }
FragmentManager的备忘录实现:
final class FragmentManagerState implements Parcelable { FragmentState[] mActive; int[] mAdded; BackStackState[] mBackStack; public FragmentManagerState() { } }
所以基于以上的Fragment的备忘录模式的实现,Android系统能够保证当FragmentActivity被销毁后,重新返回时的重建。恢复到离开时的状态。
情景分析
在看了这么多源码之后,重新返回思考刚才的重建后崩溃NPE问题,分析情景如下:
- 第一次构建FragmentPagerAdapter,FragmetManager中Fragment集合为空,需要getItem执行返回Fragment实例,UI得以显示。
- 用户离开当前Activity,onSaveInstanceState保存操作被回调执行,Fragment被FragmetManager控制器调起保存操作,保存内部状态。
- 用户回到Activity,备忘录的数据恢复操作开始执行,就是
onCreate(Bundle savedInstanceState)
方法中Bundle有被保存下来的数据,FragmetManager被恢复成之前的状态,Fragment容器中有内容。 - 再次构建FragmentPagerAdapter,但是通过FragmetManager实例能够
findFragmentByTag
得到Fragment实例。 - 整个恢复过程由源码配合完成,我们的FragmentPagerAdapter子类的getItem方法没有被调起执行。
- 最后当所有得源码都执行完,执行到我们所写的Fragment子类,一个没有被恢复的对象也就是我们由外部注入的接口实例,肯定为空,然后就发生NullPointerException空指针崩溃。
问题的解决方案
知道了问题发生的原因,解决方案的思路就很清晰,因为数据的重建恢复完全由源码完成,我们所能做的就是配合源码的执行,在适当的生命周期添加适当的代码。
改变外部对象的注入方式
配合Fragment的生命周期,改变外部对象的注入方式,比如这样
Activity实现接口
public class TabActivity extends AppCompatActivity implements OnInteractionListener {}
Fragment从生命周期中获取外部对象
public static class PlaceholderFragment extends Fragment { @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnInteractionListener){ listener= (OnInteractionListener) context; } }
优点:可以很简单的实现解决问题,代码量比较少。
缺点:Fragmen和Activity的接口实现方式隐式绑定,不容易理解。且对于ViewPager这样的,还需要再添加代码集中控制Fragment集合。
继承FragmentPagerAdapter添加新方法
根据FragmentPagerAdapter的源码,重新定义创建和绑定过程
/** * Created by LiCola on 2017/6/5. * 按照FragmentActivity和FragmentPagerAdapter 对子Fragment的生命周期和重建的顺序 特性 * 抽象父类统一提供功能和抽象方法。 * 建议项目中所有的有关FragmentPagerAdapter 都直接继承该抽象类,或按照该思路管理Fragment */public abstract class FragmentPagerRebuildAdapter<T extends Fragment> extends FragmentPagerAdapter { protected final int pageSize; /** * 泛型,让子类的功能更灵活丰富 */ private List<T> fragments; /** * 外部指定数量 * @param fm * @param pageSize */ public FragmentPagerRebuildAdapter(FragmentManager fm, int pageSize) { super(fm); this.pageSize = pageSize; fragments = new ArrayList<>(pageSize); } /** * 返回集合 * @return */ public List<T> getFragmentList() { return fragments; } /** * 根据传入位置 得到Fragment */ public T getFragmentByPosition(int position) { if (fragments==null||fragments.isEmpty()) { return null; } if (position >= fragments.size()) { return null; } return fragments.get(position); } /** * 得到ViewPager当前页的Fragment */ public T getFragmentByCurrentItem(ViewPager viewPager) { if (viewPager == null) { return null; } return getFragmentByPosition(viewPager.getCurrentItem()); } /** * 根据位置参数创建并返回一个Fragment实例 * 该方法FragmentActivity在新建Fragment时调用,销毁后重建时不会调用 * * @param position 位置参数 * @return 创建好的Fragment实例 */ protected abstract T createFragment(int position); /** * 操作某个Fragment,设置或绑定操作或数据 * 该方法,新建或重建都调用 * * @param fragment 对某个位置的Fragment * @param position 某个位置的位置参数 */ protected abstract void bindFragment(T fragment, int position); @Override public Object instantiateItem(ViewGroup container, int position) { Object object = super.instantiateItem(container, position); bindFragment((T) object, position); fragments.add((T) object); return object; } @Override public Fragment getItem(int position) { Fragment fragment = createFragment(position); if (fragment == null) { throw new UnsupportedOperationException("createFragment(position=" + position + " 没有返回Fragment实例),检查代码确保createFragment方法覆盖所有position"); } return fragment; } @Override public int getCount() { return pageSize; }}
优点:逻辑清晰,子类只需要根据提示实现抽象方法,且提供了容器控制。
缺点:需要确定Fragment数量,不能改变数量,对现有代码修改比较大。
总结
- 本文从问题出发,一步步探索源码,得到代码执行的内部逻辑。理解源码的后,能够清晰的情景分析,提出解决方案。
- 对于解决方案,在理解问题发生的原因后,方案有多种,这里只是简单的讨论两种解决方案。
讨论分析了备忘录模式应用在FragmentActivity和Fragment的应用。
水平有限,错误之处还望大家多多指正
- Android开发-Fragment的生命周期和重建问题解决方案
- Android开发体系--Activity和Fragment的生命周期
- android开发-Fragment生命周期
- Android fragment嵌套fragment问题解决方案
- 【Android 开发教程】Fragment的生命周期
- android开发中fragment的生命周期
- Android开发:Fragment不同操作的生命周期
- Android开发:Fragment不同操作的生命周期
- Android开发:Fragment不同操作的生命周期
- Android开发之Fragment的生命周期
- Android开发:Fragment不同操作的生命周期
- Android开发:Fragment不同操作的生命周期
- Android开发:Fragment不同操作的生命周期
- Android开发:Fragment不同操作的生命周期
- Android Fragment集成高德地图黑屏的问题解决方案
- Android Activity和Fragment的生命周期图
- Android Activity和Fragment的生命周期
- android 中activity 和Fragment 的生命周期
- servlet基础
- 做了个噩梦被吓醒了
- Vue.js实战之使用Vuex + axios发送请求详解
- 音视频同步原理解析;音频编码和解码原理
- C/C++指针和Java引用
- Android开发-Fragment的生命周期和重建问题解决方案
- [分块][哈希] [Romanian IOI 2017 Selection #6] Jolteon && [LOJ #6187] Odd
- IPV4须知
- Eclipse无法启动报An internal error occurred during: "reload maven project". java.lang.NullPointerExceptio
- 解决fatal error: bsoncxx/json.hpp: 没有那个文件或目录
- Arrays.binarySearch
- python 列表
- 使用c++11新特性实现线程池
- 微信支付类