深入理解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方法有了一个更深入的理解?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容