1. 程式人生 > >為什麼需要一個無參建構函式的Fragment

為什麼需要一個無參建構函式的Fragment

之前在用Android Lint時發現裡面有個選項“Fragment not instantiatable”

這裡寫圖片描述

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; i<fms.mActive.length; i++) {
       FragmentState fs = fms.mActive[i];
       if (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()來獲取傳入的資料。