Android应用开发者,你们真的了解Activity的生命周期

2023-01-15  本文已影响0人  代码我写的怎么

一开始,我觉得Activity的生命周期虽然过于复杂,但它不应该是一个难题。我的意思是:对于Android开发新手来说,如何正确地处理Activity生命周期可能有点困难,但是我无法想象对于那些富有经验的android开发者来说,这依然是一个棘手的问题。

我还是想的太简单了。

一会儿我会告诉你整个故事,但是先让我简述下我写这篇文章的目的。

我想要与你们分享我是如何处理Activity的生命周期的,它比官方文档里描述的更简单,而且还涵盖绝大多数棘手的极端情形。

Activity的生命周期仍然是一个难题:

一个星期内发生的两件事情让我意识到:即使在今天,Activity的生命周期仍然是Android开发人员面临的一个难题。

几周前一位Redditor在“androiddev”里分享了一篇关于Activity的生命周期的文章。这篇文章的写作基础来源于我在StackOverflow社区里分享的一个答案。当时提问者推荐了一种处理Activity的生命周期的方法,我觉得并不准确,于是我立即提交了一条评论,希望其他读者能意识到这一点。

然后陆续有几位redditor回答并质疑了我的观点,进而演变成了一场漫长而非常有见地的讨论。在讨论过程中,我观察到了一些有趣的关于Activity的生命周期的现象:

  1. Activity的生命周期让许多有经验的android开发人员感到困惑。

  2. 官方文档里仍然存在着一些关于Activity的生命周期的过时信息和矛盾信息。

  3. 即使是编写Google官方教程的开发人员们也没有真正地理解Activity的生命周期以及它的影响。

几天之后,我正在访问一个潜在客户。该公司雇佣了一批富有经验的Android开发人员,他们尝试在老的代码库里添加新功能,这不是一件容易的事。

在快速代码审查期间,我注意到其中一位维护人员决定在Activity的onCreate(Bundle)中订阅EventBus,并在onDestroy()中取消订阅。这样做会带来很大的麻烦,所以我建议他们重构这部分代码。

My take on Activity life-cycle::

上述两件事让我意识到还是有许多android开发人员对Activity的生命周期充满困惑。现在,我想尝试通过分享我的经验来给其他开发人员提供便利。

我不打算在这里为Activity的生命周期重新编写一份文档,这代价太大了。我会告诉你如何在Activity生命周期的各个方法之间划分逻辑以实现最简单的设计并避免最常见的陷阱。

onCreate(Bundle):

Android framework并没有提供Activity的构造函数,它会自动创建Activity类的实例。那么我们应该在哪里初始化Activity呢?

你可以把所有应该在Activity的构造函数里处理的逻辑放在onCreate(Bundle)这个方法里。

理想情况下,构造函数只会初始化对象的成员变量:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n35" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public ObjectWithIdealConstructor(FirstDependency firstDependency,
SecondDependency secondDependency) {
mFirstDependency = firstDependency;
mSecondDependency = secondDependency;
} </pre>

除了初始化成员变量外, onCreate(Bundle)方法还承担着另外两个职责:恢复之前保存的状态并调用setContentView()方法。

所以,onCreate(Bundle)方法中的基本功能结构应该是:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n38" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

getInjector().inject(this); // inject dependencies

setContentView(R.layout.some_layout);

mSomeView = findViewById(R.id.some_view);

if (savedInstanceState != null) {
mWelcomeDialogHasAlreadyBeenShown = savedInstanceState.getBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN);
}

} </pre>

显然,这并不适用于所有人,你可能会有一些不同的结构。不过没关系,只要你在onCreate(Bundle)中没有以下内容:

每当我需要决定某一段逻辑是否应该放在onCreate(Bundle)方法里时,我就会问自己这个问题:这段逻辑与Activity的初始化有关吗?如果答案是否定的,我就会另寻别处。

