NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

第一行代码——Android(二):掌握活动的生命周期

  • 2020-02-22
  • 本文字数:6660 字

    阅读完需:约 22 分钟

第一行代码——Android(二):掌握活动的生命周期

编者按:本文节选自郭霖著《第一行代码——Android》一书中的部分章节。

活动的生命周期

掌握活动的生命周期对任何 Android 开发者来说都非常重要,当你深入理解活动的生命周期之后,就可以写出更加连贯流畅的程序,并在如何合理管理应用资源方面发挥得游刃有余。你的应用程序将会拥有更好的用户体验。

活动的生命周期:返回栈

经过前面几节的学习,我相信你已经发现了这一点,Android 中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击 Back 键会销毁最上面的活动,下面的一个活动就会重新显示出来。


其实 Android 是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当我们按下 Back 键或调用finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给用户。


示意图 1 展示了返回栈是如何管理活动入栈出栈操作的。



图 1 返回栈工作示意图

活动的生命周期:活动状态

每个活动在其生命周期中最多可能会有 4 种状态。


  1. 运行状态


当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。


  1. 暂停状态


当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活着的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。


  1. 停止状态


当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。


  1. 销毁状态


当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。

活动的生命周期:活动的生存期

Activity 类中定义了 7 个回调方法,覆盖了活动生命周期的每一个环节,下面就来一一介绍这 7 个方法。


  • onCreate()。这个方法你已经看到过很多次了,每个活动中我们都重写了这个方法,它会在活动第一次被创建的时候调用。你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等。

  • onStart()。这个方法在活动由不可见变为可见的时候调用。

  • onResume()。这个方法在活动准备好和用户进行交互的时候调用。此时的活动一定位于返回栈的栈顶,并且处于运行状态。

  • onPause()。这个方法在系统准备去启动或者恢复另一个活动的时候调用。我们通常会在这个方法中将一些消耗 CPU 的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。

  • onStop()。这个方法在活动完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。

  • onDestroy()。这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。

  • onRestart()。这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。


以上 7 个方法中除了onRestart()方法,其他都是两两相对的,从而又可以将活动分为 3 种生存期。


  • 完整生存期。活动在onCreate()方法和onDestroy()方法之间所经历的,就是完整生存期。一般情况下,一个活动会在onCreate()方法中完成各种初始化操作,而在onDestroy()方法中完成释放内存的操作。

  • 可见生存期。活动在onStart()方法和onStop()方法之间所经历的,就是可见生存期。在可见生存期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。我们可以通过这两个方法,合理地管理那些对用户可见的资源。比如在onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的活动不会占用过多内存。

  • 前台生存期。活动在onResume()方法和onPause()方法之间所经历的就是前台生存期。在前台生存期内,活动总是处于运行状态的,此时的活动是可以和用户进行交互的,我们平时看到和接触最多的也就是这个状态下的活动。


为了帮助你能够更好地理解,Android 官方提供了一张活动生命周期的示意图,如图 2 所示。



图 2 活动的生命周期

活动的生命周期:体验活动的生命周期

讲了这么多理论知识,也是时候该实战一下了,下面我们将通过一个实例,让你可以更加直观地体验活动的生命周期。


这次我们不准备在 ActivityTest 这个项目的基础上修改了,而是新建一个项目。因此,首先关闭 ActivityTest 项目,点击导航栏 File→Close Project。然后再新建一个 ActivityLifeCycleTest 项目,新建项目的过程你应该已经非常清楚了,不需要我再进行赘述,这次我们允许 Android Studio 帮我们自动创建活动和布局,这样可以省去不少工作,创建的活动名和布局名都使用默认值。


这样主活动就创建完成了,我们还需要分别再创建两个子活动——NormalActivity 和 DialogActivity,下面一步步来实现。


右击 com.example.activitylifecycletest 包→New→Activity→Empty Activity,新建 NormalActivity,布局起名为 normal_layout。然后使用同样的方式创建 DialogActivity,布局起名为 dialog_layout。


现在编辑 normal_layout.xml 文件,将里面的代码替换成如下内容:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="This is a normal activity" />
</LinearLayout>
复制代码


这个布局中我们就非常简单地使用了一个 TextView,用于显示一行文字,在下一章中你将会学到更多关于 TextView 的用法。


然后再编辑 dialog_layout.xml 文件,将里面的代码替换成如下内容:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="This is a dialog activity" />
</LinearLayout>
复制代码


两个布局文件的代码几乎没有区别,只是显示的文字不同而已。


NormalActivity 和 DialogActivity 中的代码我们保持默认就好,不需要改动。


其实从名字上你就可以看出,这两个活动一个是普通的活动,一个是对话框式的活动。可是我们并没有修改活动的任何代码,两个活动的代码应该几乎是一模一样的,在哪里有体现出将活动设成对话框式的呢?别着急,下面我们马上开始设置。修改 AndroidManifest.xml 的<activity>标签的配置,如下所示:


<activity android:name=".NormalActivity"></activity><activity android:name=".DialogActivity"    android:theme="@style/Theme.AppCompat.Dialog"></activity>
复制代码


