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空指针,因为交互的接口实例为空。

分析源码

因为接口实例的注入在FragmentPagerAdaptergetItem中完成,我们发现问题的入口就是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问题,分析情景如下:

  1. 第一次构建FragmentPagerAdapter,FragmetManager中Fragment集合为空,需要getItem执行返回Fragment实例,UI得以显示。
  2. 用户离开当前Activity,onSaveInstanceState保存操作被回调执行,Fragment被FragmetManager控制器调起保存操作,保存内部状态。
  3. 用户回到Activity,备忘录的数据恢复操作开始执行,就是onCreate(Bundle savedInstanceState)方法中Bundle有被保存下来的数据,FragmetManager被恢复成之前的状态,Fragment容器中有内容。
  4. 再次构建FragmentPagerAdapter,但是通过FragmetManager实例能够findFragmentByTag得到Fragment实例。
  5. 整个恢复过程由源码配合完成,我们的FragmentPagerAdapter子类的getItem方法没有被调起执行。
  6. 最后当所有得源码都执行完,执行到我们所写的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的应用。

    水平有限,错误之处还望大家多多指正

原创粉丝点击