美文网首页提升转型Android Fragment全解
为什么使用Fragment时必须提供一个无参的构造函数?

为什么使用Fragment时必须提供一个无参的构造函数?

作者: m1Ku | 来源:发表于2018-11-05 23:22 被阅读76次

    问题

    最近在线上bugly看到一个近两个版本的出现的一个bug:

    #5712 java.lang.NoSuchMethodException
    <init> []
    com.jess.arms.a.c.onCreate(BaseActivity.java:86)
    java.lang.RuntimeException:Unable to start activity 
    ComponentInfo{xxxx.VehicleDetailActivity}: 
    android.support.v4.app.Fragment$InstantiationException: 
    Unable to instantiate fragment xxxx.mvp.ui.fragment.c: 
    could not find Fragment constructor
    

    这个bug是方法找不到异常,因为项目中用了mvparms框架,刚开始看到异常指向他的BaseActivity类,还以为是框架出了问题。但是后面注意到是项目的VehicleDetailActivity报的could not find Fragment constructor,即fragment构造器找不到的异常。于是马上将问题定位到了该类新添加的一个DialogFragment上,先看一下这个类的构造方法:

    public MorePlanFragment(String modelId, HashMap<String, Integer> map) {
        this.modelId = modelId;
        this.posMap = map;
        this.tabPos = map.get(POS_MAP_TAB);
    }
    

    这个MorePlanFragment使用了有参构造函数,而问题就出现在这里。

    分析

    既然报的找不到构造方法的错误,我们先来看一下Fragment的构造函数:

    /**
     * Default constructor.  <strong>Every</strong> 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 {@link #setArguments}
     * and later retrieved by the Fragment with {@link #getArguments}.
     *
     * <p>Applications should generally not implement a constructor. Prefer
     * {@link #onAttach(Context)} instead. It is the first place application code can run where
     * the fragment is ready to be used - the point where the fragment is actually associated with
     * its context. Some applications may also want to implement {@link #onInflate} to retrieve
     * attributes from a layout resource, although note this happens when the fragment is attached.
     */
    public Fragment() {
    }
    

    构造函数上有一段注释:

    默认构造器。
    每一个Fragment必须有一个无参的构造函数,以便当Activity恢复状态时fragment可以实例化。
    强烈建议fragment的子类不要有其他的有参构造函数,因为当fragment重新实例化时不会调用这些有参构造函数;
    如果要传值应该使用setArguments方法,在需要获取这些值时调用getArguments方法。
    

    这一段注释明确的告诉我们使用有参构造函数会出问题,建议使用无参构造函数,但是并没有告诉我们具体是哪里的问题。我们在Fragment中搜索could not find Fragment constructor这个异常,发现是在instantiate方法中抛出的。

    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);
                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);
        }
    }
    

    看上面的代码我们可以知道,Fragment的实例化是通过调用类对象的getConstructor()方法获取构造器对象并调用其newInstance()方法创建对象的。此时还会将args参数设置给Fragment。现在找到了具体报错的地方,但是这个方法是在哪里调用触发的呢?在Fragment没有找到调用的地方,由于Fragment是由FragmentManager管理的,在该类发现是在restoreAllState方法中调用的。

    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);
                    }
                    ViewModelStore viewModelStore = null;
                    if (viewModelStores != null && i < viewModelStores.size()) {
                        viewModelStore = viewModelStores.get(i);
                    }
                    Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig,
                            viewModelStore);
                    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;
                }
            }
        ...   
        }
        
    

    这方法名意为恢复所有的状态,而其中注释为创建激活Fragment的列表,并将他们从保存的状态中实例化。这个方法应该是Fragment重新实例化时调用的方法。该方法在Fragment的restoreChildFragmentState被调用。

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

    restoreChildFragmentState方法又在Fragment的onCreate方法中调用,这里将保存的savedInstanceState状态又传递给了restoreChildFragmentState以完成Fragment的重新实例化。

    @CallSuper
    public void onCreate(@Nullable Bundle savedInstanceState) {
        mCalled = true;
        restoreChildFragmentState(savedInstanceState);
        if (mChildFragmentManager != null
                && !mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) {
            mChildFragmentManager.dispatchCreate();
        }
    }
    

    结论

    经过以上的分析,我们就知道了为什么这个错误出在了Fragment的有参构造函数上。因为当Fragment因为某种原因重新创建时,会调用到onCreate方法传入之前保存的状态,在instantiate方法中通过反射无参构造函数创建一个Fragment,并且为Arguments初始化为原来保存的值,而此时如果没有无参构造函数就会抛出异常,造成程序崩溃。
    所以Fragment的构造函数以及参数传递正确使用方式为如下:

    public static MorePlanFragment newInstance(String modelId,HashMap<String, Integer> map) {
        Bundle args = new Bundle();
        args.putString("modelId",modelId);
        args.putSerializable("map",map);
        MorePlanFragment fragment = new MorePlanFragment();
        fragment.setArguments(args);
        return fragment;
    }
    

    相关文章

      网友评论

        本文标题:为什么使用Fragment时必须提供一个无参的构造函数?

        本文链接:https://www.haomeiwen.com/subject/mnaexqtx.html