深入理解Fragment

这里主要介绍一些对Fragment的深入理解。挑了一些个人认为比较有价值的,大部分技术博客通常都会忽略的点,列了出来,如果你对Fragment有什么其他疑惑,也可以在评论区留言。

Fragment究竟是什么呢?

Fragment简单说可以认为是一个带有生命周期的View。阅读过Fragment整个实现过程的,其实可以知道Fragment能显示其实最终还是把要显示的View add到ViewGroup中,它的生命周期回调,其实也全部来自Activity。然后又封装了一些本来Activity才有的特性,比如过渡动画,返回栈等等。你甚至可以说Activity能做的大部分事情,Fragment都能做,甚至市面有很多App是只有一个Activity或者少数几个Activity,然后绝大部分界面全部用Fragment实现的。比如:知乎。
如何知道呢?你可以使用下面的命令来查看当前手机显示的Activity名称。

Linux/Unix/Mac

adb shell dumpsys activity | grep "mFocusedActivity"

Windows

adb shell dumpsys activity | findstr "mFocusedActivity"
  • 为什么有App要这么做呢?
    用过知乎的应该会知道知乎的页面跳转逻辑很复杂。点开一个回答,然后点到回答的列表,会发现回答列表也有底栏,同时底栏的操作逻辑跟主界面时候的操作逻辑完全一致。每个底栏都有自己独立的返回栈。所以借助Fragment可以实现一些非常复杂的界面的跳转逻辑。试想如果用Activity要实现知乎这种复杂的跳转逻辑改有多复杂了。
    其次,Fragment的消耗要比Activity小,Fragment最终被处理是FragmentTransaction.commit方法。看过整个Fragment实现过程的话,会知道commit操作后最终会调用:
    private void scheduleCommit() {
        synchronized (this) {
            boolean postponeReady =
                    mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
            boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
            if (postponeReady || pendingReady) {
                mHost.getHandler().removeCallbacks(mExecCommit);
                mHost.getHandler().post(mExecCommit);
            }
        }
    }

前面对请求做了一系列处理后,最终通过scheduleCommit讲请求以Handler消息形式重新提交。直到Fragment中的View被Add进父ViewGroup都不会涉及到跨进程通信。但是Activity启动过程就不一样了,startActivity最终会通过

ActivityManagerNative.getDefault().startActivity

向ActivityManagerService所在进程发送一个跨进程通信消息,然后ActivityManagerService响应消息回传,App的ActivityThread接收到消息后打开Activity界面。整个过程是需要进行跨进程通信的,消耗当然要比Fragment高了。

  • 那是不是就没有坏处了?
    也不是,Fragment将失去Activity本身的很多特性,比如启动模式,不能直接通过ACTION启动。将失去Manifest中你能看到的Activity的特性。当然如果你不关心这些东西,那完全用Fragment替换也无妨。

究竟应该用 android.support.v4.app.Fragment还是android.app.Fragment?

android.app.Fragment是Google在Android3.0(API11)的时候推出的。现在大部分AndroidApp应该都已经最低兼容到4.0(API14)。首先从Api上来说不存在问题,虽然android.support.v4.app.Fragment兼容到v4,但对于大多数App来说兼容到14就够了。那从使用角度来看呢?

        android.support.v4.app.Fragment supportFragment = new android.support.v4.app.Fragment();
        android.support.v4.app.FragmentTransaction supportTransaction = getSupportFragmentManager().beginTransaction();
        supportTransaction.add(supportFragment, "tag");
        supportTransaction.commit();

        android.app.Fragment fragment = new android.app.Fragment();
        android.app.FragmentTransaction transaction = getFragmentManager().beginTransaction();
        transaction.add(fragment, "tag");
        transaction.commit();

使用上就是包路径不一样,然后一个是getSupportFragmentManager一个是getFragmentManager。Api层面基本算是完全兼容了。还有一点小区别就是SupportFragmentManager会多出一些方法,比如getFragments(但这个方法现在已经被标记为@RestrictTo(LIBRARY_GROUP)不推荐使用了)拿到SupportFragmentManager持有的Fragment引用。
另外就是android.app.Fragment是android.app.Activity原生支持。不需要额外引入lib库。但android.support.v4.app.Fragment只能用在android.support.v4.app.FragmentActivity中,需要额外引入supportV4包。getSupportFragmentManager方法就来自FragmentActivity。但考虑到通常Activity大家都会选择继承AppCompatActivity或者FragmentActivity,所以通常也不会有太大问题。
说了这么多,可以这么说从两个Fragment Api基本一致,使用起来也基本没太大区别。
基本表现一致,那怎么选?

还是推荐用android.support.v4.app.Fragment。