当Android framework创建了一个Activity实例,而且它的onCreate(Bundle) 方法执行完毕之后,这个Activity就会处于已创建状态。

处于已创建状态的Activity 并不会触发资源分配,也不会接收到系统里其他对象发出的事件。从这种意义上来讲,“created”状态是一种已经准备好了,但是不活跃和隔离的状态。

onStart():

为了使Activity能够与用户交互,Android framework接着会调用Activity的onStart()方法使其处于活跃状态。onStart() 方法中的一些基本的逻辑处理包括:

  1. Registration of View click listeners

  2. Subscription to observables (general observables, not necessarily Rx)

  3. Reflect the current state into UI (UI update)

  4. Functional flows

  5. Initialization of asynchronous functional flows

  6. Resources allocations

对于第1点和第2点,你可能会感到很惊讶。因为在大多数官方文档和教程里,它们都会被放在onCreate(Bundle)里。我认为这是对复杂的Activity的生命周期的一种误解,我会在接下来的onStop()方法里讲到这一点。

所以,onStart()方法中的基本功能结构应该是:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n70" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@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;
}
} </pre>

需要强调的一点是,你实际在onStart()中所执行的操作是由每个特定Activity的详细需求决定的。

在Android framework调用完onStart()方法后,Activity将从已创建状态转换为已启动状态。在此状态下,它可以正常工作,并且可以与系统的其他组件协作。

onResume():

关于onResume()方法的第一条准则是你不需要覆盖onResume()方法。第二条准则也是你不需要覆盖onResume()方法。第三条准则是在某些特殊情况下你才会确实需要覆盖onResume()方法。

我搜索了我的一个项目的代码库,发现有32个onStart()方法被覆盖重写,平均每段代码约5行,一共有大约150多行代码。相反,只有2个onResume()方法被覆盖重写,一共8行代码,这8行代码主要是用于恢复播放动画和视频。

这总结了我对onResume()的看法:它只能用于在屏幕上启动或恢复一些动态的对象。你想在onResume()而不是onStart()中做这件事的原因将在稍后的onPause()部分讨论。

在Android framework调用完onResume()方法后,Activity将从已启动状态转换为"resumed"状态。在此状态下,用户可以与它进行交互。

onPause():

在这个方法里,您应该暂停或停止在onResume()中恢复或启动的动画和视频。就像onResume()一样,你几乎不需要重写onPause()。在onPause()方法而不是onStop()方法中处理动画的原因是因为当Activity被部分隐藏(比如,弹出系统对话框)或者是在多窗口模式下失去焦点的时候,只有onPause()方法会被调用。因此如果你想在 只有用户正在与Activity交互的情况下播放动画,同时避免在多窗口模式下分散用户注意力并延长电池寿命,onPause() 是你唯一可以信赖的选择。 这一结论的前提是你希望你的动画或视频在用户进入多窗口模式时停止播放,如果你希望你的动画或视频在用户进入多窗口模式时继续播放,那么你不应该在onPause()中暂停它。在这种情况下,你需要将onResume()/ onPause()中的逻辑移动到onStart()/ onStop()。

一种特殊的情况是相机的使用,由于相机是所有应用程序共享的单一资源,因此通常您会想要在onPause()方法中释放它。

在Android framework调用完onPause()方法后,Activity将从"resumed"状态转换为已启动状态。

onStop():

在这个方法里,您将注销所有观察者和监听者,并释放onStart()中分配的所有资源。

所以,onStop()方法里的基本功能结构应该是:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n88" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
public void onStop() {
super.onStop();

mSomeView.setOnClickListener(null);

mFirstDependency.unregisterListener(this);
} </pre>

让我们来讨论一个问题:为什么你需要在这个方法里取消注册?

首先,如果此Activity不被mFirstDependency取消注册的话,您可能会泄漏它。这不是我愿意承担的风险。

所以,问题变成了为什么要在onStop()方法里取消注册,而不是onPause()方法或者是onDestroy()方法?

