阅读 1471

[译] android应用开发者,你们真的了解Fragment的生命周期吗?

如果你问我Android系统框架里哪三个问题最复杂,那么Fragment的生命周期肯定会成为其中的一个。

Android Framwork开发人员中的传奇人物Dianne Hackborn在2010年将Fragment引入了Android,他在提交信息中写道:

Author: Dianne Hackborn
Date: Thu Apr 15 14:45:25 2010 -0700
Introducing Fragment.
Basic implementation of an API for organizing a single activity into separate, discrete pieces. Currently supports adding and removing fragments, and performing basic lifecycle callbacks on them.

“将单一的Activity拆分成多个独立的部件”的想法非常好。 然而,从今天Fragment的的实际使用效果来看,这一API的实现和演变并不理想。

截至目前,Fragment是Android系统框架里最具有争议的Android API之一。 许多专业的Android开发人员甚至都不会在他们的项目中使用Fragments。 不是因为他们不了解Fragment的好处,而是因为他们清楚地看到了Fragment的缺点和局限性。

如果你认真的看过下面这个流程图的话,你就知道我并没有夸大Fragment生命周期的复杂性。这很恐怖。

幸运的是,您在应用中使用Fragment的时候无需了解整个生命周期的转换过程。

在这篇文章中,我将介绍一些处理Fragment生命周期的方法,它隐藏了大部分的复杂细节。 我使用这种方法已经有好几年了,它确实有效。

Activity的生命周期

正如你即将看到的,当我在使用Fragments的时候,我会尽可能地将它们从Activity的生命周期中解耦出来。但是,这并不能改变它们之间有许多相似之处的事实。

我已经写了一篇关于我是如何处理Activity的生命周期的文章:android应用开发者,你们真的了解Activity的生命周期吗?。 该文章收到了非常积极的反馈,并在不到一个月的时间内成为了我博客上最受欢迎的文章之一。

正如我所说的,Activity和Fragment的生命周期在很多方面都是相似的。 所以,在这篇文章中,我会在他们之间进行很多类比。但是,我不想做重复的工作。 因此,我假设你已经阅读过有关Activity生命周期的文章。

My Take on Fragment Lifecycle

我处理Fragment生命周期的方法旨在实现两个目标:

  1. 使Fragment的生命周期处理逻辑尽可能类似于Activity的处理逻辑。
  2. 使Fragment的生命周期处理逻辑独立于Activity的生命周期。

使用类似于处理Activity的生命周期的方法来处理Fragment的生命周期,能大幅降低应用程序的整体复杂性。 开发人员只需要学习一种方法,而不是两种不同的方法。 这意味着开发过程中的工作量会减少,维护会更容易,新团队成员熟悉项目的速度会更快。 我也完全确定这样做可以降低产生bug的风险,尽管这只是我个人的一种主观想法。

通过实现上述两个目标,我大大降低了与Fragment相关的问题的复杂性,从而使它更具吸引力。

onCreate(Bundle)

请记住,您无权访问Activity的构造函数,因此您无法通过它来将依赖注入到Activity中。好消息是:Fragment有一个公开的构造函数,我们甚至可以定义更多的构造函数。 坏消息是:这样做会导致严重的bug,所以我们不能这样做。

当Activity被强制销毁,之后又被自动恢复的时候,Android系统会在这一过程中销毁并重新创建Fragment。 重新创建的机制是通过使用反射的方法来调用Fragment的无参构造函数来实现的。 因此,如果您是使用带参数的构造函数来实例化Fragment,并在其中将依赖的对象传递给它,那么在保存和恢复后,所有这些依赖的对象都将被设置为null。

因此,就像Activity一样,您需要使用onCreate(Bundle)方法作为构造函数的替代者。 Fragment中依赖对象的注入和初始化就发生在这里。

但是,与Activity的onCreate(Bundle)方法不同的是, 您不得在Fragment的onCreate(Bundle)方法中执行任何与Android View相关的操作。 这个非常重要,其原因将在下一节中详细阐述。

总而言之,Fragment的onCreate(Bundle)方法的基本的处理逻辑如下所示:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    getInjector().inject(this); // inject dependencies
 
    if (savedInstanceState != null) {
        mWelcomeDialogHasAlreadyBeenShown = savedInstanceState.getBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN);
    }
     
    mActivityAsListener = (ActivityAsListener) requireActivity();
 
}
复制代码