为什么?我们之前尝试过使用原生Fragment,从API层面确实没遇到太多麻烦。主要是在适配第三方库的时候遇到了很多麻烦,因为第三方库基本全部都是选择使用的v4Fragment。另外还有一个麻烦是v4是额外的包,所以v4Fragment是可以升级的,但Fragment就只能跟随手机系统版本升级了。比如v4FragmentManager后面新添加的方法registerFragmentLifecycleCallbacks可以用来很方便的监听Fragment生命周期,但是用FragmentManager就没有这个待遇了。使用v4Fragment就没有那么多麻烦了。虽然v4要额外引入supportV4包,但这个包对于大家做应用开发的话,基本是必定要引入的。所以这个引入成本是必须承受的。

为什么DialogFragment能自动恢复?

Google推荐我们使用DialogFragment。因为DialogFragment在Activity重建后依然可以自动恢复,但是Dialog就不可以。那问题来了,为什么DialogFragment就可以自动恢复,但是Dialog就不可以?
首先Dialog不能恢复,这个应该很容易理解,因为Activity都被重建了,当然不会自动恢复。除非你手动保存Dialog状态,然后在重建时候重新show出来。
那Fragment呢?在FragmentActivity源码中是这么处理onSaveInstanceState的。

    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
    ......
    }

mFragments是个FragmentController。通过saveAllState拿到了所有Fragment的State,在onSaveInstanceState的时候保存起来了。
然后我们在看下FragmentActivity的onCreate方法。

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);

        super.onCreate(savedInstanceState);

        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            mFragments.restoreLoaderNonConfig(nc.loaders);
        }
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);

            // Check if there are any pending onActivityResult calls to descendent Fragments.
            if (savedInstanceState.containsKey(NEXT_CANDIDATE_REQUEST_INDEX_TAG)) {
                mNextCandidateRequestIndex =
                        savedInstanceState.getInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG);
                int[] requestCodes = savedInstanceState.getIntArray(ALLOCATED_REQUEST_INDICIES_TAG);
                String[] fragmentWhos = savedInstanceState.getStringArray(REQUEST_FRAGMENT_WHO_TAG);
                if (requestCodes == null || fragmentWhos == null ||
                            requestCodes.length != fragmentWhos.length) {
                    Log.w(TAG, "Invalid requestCode mapping in savedInstanceState.");
                } else {
                    mPendingFragmentActivityResults = new SparseArrayCompat<>(requestCodes.length);
                    for (int i = 0; i < requestCodes.length; i++) {
                        mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]);
                    }
                }
            }
        }
    ......
    }

在saveInstanceState不为null的时候,调用了restoreAllState来恢复Fragment。

    public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
        mHost.mFragmentManager.restoreAllState(state, nonConfig);
    }

继续跟进源码可以看到具体的恢复过程。

    void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
    ......
                Fragment f = fs.instantiate(mHost, mParent, childNonConfig);
    ......
    }

代码比较多,这里只贴了实际恢复的部分,其他代码逻辑也不复杂,可以自行查看。FragmentState最终调用了Fragment.instantiate

    public Fragment instantiate(FragmentHostCallback host, Fragment parent,
        ......
            mInstance = Fragment.instantiate(context, mClassName, mArguments);
        ......
        return mInstance;
    }

最终通过class.newInstance创建出了Fragment实例。

    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.newInstance();
            if (args != null) {
                args.setClassLoader(f.getClass().getClassLoader());
                f.mArguments = 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);
        }
    }

Activity的实例化也是通过反射创建的。这也是为什么系统能创建和恢复出Activity/Fragment实例的原因。恢复后引用虽然不是同一个,但是状态是一致,所以也就会明白,为什么Activity/Fragment一定要有一个无参构造方法,对于参数必须通过onSaveInstanceState来保存和恢复了。

如何优雅的初始化Fragment?

大家可能会看到下面的代码。通常下面代码会在Activity的onCrate方法中初始化。

private SampleFragment mFragment;
private void init(){
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        mFragment = new SampleFragment();
        transaction.add(R.id.fragment_content, mFragment);
        transaction.commit();
}

有什么问题吗?粗看似乎没什么问题。但实际这是一种很不好的写法。你可以写个Demo然后利用Android Studio的dump java heap(或者在SampleFragment的构造方法中打印日志也可以)看下内存中有几个Fragment实例。然后把屏幕旋转下,触发Activity的SaveInstanceState然后再看下内存中有几个实例。你会发现内存中出现了两个SampleFragment实例。
把上面代码改造成下面这样,再看下旋转屏幕后,有几个SampleFragment实例。

private SampleFragment mFragment;
private void initFrom(){
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_content);
        if (fragment == null) {
            fragment = new SampleFragment();
        }
        mFragment = (SampleFragment) fragment;
        transaction.add(R.id.fragment_content, mFragment);
        transaction.commit();
}

上面代码无论怎么跑SampleFragment永远都只有一个实例。为什么?
好了。说了这么多,可能会很多人会说,用自己手动new Fragment的写法并没有遇到过问题,怎么回事?主要是因为现在很多App都没有适配横屏模式,很多都锁死只能竖屏,然后现在的Android手机普遍内存都比较大,也较少的出现系统内存不足,触发onSaveInstanceState的情况。所以问题没有发生并不是说这样是对的。
另外,你会发现无论你用add还是replace至少加一个tag或者Rid。为什么?其实就是为了让你重新找回Fragment实例引用的。