这里是两个活动的注册代码,但是 DialogActivity 的代码有些不同,我们给它使用了一个android:theme属性,这是用于给当前活动指定主题的,Android 系统内置有很多主题可以选择,当然我们也可以定制自己的主题,而这里@style/Theme.AppCompat.Dialog则毫无疑问是让 DialogActivity 使用对话框式的主题。


接下来我们修改 activity_main.xml,重新定制主活动的布局,将里面的代码替换成如下内容:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">
<Button android:id="@+id/start_normal_activity" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Start NormalActivity" />
<Button android:id="@+id/start_dialog_activity" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Start DialogActivity" />
</LinearLayout>
复制代码


可以看到,我们在 LinearLayout 中加入了两个按钮,一个用于启动 NormalActivity,一个用于启动 DialogActivity。


最后修改 MainActivity 中的代码,如下所示:


public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG,"onCreate"); setContentView(R.layout.activity_main); Button startNormalActivity = (Button) findViewById(R.id.start_normal_ activity); Button startDialogActivity = (Button) findViewById(R.id.start_dialog_ activity); startNormalActivity.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, NormalActivity.class); startActivity(intent); } }); startDialogActivity.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity.this, DialogActivity.class); startActivity(intent); } }); }
@Override protected void onStart() { super.onStart(); Log.d(TAG, "onStart"); }
@Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume"); }
@Override protected void onPause() { super.onPause(); Log.d(TAG, "onPause"); }
@Override protected void onStop() { super.onStop(); Log.d(TAG, "onStop"); }
@Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy"); }
@Override protected void onRestart() { super.onRestart(); Log.d(TAG, "onRestart"); }
}
复制代码


onCreate()方法中,我们分别为两个按钮注册了点击事件,点击第一个按钮会启动 NormalActivity,点击第二个按钮会启动 DialogActivity。然后在 Activity 的 7 个回调方法中分别打印了一句话,这样就可以通过观察日志的方式来更直观地理解活动的生命周期。


现在运行程序,效果如图 3 所示。



图 3 MainActivity 界面


这时观察 logcat 中的打印日志,如图 4 所示。



图 4 启动程序时的打印日志


可以看到,当 MainActivity 第一次被创建时会依次执行onCreate()onStart()onResume()方法。然后点击第一个按钮,启动 NormalActivity,如图 5 所示。



图 5 NormalActivity 界面


此时的打印信息如图 6 所示。



图 6 打开 NormalActivity 时的打印日志


由于 NormalActivity 已经把 MainActivity 完全遮挡住,因此onPause()onStop()方法都会得到执行。然后按下 Back 键返回 MainActivity,打印信息如图 7 所示。



图 7 返回 MainActivity 的打印日志


由于之前 MainActivity 已经进入了停止状态,所以onRestart()方法会得到执行,之后又会依次执行onStart()onResume()方法。注意此时onCreate()方法不会执行,因为 MainActivity 并没有重新创建。


然后再点击第二个按钮,启动 DialogActivity,如图 8 所示。



图 8 DialogActivity 界面


此时观察打印信息,如图 9 所示。



图 9 打开 DialogActivity 时的打印日志


可以看到,只有onPause()方法得到了执行,onStop()方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。相应地,按下 Back 键返回 MainActivity 也应该只有onResume()方法会得到执行,如图 10 所示。



图 10 再次返回 MainActivity 的打印日志


最后在 MainActivity 按下 Back 键退出程序,打印信息如图 11 所示。



图 11 退出程序时的打印日志


依次会执行onPause()onStop()onDestroy()方法,最终销毁 MainActivity。


这样活动完整的生命周期你已经体验了一遍,是不是理解得更加深刻了?

活动的生命周期:活动被回收了怎么办

前面我们已经说过,当一个活动进入到了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个活动 A,用户在活动 A 的基础上启动了活动 B,活动 A 就进入了停止状态,这个时候由于系统内存不足,将活动 A 回收掉了,然后用户按下 Back 键返回活动 A,会出现什么情况呢?其实还是会正常显示活动 A 的,只不过这时并不会执行onRestart()方法,而是会执行活动 A 的onCreate()方法,因为活动 A 在这种情况下会被重新创建一次。


这样看上去好像一切正常,可是别忽略了一个重要问题,活动 A 中是可能存在临时数据和状态的。打个比方,MainActivity 中有一个文本输入框,现在你输入了一段文字,然后启动 NormalActivity,这时 MainActivity 由于系统内存不足被回收掉,过了一会你又点击了 Back 键回到 MainActivity,你会发现刚刚输入的文字全部都没了,因为 MainActivity 被重新创建了。


如果我们的应用出现了这种情况,是会严重影响用户体验的,所以必须要想想办法解决这个问题。查阅文档可以看出,Activity 中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在活动被回收之前一定会被调用,因此我们可以通过这个方法来解决活动被回收时临时数据得不到保存的问题。


onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。


在 MainActivity 中添加如下代码就可以将临时数据进行保存:


