为什么需要一个无参构造函数的Fragment

之前在用Android Lint时发现里面有个选项“Fragment not instantiatable”

为什么需要一个无参构造函数的Fragment_第1张图片

Every fragment must have an empty constructor, so it can be instantiated when restoring its activity’s state. It is strongly recommended that subclasses do not have other constructors with parameters, since these constructors will not be called when the fragment is re-instantiated; instead, arguments can be supplied by the caller with setArguments(Bundle) and later retrieved by the Fragment with getArguments().
  Fragment文档中说明,自定义Fragment时强烈建议我们不使用有参构造函数,以便可以再次实例化。这可是Fragment的一大火坑呀,我们在写一个类的时候,通常都是通过有参的构造函数传入需要的参数,Fragment却反其道而行之,相信很多人已经跳进了这个火坑。虽然文档上说了实例化是可能会出现问题,但是并没有具体指出是哪一行代码出错,既然这样,只有自己动手,到源码中去一探究竟。在Fragment的源码中,发现instantiate(Context context, String fname, @Nullable Bundle args)这个方法会抛出找不到非空构造函数的异常:

  public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
      try {
          Class clazz = sClassMap.get(fname);
          if (clazz == null) {
              // Class not found in the cache, see if it's real, and try to add it
              clazz = context.getClassLoader().loadClass(fname);
              if (!Fragment.class.isAssignableFrom(clazz)) {
                  throw new InstantiationException("Trying to instantiate a class " + fname
                          + " that is not a Fragment", new ClassCastException());
              }
              sClassMap.put(fname, clazz);
          }
          Fragment f = (Fragment) clazz.getConstructor().newInstance();
          if (args != null) {
              args.setClassLoader(f.getClass().getClassLoader());
              f.setArguments(args);
          }
          return f;
      } catch (ClassNotFoundException e) {
          throw new InstantiationException("Unable to instantiate fragment " + fname
                  + ": make sure class name exists, is public, and has an"
                  + " empty constructor that is public", e);
      } catch (java.lang.InstantiationException e) {
          throw new InstantiationException("Unable to instantiate fragment " + fname
                  + ": make sure class name exists, is public, and has an"
                  + " empty constructor that is public", e);
      } catch (IllegalAccessException e) {
          throw new InstantiationException("Unable to instantiate fragment " + fname
                  + ": make sure class name exists, is public, and has an"
                  + " empty constructor that is public", e);
      } catch (NoSuchMethodException e) {
          throw new InstantiationException("Unable to instantiate fragment " + fname
                  + ": could not find Fragment constructor", e);
      } catch (InvocationTargetException e) {
          throw new InstantiationException("Unable to instantiate fragment " + fname
                  + ": calling Fragment constructor caused an exception", e);
      }
  }

  看到上面第13行代码可知,Fragment的实例化是通过类对象的getConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,在getConstructor()中没有传入参数,所以当我们自定义一个有参的构造函数的Fragment时,当instantiate(Context context, String fname, @Nullable Bundle args)这个方法被调用时,就无法新建一个Fragment。现在找到了报错的根源,但是具体是在Fragment的哪一个时期还没有找到,由于Fragment是由FragmentManager管理的,进入FragmentManager这个类中发现在restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig)这个方法中调用了FragmentState的instantiate():

void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
......
   // Build the full list of active fragments, instantiating them from
   // their saved state.
   mActive = new SparseArray<>(fms.mActive.length);
   for (int i=0; iif (fs != null) {
           FragmentManagerNonConfig childNonConfig = null;
           if (childNonConfigs != null && i < childNonConfigs.size()) {
               childNonConfig = childNonConfigs.get(i);
           }
           Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig);//关键行
           if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
           mActive.put(f.mIndex, f);
           // Now that the fragment is instantiated (or came from being
           // retained above), clear mInstance in case we end up re-restoring
           // from this FragmentState again.
           fs.mInstance = null;
       }
......
}

  上面的第13行代码中,FragmentState的instantiate()会调用到Fragment的instantiate(Context context, String fname, @Nullable Bundle args),从第3、4行的注释基本可以猜想,这个方法应该是重建Fragment时调用的。具体是不是这样呢,我们继续验证。接下来发现,在Fragment的restoreChildFragmentState(@Nullable Bundle savedInstanceState, boolean provideNonConfig)这个方法中调用了FragmentManager的restoreAllstate(),在Fragment的OnCreate()中又调用的restoreChildFragmentState():

public void onCreate(@Nullable Bundle savedInstanceState) {
    mCalled = true;
    final Context context = getContext();
    final int version = context != null ? context.getApplicationInfo().targetSdkVersion : 0;
    if (version >= Build.VERSION_CODES.N) {
        restoreChildFragmentState(savedInstanceState, true);
        if (mChildFragmentManager != null
                && !mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) {
            mChildFragmentManager.dispatchCreate();
        }
    }
}

void restoreChildFragmentState(@Nullable Bundle savedInstanceState, boolean provideNonConfig) {
    if (savedInstanceState != null) {
        Parcelable p = savedInstanceState.getParcelable(Activity.FRAGMENTS_TAG);
        if (p != null) {
            if (mChildFragmentManager == null) {
                instantiateChildFragmentManager();
            }
            mChildFragmentManager.restoreAllState(p, provideNonConfig ? mChildNonConfig : null);
            mChildNonConfig = null;
            mChildFragmentManager.dispatchCreate();
        }
    }
}

再回头看一下Fragment的instantiate()这个函数:

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
......
    Fragment f = (Fragment) clazz.getConstructor().newInstance();
    if (args != null) {
       args.setClassLoader(f.getClass().getClassLoader());
       f.setArguments(args);
    }
......
}

可以得出这样的结论:
  当Fragment因为某种原因,例如旋转屏幕时Activity重建,此时Fragment也会重建,然后通过onCreate( Bundle savedInstanceState)传入之前保存的数据savedInstanceState,然后通过反射无参构造实例化一个新的Fragment,并且给mArgments初始化为原先的值,如果Fragment的构造函数不是无参的,就无法实例化Fragment,进而导致程序崩溃。
  因此,在Fragment中,传递数据的正确方式应该是通过setArguments(Bundle) 然后在需要的时候通过getArguments()来获取传入的数据。

你可能感兴趣的:(Android)