还有一点忘了说 - 将Activity转换为listener也发生在onCreate(Bundle)中。 如果你喜欢这种方式的话,这将比在onAttach(Context)中抛出一个通用的ClassCastException要有意义的多。

View onCreateView(LayoutInflater, ViewGroup, Bundle)

这个方法是Fragment独有的,这是它与Activity生命周期最显着的区别。

但它也是Fragment相关问题的“万恶之源”。 稍后我会讨论它的“邪恶”,但请记住,如果您使用Fragment,最好不要低估View onCreateView(LayoutInflater,ViewGroup,Bundle)的复杂性。

那么,故事是什么呢?

Activity在生命周期的转换过程中都只有同一个View hierarchy。 你在Activity的onCreate(Bundle)中初始化这个View hierarchy,然后它就会一直存在于Activity的整个生命周期,直到Activity被垃圾收集器回收为止。 您可以手动更改Activity的View hierarchy的组成,Android系统是不会为您做任何事情的。

然而,Fragment在其生命周期中可以存在有多个View hierarchy,由Android系统决定何时进行替换。

换句话说,你可以在程序运行的时候动态改变Fragment的View hierarchy,现在你应该清楚为什么不能在Fragment的onCreate(Bundle)中操作View了吧。 onCreate(Bundle)方法在Fragment被Attach到Activity后仅被调用一次,它无法支持Fragment的View hierarchy的动态化。

每次需要创建新的View hierarchy的时候,Android系统都会调用onCreateView(LayoutInflater, ViewGroup, Bundle)方法。 您的工作是创建View hierarchy并将其初始化为正确的状态,然后将它作为该方法的返回值,之后它就会被Android系统接管。

重写这个方法的主要原则是:Fragment中所有持有与View hierarchy相关的对象的引用的成员变量,必须在View onCreateView(LayoutInflater,ViewGroup,Bundle)中进行初始化。 换句话说,如果Fragment的成员变量持有View或者相关对象的引用,请确保在此方法中初始化这些成员变量,这非常重要。

总而言之,Fragment的View onCreateView(LayoutInflater, ViewGroup, Bundle)方法的基本的处理逻辑如下所示:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View root = inflater.inflate(R.layout.some_layout, container, false);
    mSomeView = rootView.findViewById(R.id.some_view); // must assign all Fragment's fields which are Views
    mLstSomeList = rootView.findViewById(R.id.lst_some_list); // must assign all Fragment's fields which are Views
    mAdapter = new SomeAdapter(getContext()); // must assign all Fragment's fields related to View hierarchy
    mLstSomeList.setAdapter(mAdapter);
    return rootView;
}
复制代码

这个方法的另一个有趣的地方是它接收了一个参数:保存状态的Bundle。 老实说,我觉得这样很麻烦。Android系统框架开发的人员似乎自己也不确定应该在哪里恢复这些状态,所以他们将这个Bundle参数注入了到这个方法里,让我们自己去弄清楚。

不要在这个方法里恢复状态。在后面介绍onSaveInstanceState(Bundle)方法时我会解释不要这样做的原因。

用户Boza_s6在Reddit上提交了他(她)对这篇文章的反馈,我们进行了一次非常有趣的讨论。 问题的焦点在于当在Fragment里使用列表和适配器的时候,我的方法是否会导致内存泄漏。根据上面的讨论,我想这个问题的答案是清楚的。

如果您遵循我在本文中分享的原则,那么就不存在内存泄漏的风险。事实上,我使用这种方法的部分原因就是为了减轻Fragment内存泄漏的内在风险。

我的原则是Fragment里每个与View hierarchy相关的成员变量都必须在此方法中初始化, 这包括列表适配器,用户交互事件的监听器等。保持Fragment里的代码可维护性的唯一方法是确保此方法在重新创建整个View hierarchy的时候会重新初始化Fragment里与之相关的成员变量。

onStart()

Fragment的这个方法与Activity的onStart()方法具有完全相同的职责和指导原则。 你可以阅读我之前关于Activity的生命周期的文章,android应用开发者,你们真的了解Activity的生命周期吗?

总而言之,Fragment的onStart()方法的基本的处理逻辑如下所示:

@Override
public void onStart() {
    super.onStart();
 
    mSomeView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            handleOnSomeViewClick();
        }
    });
 
    mFirstDependency.registerListener(this);
 
    switch (mSecondDependency.getState()) {
        case SecondDependency.State.STATE_1:
            updateUiAccordingToState1();
            break;
        case SecondDependency.State.STATE_2:
            updateUiAccordingToState2();
            break;
        case SecondDependency.State.STATE_3:
            updateUiAccordingToState3();
            break;
    }
 
    if (mWelcomeDialogHasAlreadyBeenShown) {
        mFirstDependency.intiateAsyncFunctionalFlowAndNotify();
    } else {
        showWelcomeDialog();
        mWelcomeDialogHasAlreadyBeenShown = true;
    }
}

复制代码

如您所见,这个方法里包含了Fragment的大部分功能逻辑。 保持onStart()方法在Activity和Fragment里的一致性具有很多好处。

onResume()

这个方法的处理逻辑与Activity的onResume()方法相同。

onPause()

这个方法的处理逻辑与Activity的onPause()方法相同。

onStop()

这个方法的处理逻辑同样与Activity的onStop()方法相同。其基本的处理逻辑如下所示:

@Override
public void onStop() {
    super.onStop();
 
    mSomeView.setOnClickListener(null);
 
    mFirstDependency.unregisterListener(this);
}

复制代码

这里有一行有趣而又令人惊讶的代码:mSomeView.setOnClickListener(null)。 我之前已经解释过了为什么你可能要在Activity的onStop()注销点击事件的监听器,所以这里我不会再重复。

不过,我想借此机会回答另一个问题:注销点击事件的监听器是不是必需的?

据我所知,绝大多数的Android应用程序都不会这样做,并且仍然运行良好。 所以,我认为这不是强制性的。但是如果你不这样做,你的app可能会遇到一定数量的bug和崩溃。

onDestroyView()

绝大多数情况下,您都不应该重写此方法。 我想有些读者会对此感到惊讶,但我真的是这么想的。

正如我前面所说的,你必须在onCreateView(LayoutInflater,ViewGroup,Bundle)里初始化Fragment里所有持有View或者相关对象的引用的成员变量。这个要求源自于这样一个事实:Fragment的View hierarchy可以被重新创建,所以所有未在该方法中被初始化的持有View的引用对象都将被置为null。这样做非常重要,否则你的app可能会遇到一些非常令人讨厌的bug和崩溃。

如果你这样做了,Fragment将一直持有对这些View的强引用,直到下一次调用View onCreateView(LayoutInflater,ViewGroup,Bundle)方法或者整个Fragment被销毁。

现在有一种广泛的建议是,您应该在onDestroyView()方法里将所有前面提到的成员变量设置为Null。 目的是尽快释放这些引用以允许垃圾收集器在onDestroyView()返回后立即回收它们,这样就能更快地释放与这些View相关的内存空间。

上面的解释虽然听起来很合理,但这是过早优化的经典案例。 在绝大多数情况下,你并不需要这种优化。因此,你没有必要去重写onDestroyView()的方法,这只会使得本来已经很复杂的处理Fragment生命周期的逻辑更复杂。

所以,你不需要重写onDestroyView()方法。

onDestroy()

像Activity一样,您不需要在Fragment中重写此方法。

onSaveInstanceState(Bundle)

这个方法的基本的处理逻辑如下所示:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN, mWelcomeDialogHasAlreadyBeenShown);
}

复制代码

我希望你不要被这个方法的表面的简单性所误导。 保存和恢复流程的错误处理是Android应用程序中bug和崩溃的主要原因之一。 在之前关于Activity生命周期的文章中我花了很大的篇幅来讨论这个方法,这不是巧合。

因此,在Activity中处理保存和恢复状态的过程很麻烦。你可能会认为Fragment不会比这更糟糕。然而,令人惊讶的是,Fragment更糟糕。

2011年2月Dianne Hackborn在Javadoc里介绍了这个方法,其中包含一些令人可怕的描述:

这个方法对应于Activity的onSaveInstanceState(Bundle)方法,所以对Activity的onSaveInstanceState(Bundle)的大多数讨论也适用于此。但请注意:此方法可能在onDestroy()之前的任何时候被调用。在许多情况下,Fragment可能会被销毁(例如,当Fragment被放置在回退栈上而没有UI显示时),但是它的状态不会被保存,除非其所属的Activity确实需要保存其状态。

如果这个“注释”不能令你感到惊讶的话,那么说明你还没有深入的理解Activity和Fragment生命周期。