想要快速清晰地解释这些确实有点棘手。

如果在onPause()方法里调用mFirstDependency.unregisterListener(this),那么Activity将不会收到相关异步流程完成的通知。因此,它不能让用户感知到这一事件,从而完全违背了多窗口模式的设计初衷,这不是一种好的处理方式。

如果在onDestroy()方法里调用mFirstDependency.unregisterListener(this),这同样不是一种好的处理方式。

当应用被用户推到后台(例如,点击“home”按钮)时,Activity的onStop()将被调用,从而使得其返回到“已创建”状态,这个Activity可以在几天甚至几周的时间内保持这个状态。

如果这时候mFirstDependency产生了连续的事件流,那么在这几天甚至几周的时间里,Activity可以都处理这些事件,即使用户在这段时间内从未真正与它交互过。这将是对用户电池寿命的不负责任的浪费,而且在这种情况下,应用消耗的内存会逐渐增多,应用进程被OOM(Out Of Memory)Killer杀死的可能性也会增大。

因此,在onPause()和onDestroy()里调用mFirstDependency.unregisterListener(this)都不是一种好的处理方式,您应该在onStop()中执行此操作。

关于注销View的事件监听器,我想多说几句。

由于Android UI框架的不合理设计,像这个问题中所描述的奇怪场景是可能会发生的。有几种方法可以解决这个问题,但是从onStop()中注销View的事件监听器是最干净的一种。

或者,您可以完全忽略这种罕见的情况。这是大多数Android开发人员所做的,但是,您的用户偶尔会遇到一些奇怪的行为和崩溃。

在Android framework调用完onStop()方法后,Activity将从已启动状态转换为已创建状态。

onDestroy():

永远都不要覆盖重写这个方法。

我搜索了我所有的项目代码库,没有一个地方需要我重写onDestroy()方法。

如果你思考一下我前面对onCreate(Bundle)的职责的描述,你会发现这是完全合理的,它仅仅执行了初始化Activity对象的操作,所以完全没有必要手动完成清理工作。

当我在查看新的Android代码库时,我通常会搜索几个常见的错误模式,这使我能够快速地了解代码的质量。 onDestroy()的覆盖重写是我寻找的第一个错误模式。

onSaveInstanceState(Bundle):

此方法用于保存一些临时的状态,在这个方法里你位移需要做的就是将你想保存的状态存入Bundle数据结构中:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n111" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN, mWelcomeDialogHasAlreadyBeenShown);
} </pre>

在这里,我想提一个与Fragment有关的常见陷阱。

如果你在这个方法执行完之后,提交Fragment事务,那么应用程序会抛出IllegalStateException异常而导致崩溃。

你需要知道的一点是onSaveInstanceState(Bundle)将在onStop()之前被调用。

好消息是,经过多年的努力,谷歌终于意识到了这个bug。 在Android P中,onStop()将在onSaveInstanceState(Bundle)之前被调用。

总结:

是时候结束这篇文章了。

虽然本文的内容既不简短也不简单,但我认为它比大多数文档(包括官方教程)更简单,更完整。

我知道一些经验丰富的专业开发人员会质疑我处理Activity生命周期的方式,没关系。事实上,我非常期待您的质疑。

不过,请记住,我已经使用这个方案好几年了。根据我的经验,使用这种方法编写的代码比许多项目中看到的对于Activity生命周期的处理逻辑要清晰简洁的多。

这种方法的最终验证来自Google本身。

从Nougat开始,Android系统支持多屏任务。我在这篇文章中与你分享的方法几乎不需要任何调整。这基本上证实,相对于官方文档,它包含了对Activity生命周期更深入的见解。

另外,Android P中对于Activity的onStop()和onSaveInstanceState(Bundle)方法之间的调用顺序的调整将使得这种方法对于Fragments的使用来说是最安全的。

我不认为这是巧合。

上一篇 下一篇

猜你喜欢

热点阅读