@Overrideprotected void onSaveInstanceState(Bundle outState) {    super.onSaveInstanceState(outState);    String tempData = "Something you just typed";    outState.putString("data_key", tempData);}
复制代码


数据是已经保存下来了,那么我们应该在哪里进行恢复呢?细心的你也许早就发现,我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在活动被系统回收之前有通过onSaveInstanceState()方法来保存数据的话,这个参数就会带有之前所保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。


修改 MainActivity 的onCreate()方法,如下所示:


@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    Log.d(TAG, "onCreate");    setContentView(R.layout.activity_main);    if (savedInstanceState != null) {        String tempData = savedInstanceState.getString("data_key");        Log.d(TAG, tempData);    }    ...}
复制代码


取出值之后再做相应的恢复操作就可以了,比如说将文本内容重新赋值到文本输入框上,这里我们只是简单地打印一下。


不知道你有没有察觉,使用Bundle来保存和取出数据是不是有些似曾相识呢?没错!我们在使用 Intent 传递数据时也是用的类似的方法。这里跟你提醒一点,Intent 还可以结合Bundle一起用于传递数据,首先可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在 Intent 里。到了目标活动之后先从 Intent 中取出Bundle,再从Bundle中一一取出数据。具体的代码我就不写了,要学会举一反三哦。


图书简介https://www.ituring.com.cn/book/1841



相关阅读


第一行代码——Android(一):前行必备,如何使用日志工具


2020-02-22 08:001579

评论

发布
暂无评论
发现更多内容

2022年终总结:一年读完的40本书

石云升

读书笔记 年终总结 1月月更

一个词语总结2022,你的是什么? | 2022 年度总结

陈言必行

2022年终总结

Guitar Pro2024免费版吉他打谱软件

茶色酒

Guitar Pro8 Guitar Pro2024

ProgressBar(进度条)

智趣匠

Android Studio 进度条 ProgressBar

React源码分析5-commit

flyzz177

React

对话阿里云叔同:如何看待 2022 年云原生的发展,2023 年有哪些值得关注的技术?

Serverless Devs

阿里云 Serverless 云原生

Jenkins 构建过程中提示 GPG 错误

HoneyMoose

Jenkins 项目的 gpg: signing failed: Bad passphrase 错误

HoneyMoose

Spring Cloud Alibaba x AppActive 带来的全新异地活动解决方案

阿里巴巴中间件

阿里云 云原生 Spring Cloud Aliababa

2023-01-09:以下go语言代码输出什么?A:+Inf; B:zero; C:something else; D:doesn‘t compile。 package main import (

福大大架构师每日一题

golang go语言 福大大 选择题

比 CK 再快 24% ! YMatrix 5.0 SSB 基准测试报告

YMatrix 超融合数据库

Clickhouse 性能基准测试 超融合数据库 YMatrix

​​苹果应用上架后多久可以下载​

雪奈椰子

ios打包

有什么好用的云渲染?这篇文章给你答案

Renderbus瑞云渲染农场

云渲染 好用的云渲染平台

恭喜龙蜥获得中国开源云联盟2022年度中国“最佳开源实践案例”和“杰出开源贡献者”奖项

OpenAnolis小助手

开源 龙蜥社区 COSCL 木兰峰会 中国开源云联盟

《PyTorch 深度学习实战》学习笔记--Mac M1 安装PyTorch2.0

IT蜗壳-Tango

苹果开发者账号可以多人使用吗​​​

雪奈椰子

ios打包

数智化转型进入“精装时代”,容联云助力千行百业加速上云用数赋智

脑极体

Studio One2023终于有了视频支持,可以方便做视频配乐了

茶色酒

Studio One2023

2022年中国潮流运动消费发展白皮书

易观分析

运动 潮流运动 潮流

模块五作业

张贺

React源码分析7-state计算流程和优先级

flyzz177

React

程序摄像头Trace Profiling:生产环境10分钟黄金时间快速排障手册

KINDLING

Java Linux 运维 ebpf

聊聊降本提效这件事儿

阿里巴巴中间件

阿里云 容器 云原生 中间件 降本增效

微软提出通用解码器 X-Decoder,支持图像分割和语言分词

Zilliz

算法模型

对话开发者:Serverless 落地的困境与破局

Serverless Devs

阿里云 Serverless 云原生

小程序原理之: WXSS 编译

Speedoooo

小程序 小程序技术 小程序编译

写了2年文章的我,昨天第一次露脸直播。

王中阳Go

深度思考 高效工作 学习方法 程序员 微服务架构

2023年,祝你有个好习惯!

石云升

习惯 年终总结 1月月更

React源码分析6-hooks源码

flyzz177

React

openEuler委员会主席江大勇:激发原创力量,逐梦数智未来

openEuler

数据库 开源 操作系统 openEuler 资讯

重磅 | 九科信息加入深圳市智能制造产业促进会,共促智能制造产业健康发展

九科Ninetech

第一行代码——Android(二):掌握活动的生命周期_移动_郭霖_InfoQ精选文章