根据官方文件:这个方法可能在onDestroy()之前的任何时候调用。 这里有两个主要问题:

  1. 正如Dianne说的那样,与Fragment相关联的View hierarchy可以被销毁而不实际保存其状态。 因此,如果您想在View onCreateView(LayoutInflater,ViewGroup,Bundle)中恢复Fragment的状态,那么您将冒着覆盖最新状态的风险。这是一个非常严重的错误, 这正是我告诉你只能在onCreate(Bundle)中恢复状态的原因。

  2. 如果在onDestroy()之前的任何时候都可以调用onSaveInstanceState(Bundle),那么您无法保证何时才能安全地更改Fragment的状态(例如替换嵌套的Fragments)。

我不认为Dianne Hackborn对这个方法的描述是准确的。事实上,我认为Dianne Hackborn在2011年写这篇文章的时候就已经犯了一个错误。“任何时候”的概念意味着某种不确定性或随机性,我认为它从来就不存在。最有可能的是,只有几个因素影响了这种行为,Dianne决定不列出它们,因为她认为它们不够重要。

如果真是这样的话,那么她显然是错的。

如果这个描述在当时是正确的,那么就说明设计这个框架的Google开发人员并不知道这样的行为会导致上面列出的两个问题。特别是,它意味着包含已保存状态的Bundle从未被传入View onCreateView(LayoutInflater,ViewGroup,Bundle)方法。

setRetainInstance(boolean)

切勿使用retained的Fragment。你不需要它们。

如果你这样做了的话,请记住它改变了Fragment的生命周期。 那么本文中所描述的一切内容都是无效的。

为什么Fragment如此复杂?

正如你所看到的,Fragment确实很复杂,我甚至会说非常复杂。

Fragment最大的问题是它的View hierarchy可以独立于Fragment对象本身被销毁和重新创建,如果不是这样的话,Fragment的生命周期几乎与Activity的生命周期一模一样。

造成这种复杂性的原因是什么?显然我不知道答案,我只能根据我的理解力去进行推测。

我认为引入这种机制是为了优化内存的消耗。当Fragment不可见的时候,销毁Fragment的View hierarchy允许释放一些内存。 但另一个问题是:Fragment实现了对状态保存和恢复流程的支持,Google为什么要这样做?

我不知道。

然而,我所知道的是,FragmentStatePagerAdapter并不是采用这种机制去保存和恢复Fragments的状态。

就我而言,这整个机制是一种过早的优化。它没有任何用处,反而会使得Fragment使用更复杂。

比较讽刺的一点在于,Google开发人员似乎自己都不了解Fragment的生命周期。

Google发布了LiveData Architecture Component,但是存在一个严重漏洞。 如果有一位开发人员仔细的研究过Fragment,那么他们就会真正理解Fragment的生命周期,在设计阶段他们就会发现这个问题。

Google花了几个月的时间来修复这个bug。最后,在Google IO 18期间,他们宣布bug已修复。 解决方法是为Fragment的View hierarchy引入了另一种生命周期。

因此,如果您使用的是LiveData组件,现在您需要记住Fragment对象有两个不同的生命周期。这让我非常难过。

结束语

好的,让我们结束这篇文章。

Fragment真是一团糟。唯一比Fragment更糟糕的是他们的官方文档。 Google开发人员并没有完全理解Fragment的生命周期,并且会继续增加它的复杂性。

说了这么多,其实我一直在使用Fragment,并在将来继续使用它们。 与“一个界面一个Activity”的方法相比,Fragment可以提供更好的用户体验。

为了在使用Fragment的同时并保持我的理智,我正在使用本文所描述的处理Fragment生命周期的方法。它可能不包含所有的情形,但在过去的几年里它对我很有帮助。

这种方法有两个好处:它使得处理Fragment的生命周期非常类似于Activity的生命周期并且独立于它。您可能已经注意到,我在这篇文章中并没有提到Activity的状态。 只要我坚持使用这种方法,我并不在乎Activity会发生什么,我甚至不用关心这个Fragment是否是嵌套的。

因此,我重写了Fragment里最少数量的方法,而且互相之间没有依赖关系。 这使得Fragment的复杂性对我来说是可管理的。

我相信自己在写这篇文章时错过了一些重要的观点。 因此,如果您有任何补充或者想要指出本文中的任何缺陷,请随时在下面的评论中提出来。

关注下面的标签,发现更多相似文章
评论