为什么使用FragmentTransaction的add(Fragment fragment, String tag);方法Fragment不会显示?

前面说到Fragment在add或者replace的时候一定需要指定tag和RId中的至少一个。那问题来了,假如我调用add不指定RId的方法会怎么样?
实际测试下会发现界面没有任何变化,如果你打印Fragment的生命周期的话会发现Fragment的生命周期是正常的,但实际没有显示。我们来看下Fragment实际是如何处理View的显示的。详细的Fragment处理过程比较复杂,回头有空了会写一篇详细的文章介绍Fragment是如何显示在Activity中的,这里先略过。直接在android.support.v4.app.FragmentManager类中搜索:"case Fragment.CREATED:"找到下面代码

                case Fragment.CREATED:
                    if (newState > Fragment.CREATED) {
                        if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
                        if (!f.mFromLayout) {
                            ViewGroup container = null;
                            if (f.mContainerId != 0) {
                                if (f.mContainerId == View.NO_ID) {
                                    throwException(new IllegalArgumentException(
                                            "Cannot create fragment "
                                                    + f
                                                    + " for a container view with no id"));
                                }
                   ......

注意到if (f.mContainerId != 0) 这一行,假如mContainerId为0则container就null。然后

                            if (f.mView != null) {
                                ......
                                if (container != null) {
                                    container.addView(f.mView);
                                }

最终调用了container的addView方法让View实际添加了ViewGroup中。所以到这里就清楚了假如addView没有指定id,那么container就是null,container是null那么即使onCreateView返回了真实有效的View也一样没用,因为没有地方可以给它add,当然也就不会显示。

为什么DialogFragment也不需要指定id,但是DialogFragment就可以正常显示?

DialogFragment大家通常可能会有类似下面的代码。

        new SampleDialogFragment().show(getSupportFragmentManager(), "dialog");

问题来了,我们确实没有给DialogFragment指定id,那为什么DialogFragment还能正常显示?跟进show方法

    public void show(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }

DialogFragment在show的时候调用的是无Rid的add方法。不开心了,凭什么DialogFragment能显示?
先不急,我们把前面的SampleFragment继承从Fragment改成DialogFragment,然后也调用无RId的add方法看看会怎么样。
实际测试后会发现继承修改成DialogFragment后,SampleFragment也能显示了。
问题来了,为什么呢?明明没有指定ID,View被搞哪里去了?
弄清楚这个问题前,我们先想下

  • 怎么显示一个View?
    把View添加进ViewGroup
  • 那ViewGroup从哪里来?
    顺着setContent方法一路找下去就会发现,Activity被显示是因为Window,最终的根ViewGroup来自Window。可以理解为有Window就可以显示View。(一个Activity可以有多个Window,有兴趣的可以搜下相关文档,怎么创建View,怎么在Window中显示View,这里不做详细介绍)
    到这里一下子就柳暗花明了,一个View能否显示要看是否被添加进Window里了。实际负责显示的是Window。DialogFragment虽然没有被add进父ViewGroup,只要它被add进Window其实一样可以显示。那DialogFragment是不是这样做了呢?DialogFragment源码其实并不多,自己可以详细一点点看一遍,这里只说重点部分。
    public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
        if (!mShowsDialog) {
            return super.getLayoutInflater(savedInstanceState);
        }

        mDialog = onCreateDialog(savedInstanceState);

        if (mDialog != null) {
            setupDialog(mDialog, mStyle);

            return (LayoutInflater) mDialog.getContext().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
        }
        return (LayoutInflater) mHost.getContext().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);
    }

这里LayoutInflater被复写成了Dialog的LayoutInflater。

    @NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        return new Dialog(getActivity(), getTheme());
    }

而且onCreateDialog方法默认会创建一个Dialog,而且还加了NonNull的注解。

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        if (!mShowsDialog) {
            return;
        }

        View view = getView();
        if (view != null) {
            if (view.getParent() != null) {
                throw new IllegalStateException(
                        "DialogFragment can not be attached to a container view");
            }
            mDialog.setContentView(view);
        }
        final Activity activity = getActivity();
        if (activity != null) {
            mDialog.setOwnerActivity(activity);
        }
        mDialog.setCancelable(mCancelable);
        mDialog.setOnCancelListener(this);
        mDialog.setOnDismissListener(this);
        if (savedInstanceState != null) {
            Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
            if (dialogState != null) {
                mDialog.onRestoreInstanceState(dialogState);
            }
        }
    }

这里一下就完全清楚了,View被添加到Dialog里。你只要继承了DialogFragment,那么虽然你add没有指定RId,但是View会被set到Dialog里,所以最终显示是在Dialog中。也就是说DialogFragment实际显示还是Dialog,但是利用了Fragment的生命周期管理来实现一些比如重建之类的工作。说到这里是不是对Fragment的add无Rid方法有了一个更深入的理解?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容