深入理解Fragment

作者: Jerry2015 | 来源:发表于2017-08-22 14:39 被阅读103次

    这里主要介绍一些对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方法有了一个更深入的理解?

    相关文章

      网友评论

        本文标题:深入理解Fragment

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