天天看点

Android学习记录——2.活动

文章目录

  • ​​1.活动是什么​​
  • ​​2.活动的基本用法​​
  • ​​2.1 手动创建活动​​
  • ​​2.2 创建和加载布局​​
  • ​​2.3 在AndroidManifest文件中注册​​
  • ​​2.4 在活动中使用Toast​​
  • ​​2.5 在活动中使用Menu​​
  • ​​2.6 销毁一个活动​​
  • ​​3.使用Intent在活动之间穿梭​​
  • ​​3.1 使用显式Intent​​
  • ​​3.2 使用隐式Intent​​
  • ​​3.3 更多隐式Intent的用法​​
  • ​​3.4 向下一个活动传递数据​​
  • ​​3.5 返回数据给上一个活动​​
  • ​​4.活动的生命周期​​
  • ​​4.1 返回栈​​
  • ​​4.2 活动状态​​
  • ​​4.3 活动的生存期​​
  • ​​4.4 体验活动的生命周期​​
  • ​​4.5 活动被回收了怎么办​​
  • ​​5.活动的启动模式​​
  • ​​5.1 standard​​
  • ​​5.2 singleTop​​
  • ​​5.3 singleTask​​
  • ​​5.4 singleInstance​​
  • ​​6.活动的最佳实现​​
  • ​​6.1 知晓当前是在哪一个活动​​
  • ​​6.2 随时随地退出程序​​
  • ​​6.3 启动活动的最佳写法​​

1.活动是什么

活动(Activity)是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个活动,但不包含任何活动的应用程序很少见,谁也不想让自己的应用永远无法被用户看到吧?

2.活动的基本用法

2.1 手动创建活动

创建活动

创建ActivityTest项目成功后(在Add an Activity to Mobile界面中不再选择Empty Activity,而是选择Add No Activity),仍然会默认使用Android模式的项目结构,这里我们手动改成Project模式。目前ActivityTest项目中虽然还是会自动生成很多文件,但是app/src/main/java/com.example.activitytest目录应该是空的了,如下图所示:

Android学习记录——2.活动

现在右击com.example.activitytest包->New->Activity->Empty Activity,会弹出一个创建活动的对话框,我们将活动命名为FirstActivity,并且不要勾选Generate Layout File和Launcher Activity这两个选项,如图所示:

Android学习记录——2.活动

其中:

  1. 勾选Generate Layout File:表示会自动为FirstActivity创建一个对应的布局文件。
  2. 勾选Launcher Activity:表示会自动将FirstActivity设置为当前项目的主活动。
  3. 勾选BackwardsCompatibility:表示会为项目启用向下兼容的模式。

你需要知道,项目中的任何活动都应该重写Activity的onCreate()方法,而目前我们的FirstActivity中已经重写了这个方法,这是由Android Studio自动帮我们完成的,代码如下所示:

Android学习记录——2.活动

可以看到,onCreate()方法非常简单,就是调用了父类的onCreate()方法(super.onCreate():直接访问并调用父类中的方法)。

2.2 创建和加载布局

创建布局

在项目的目录中,右击app/src/main/res目录->New->Directory,会弹出一个新建目录的窗口,这里先创建一个名为layout的目录。然后对着layout目录右键->New->Layout resource file,又会弹出一个新建布局资源文件的窗口,我们将这个布局文件命名为first_layout,根元素就默认选择为LinearLayout,如下图所示:

Android学习记录——2.活动

点击OK完成布局的创建,这时候可以看到如下图所示的布局编辑器:

Android学习记录——2.活动

这是Android Studio为我们提供的可视化布局编辑器,你可以在屏幕的中央区域浏览当前的布局。在窗口的右下方有两个切换卡,左边是Design,右边是Text。其中:

  1. Design:是当前的可视化布局编辑器,在这里你不仅可以浏览当前的布局,还可以通过拖放的方式编辑布局。
  2. Text:是通过XML文件的方式来编辑布局的。

现在点击一下Text切换卡,可以看到如下代码:

Android学习记录——2.活动

由于我们刚才在创建布局文件时选择了LinearLayout作为根元素,因此现在布局文件中已经有一个LinearLayout元素了。我们现在对这个布局稍做编辑,添加一个按钮,如下所示:

Android学习记录——2.活动

这里添加了一个Button元素,并在Button元素的内部增加了几个属性,其中:

  1. android:id:给当前的元素定义一个唯一标识符,之后可以在代码中对这个元素进行操作。如果你需要在XML中引用一个id,就使用@id/id_name这种语法;而如果你需要在XML中定义一个id,就使用@+id/id_name这种语法。
  2. android:layout_width:指定了当前元素的宽度,这里使用match_parent表示让当前元素和父元素一样宽。
  3. android:layout_height:指定了当前元素的高度,这里使用wrap_content表示让当前元素的高度只要能刚好包含里面的内容就行。
  4. android:text:指定了元素中显示的文字内容。

    现在按钮已经添加了,可以通过右侧工具栏的Preview来浏览一下当前布局,如下图所示:

  5. Android学习记录——2.活动
  6. 可以看到,按钮已经成功显示出来了,这样一个简单的布局就编写完成了。接下来要做的,就是在活动中加载这个布局。

    重新回到FirstActivity,在onCreate()方法中加入如下代码:

  7. Android学习记录——2.活动
  8. 可以看到,这里调用了setContentView()方法来给当前的活动加载一个布局,而在setContenView()方法中,我们一般都会传入一个布局文件的id。项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此我们刚才创建的first_layout.xml布局的id现在应该是已经添加到R文件中了。只需要调用R.layout.first_layout就可以得到first_layout.xml布局的id,然后将这个值传入setContentView()方法即可。

2.3 在AndroidManifest文件中注册

所有的活动都要在AndroidManifest.xml中进行注册才能生效,而实际上,FirstActivity已经在AndroidManifest.xml中进行过注册了,打开app/src/main/AndroidManifest.xml,代码如下所示:

Android学习记录——2.活动

可以看到,活动的注册声明要放在​

​<application>​

​​标签内,这里是通过​

​<activity>​

​标签来对活动进行注册的。每当活动被创建时,Android Studio会自动在AndroidManifest.xml完成活动的注册。

在​

​<activity>​

​​标签中我们使用了android:name来指明具体注册哪一个活动,这里填入的.FirstActivity其实就是com.example.activitytest.FirstActivity的缩写而已。由于在最外层的​

​<manifest>​

​标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注册活动时这一部分就可以省略了,直接使用.FirstActivity就足够了。

不过,仅仅是这样注册了活动,我们的程序仍然是不能运行的,因为还没有为程序配置主活动。也就是说,当程序运行起来的时候,不知道要首先启动哪个活动。配置主活动的方法,就是在​

​<activity>​

​​标签的内部加入​

​<intent-filter>​

​​标签,并在这个标签里添加​

​<action android:name="android.intent.action.MAIN"/>​

​​和​

​<category android:name="android.intent.category.LAUNCHER"/>​

​这两句声明即可。

除此之外,我们还可以使用android:label指定活动中标题栏的内容,标题栏是显示在活动最顶部的,待会儿运行的时候就能看到。需要注意的是,给主活动指定的label不仅会成为标题栏中的内容,还会成为启动器(Launcher)中应用程序显示的名称。

修改AndroidManifest.xml文件,代码如下所示:

Android学习记录——2.活动

这样的话,FirstActivity就成为我们这个程序的主活动了,即点击桌面应用程序图标时首先打开的就是这个活动。另外需要注意的是,如果你的应用程序没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是你无法在启动器中看到或打开这个程序。这种程序一般都是作为第三方服务供其他应用在内部进行调用的,例如支付宝快捷支付服务。

现在,运行一下程序,结果如下图所示:

Android学习记录——2.活动

在界面的最顶部是一个标题栏,界面显示着我们刚才在注册活动时指定的内容。标题栏的下面就是在布局文件first_layout.xml中编写的界面,还可以看到我们刚刚定义的按钮。

2.4 在活动中使用Toast

Toast是Android系统提供的一种非常好的提醒方式,在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间,我们现在就试一下在活动中使用Toast。

首先需要定义一个弹出Toast的触发点,正好界面上有个按钮,那我们就让点击这个按钮的时候弹出一个Toast吧。在onCreate()方法中添加如下代码:

Android学习记录——2.活动

在活动中,可以通过findViewById()方法获取到在布局文件中定义的元素,这里我们传入R.id.button_1,来得到按钮的实例,这个值是刚才在first_layout.xml中通过android:id属性指定的。findViewById()方法返回的是一个View对象,我们需要向下转型将它转成Button对象。得到按钮的实例之后,我们通过调用setOnClickListener()方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()方法。因此,弹出Toast的功能当然是要在onClick()方法中编写了。

Toast的用法非常简单,通过静态方法makeText()创建出一个Toast对象,然后调用show()将Toast显示出来就可以了。这里需要注意的是,makeText()方法需要传入3个参数。第一个参数是Context,也就是Toast要求的上下文,由于活动本身就是一个Context对象,因此这里直接传入FirstActivity.this即可。第二个参数是Toast显示的文本内容,第三个参数是Toast显示的时长,有两个内置常量可以选择Toast.LENGTH_SHORT和Toast.LENGTH_LONG。

现在重新运行一下程序,并点击一下按钮,效果如图所示:

Android学习记录——2.活动

2.5 在活动中使用Menu

Android给我们提供了一种方式,可以让菜单都能得到展示的同时,还能不占用任何屏幕空间。

首先在res目录下新建一个menu文件夹,右击res目录->New->Directory,输入文件夹名menu,点击OK。接着在这个文件夹下再新建一个名叫main的菜单文件,右击menu文件夹->New->Menu resource file ,如图所示:

Android学习记录——2.活动

文件名输入main,点击OK完成创建。然后在main.xml中添加如下代码:

Android学习记录——2.活动

这里我们创建了两个菜单项,其中标签就是用来创建具体的某一个菜单项,然后通过android:id给这个菜单项指定一个唯一的标识符,通过android:title给这个菜单项指定一个名称。

接着重新回到FirstActivity中来重写onCreateOptionsMenu()方法,重写方法可以使用Ctrl+O快捷键,如图所示:

Android学习记录——2.活动

然后在onCreateOptionMenu()方法中编写如下代码:

Android学习记录——2.活动

通过getMenuInflater()方法能够得到MenuInflater对象,再调用它的inflate()方法就可以给当前活动创建菜单了。inflate()方法接受两个参数,第一个参数用于指定我们通过哪一个资源文件夹创建菜单,这里当然传入R.menu.main。第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当中,这里直接使用onCreateOptionMenu()方法中传入的menu参数。然后给这个方法返回true,表示允许创建的菜单显示出来,如果返回了false,创建的菜单将无法显示。

仅仅让菜单显示出来是不够的,我们定义菜单不仅是为了看的,关键是要菜单真正可用才行,因此还要再定义菜单响应事件。在FirstActivity中重写onOptionsItemSelected()方法:

Android学习记录——2.活动

在onOptionsItemSelected()方法中,通过调用item.getItemId()来判断我们点击的是哪一个菜单项,然后给每个菜单项加入自己的逻辑处理,这里就活学活用,弹出一个Toast。

重新运行程序,可以看到在标题栏的右侧多了一个三点的符号,这个就是菜单按钮,效果如图所示:

Android学习记录——2.活动

2.6 销毁一个活动

想要销毁一个活动,除了按下Back键之外,Activity类提供了一个finish()方法,我们在活动中调用一下这个方法就可以销毁当前活动了。

修改监听器的方法,如图所示:

Android学习记录——2.活动

重新运行程序,点击一下按钮,当前的活动就会被成功摧毁,这跟按下Back键是一个效果

3.使用Intent在活动之间穿梭

3.1 使用显式Intent

创建一个新的项目,选择Empty Activity,但是在创建布局时不要勾选Launcher Activity选项。

新建一个名为SecondActivity的活动,并且让其自动生成布局文件activity_second.xml,随后修改其布局文件,代码如图所示:

Android学习记录——2.活动

在布局中添加一个按钮,按钮上显示Button 2。

之后,不用修改SecondActivity.java中的代码,保持默认即可。

接下来,需要在AndroidManifest.xml中注册活动,不过AndroidStudio已经帮我们自动完成了,代码如图所示:

Android学习记录——2.活动

由于SencondActivity不是主活动,所以不需要配置标签里的内容,注册活动的代码也变得简单了很多。为了从第一个活动跳转到第二个活动,这里需要引入一个新的概念:Intent

Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务以及发送广播等场景。

Intent分为两种:显示Intent和隐式Intent,这里就先来看一下显式Intent如何使用。

Intent有多个构造函数的重载,其中一个是

Intent(Context packageContetxt,Class<?> cls)      

这个构造函数接受两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数Class则是指定想要启动的目标活动,通过这个构造函数就可以构建出Intent的“意图”

那么我们应该怎么使用这个Intent呢?Activity类中提供了一个StartActivity()方法,这个方法是专门用于启动活动的,它接收一个Intent参数,这里将构建好的Intent传入StartActivity()方法就可以启动目标活动了。

修改MainActivity中按钮的点击事件,代码如图所示:

Android学习记录——2.活动

这里构建出了一个Intent,传入MainActivity.this作为上下文,传入SecondActivity.class作为目标活动,这样的话,就可以在MainActivity这个活动的基础上打开SecondActivity这个活动,然后通过startActivity()方法来执行这个Intent。

运行程序,点击按钮,就可以从第一个活动跳转到第二个活动了。如果想要返回上一个活动,只需按下Back键销毁当前活动,从而就能回到上一个活动了。

3.2 使用隐式Intent

相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要去启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent并帮我们找出合适的活动去启动。

通过在​

​<activity>​

​​标签下配置​

​<intent-filter>​

​的内容,可以指定当前活动能够响应action和category,修改AndroidManifest.xml中的代码,如图所示:

Android学习记录——2.活动

在​

​<action>​

​​标签中我们指明了当前活动可以响应com.mxt.firstandroidapplication.ACTION_START这个action,而​

​<category>​

​​标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category。只有​

​<action>​

​​和​

​<category>​

​中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能够响应该Intent。

修改MainActivity中按钮的点击事件,代码如图所示:

Android学习记录——2.活动

可以看到,这里使用了Intent的另一个函数,直接将action的字符串传了进去,表明我们想要启动能够响应com.mxt.firstandroidapplication.ACTION_START这个action的活动。之前有提到过只有​

​<action>​

​​和​

​<category>​

​中的内容同时能够匹配上Intent中指定的action和category时才能响应,而这里因为android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法的时候会自动将这个category添加到Intent中。

重新运行程序,可以发现跟之前调用显式Intent一样,发生了活动的跳转。不同的是,这里使用的隐式Intent的方式来启动的。

每个Intent中只能指定一个action,但却能指定多个category,面前我们的Intent中只有一个默认的category,现在就来再增加一个,修改按钮的点击事件,如图所示:

Android学习记录——2.活动

可以调用Intent中的addCategory()方法来添加一个category,这里我们指定了一个自定义的category,值为com.mxt.firstandroidapplication.MY_CATEGORY

重新运行程序,点击一下按钮,就会发现程序崩溃了,如图所示:

Android学习记录——2.活动

观察日志台,查看错误日志,可以看出发生崩溃的原因是没有任何一个活动能够相应我们的Intent。因为刚刚在Intent中新增了一个category,而SecondActivity的​

​<intent-filter>​

​​标签中并没有声明可以相应这个category,所以就出现了没有任何活动可以响应该Intent的情况,在​

​<intent-filter>​

​中再添加一个category的声明,代码如图所示:

Android学习记录——2.活动

重新运行程序,一切正常。

3.3 更多隐式Intent的用法

通过上一节的学习,我们掌握了隐式Intent来启动活动的方法,但实际上隐式Intent还有更多的内容需要你去了解,本节就来介绍一下。

使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。

例如,在应用程序中展示一个网页,不需要去实现一个浏览器,只需要调用系统的浏览器打开这个网页即可,修改按钮点击事件的代码,如图所示:

Android学习记录——2.活动

这里指定了Intent的action是Intent.ACTION_VIEW,这是一个Andorid系统内置的动作,其常量值为android.intent.action.VIEW,然后通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用Intent的setData()方法将这个Uri对象传递进去。

运行程序,效果如下所示:

Android学习记录——2.活动

与此对应,我们还可以在​

​<intent-filter>​

​​标签中再配置一个​

​<data>​

​​标签,用于更精确地指定当前活动能够响应什么类型的数据。​

​<data>​

​标签中主要可以配置以下内容:

  • android:scheme:用于指定数据的协议部分,如上例中的http部分
  • android:host:用于指定数据的主机名部分,如上例中的www.baidu.com部分
  • android:port:用于指定数据的端口部分,一般紧随在主机名之后
  • android:path:用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容
  • android:mimetype:用于指定可以处理的数据类型,允许使用通配符的方式进行指定

只有<data>标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。不过一般在<data>标签中都不会指定过多的内容,如上面浏览器示例中,其实只需要指定android:scheme为http,就可以响应所有的http协议的Intent 了。

为了让你能够更加直观地理解,我们来自己建立一个活动,让它也能响应打开网页的Intent。

新建一个名为ThirdActivity的活动,在其默认布局中添加一个按钮,如图所示:

Android学习记录——2.活动

ThirdActivity.java代码不用改变,只需要在AndroidManifest.xml中修改注册信息即可,代码如下:

Android学习记录——2.活动

我们在ThirdActivity的中配置了当前活动能够响应的action是Intent. ACTION_VIEW的常量值,而category则毫无疑问指定了默认的category值,另外在 标签中我们通过android:scheme指定了数据的协议必须是http协议,这样ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent 了。运行程序,点击按钮,效果如图:

Android学习记录——2.活动

可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。选择 Browser还会像之前一样打开浏览器,并显示百度的主页,而如果选择了 ActivityTest,则会启动 ThirdActivity。 JUST ONCE表示只是这次使用选择的程序打开,ALWAYS则表示以后一直都使用 这次选择的程序打开。需要注意的是,虽然我们声明了 ThirdActivity是可以响应打开网页的Intent 的,但实际上这个活动并没有加载并显示网页的功能,所以在真正的项目中尽量不要岀现这种有 可能误导用户的行为,不然会让用户对我们的应用产生负面的印象。

除了 http协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打 电话。下面的代码展示了如何在我们的程序中调用系统拨号界面:

Android学习记录——2.活动

首先指定了 Intent的action是Intent .ACTION_DIAL,这又是一个Android系统的内置动 作。然后在data部分指定了协议是tel,号码是10086。重新运行一下程序,在MainActivity的界 面点击一下按钮,运行程序,结果如图所示:

Android学习记录——2.活动

3.4 向下一个活动传递数据

经过前面几节的学习,我们已经对Intent有了一定的了解。不过到目前为止,我们都只是简单地使用Intent来启动一个活动,其实Intent还可以在启动活动的时候传递数据,下面我们来一起看一下。

在启动活动时传递数据的思路很简单,Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,启动了另一个活动后,只需要把这些数据再从Intent 中取岀就可以了。比如说MainActivity中有一个字符串,现在想把这个字符串传递到Second-Activity 中,你就可以这样编写:

Android学习记录——2.活动

这里我们还是使用显式Intent的方式来启动SecondActivity,并通过putExtra()方法传递了 一个字符串。注意这里putExtra()方法接收两个参数,第一个参数是键,用于后面从Intent中取值,第二个参数才是真正要传递的数据。

然后我们在SecondActivity中将传递的数据取出,并打印出来,代码如下所示:

Android学习记录——2.活动

首先可以通过getlntent()方法获取到用于启动SecondActivity的Intent,然后调用 getStringExtra()方法,传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()方法来获取传递的数据。如果传递的是整型数据,则 使用getIntExtra()方法;如果传递的是布尔型数据,则使用getBooleanExtra()方法,以此类推。

重新运行程序,在MainActivity的界面点击一下按钮会跳转到SecondActivity,查看日志打印信息,如图所示:

Android学习记录——2.活动

可以看到,我们在SecondActivity中成功得到了从MainActivity传递过来的数据。

3.5 返回数据给上一个活动

既然可以传递数据给下一个活动,那么能不能够返回数据给上一个活动呢?答案是肯定的。 不过不同的是,返回上一个活动只需要按一下Back键就可以了,并没有一个用于启动活动Intent来传递数据。通过查阅文档你会发现,Activity中还有一个startActivityForResult ()方法也 是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。毫无疑 问,这就是我们所需要的。

startActivityForResult ()方法接收两个参数,第一个参数还是Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源。我们还是来实战一下,修改M爱你Activity中按钮的 点击事件,代码如图所示:

Android学习记录——2.活动

这里我们使用了 startActivityForResult()方法来启动SecondActivity,请求码只要是一 个唯一值就可以了,这里传入了 l。接下来我们在SecondActivity中给按钮注册点击事件,并在 点击事件中添加返回数据的逻辑,代码如图所示:

Android学习记录——2.活动

可以看到,我们还是构建了一个Intent,只不过这个Intent仅仅是用于传递数据而已,它没有指定任何的“意图”。紧接着把要传递的数据存放在Intent中,然后调用了setResult()方法。这个方法非常重要,是专门用于向上一个活动返回数据的。setResult()方法接收两个参数, 第一个参数用于向上一个活动返回处理结果,一般只使用RESULT_0K或RESULT_CANCELED这两个值,第二个参数则把带有数据的Intent传递回去,然后调用了 finish(()方法来销毁当前活动。

由于我们是使用 startActivityForResult()方法来启动 SecondActivity 的,在 SecondActivity 被销毁之后会回调上一个活动的onActivityResult()方法,因此我们需要在MainActivity中重写这个方法来得到返回的数据,如下所示:

Android学习记录——2.活动

onActivityResult()方法带有三个参数,第一个参数requestcode,即我们在启动活动时传入的请求码。第二个参数resultCode,即我们在返回数据时传入的处理结果。第三个参数 data,即携带着返回数据的Intent。由于在一个活动中有可能调用startActivityForResult()方法去启动很多不同的活动,每一个活动返回的数据都会回调到onActivityResult()这个方法中,因此我们首先要做的就是通过检查requestcode的值来判断数据来源。确定数据是从 SecondActivity返回的之后,我们再通过resultCode的值来判断处理结果是否成功。最后从data 中取值并打印出来,这样就完成了向上一个活动返回数据的工作。

重新运行程序,在MainActivity的界面点击按钮会打开SecondActivity,然后在SecondActivity 界面点击Button 2按钮会回到MainActivity,这时查看logcat的打印信息,结果如图所示:

Android学习记录——2.活动

可以看到,SecondActivity已经成功返回数据给FirstActivity 了。

这时候你可能会问,如果用户在SecondActivity中并不是通过点击按钮,而是通过按下Back 键回到MainActivity,这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,我们 可以通过在SecondActivity中重写onBackPressed ()方法来解决这个问题,代码如图所示:

Android学习记录——2.活动

这样的话,当用户按下Back键,就会去执行onBackPressedO方法中的代码,我们在这里添加返回数据的逻辑就行了。

4.活动的生命周期

4.1 返回栈

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

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

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

示意图如下所示:

Android学习记录——2.活动

4.2 活动状态

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

  1. 运行状态

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

  2. 暂停状态

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

  3. 停止状态

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

  4. 销毁状态

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

4.3 活动的生存期

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官方提供了一张活动生命周期的示意图,如图所示:

Android学习记录——2.活动

4.4 体验活动的生命周期

这段示例代码过长,建议参考《第一行代码 Android》的原文说明,这里只作简单说明。

假设A是主活动,B是普通活动,C是对话框式活动(即只会浮现出一个对话框,不会跳转到新页面)。A活动里有两个按钮,分别可以进入B和C活动。假设A活动里重写了上一节提到的7个与活动的生命周期有关的回调方法,并且均加入了日志输出语句。

  • 启动程序,自动跳转到A活动,会调用onCreate、onStart、onResume方法(A活处于运行状态)
  • 从A活动中点击第一个按钮,进入B活动,会调用onPause、onStop方法(A活动处于停止状态)
  • 在B活动中按下Back键,退回A活动,会调用onRestart、onStart、onResume方法(A活动处于运行状态)
  • 从A活动中点击第二个按钮,进入C活动,会调用onPause方法(A活动处于暂停状态)
  • 在C活动中按下Back键,退回A活动,会调用onResume方法(A活动处于运行状态)
  • 在A活动中按下Back键,退出应用,会调用onPause、onStop、onDestroy方法(A活动处于销毁状态)

4.5 活动被回收了怎么办

前面我们已经说过,当一个活动进入到了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个活动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()方法保存字符串,使用putlnt()方法保存整型数 据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。

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

Android学习记录——2.活动

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

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

Android学习记录——2.活动

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

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

5.活动的启动模式

在实际项目中,我们应该根据特定的需求为每个活动指定恰当的启动模式。启动模式一共有4种,分别是standard,singleTop,singleTask和 singlelnstance,可以在 AndroidManifest.xml 中通过给​

​<activity>​

​标签指定 android: launchMode 属性来选择启动模式。下面我们来逐个进行学习。

5.1 standard

standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。因此,到目前为止我们写过的所有活动都是使用的standard模式。经过上一节的学习, 你已经知道了Android是使用返回栈来管理活动的,在standard模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动, 系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。

Android学习记录——2.活动

5.2 singleTop

可能在有些情况下,你会觉得standard模式不太合理。活动明明已经在栈顶了,为什么再次启动的时候还要创建一个新的活动实例呢?别着急,这只是系统默认的一种启动模式而已,你完 全可以根据自己的需要进行修改,比如说使用singleTop模式。当活动的启动模式指定为 singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。

Android学习记录——2.活动

5.3 singleTask

用singleTop模式可以很好地解决重复创建栈顶活动的问题,但是正如你在上一节所看到 的,如果该活动并没有处于栈顶的位置,还是可能会创建多个活动实例的。那么有没有什么办法可以让某个活动在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实 现了。当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统 出栈,如果没有发现就会创建一个新的活动实例。

Android学习记录——2.活动

5.4 singleInstance

singlelnstance模式应该算是4种启动模式中最特殊也最复杂的一个了,你也需要多花点功夫来理解这个模式。不同于以上3种启动模式,指定为singlelnstance模式的活动会启用一个新的返回栈来管理这个活动(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返 回栈)。那么这样做有什么意义呢?想象以下场景,假设我们的程序中有一个活动是允许其他程 序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢? 使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个活动在 不同的返回栈中入栈时必然是创建了新的实例。而使用singlelnstance模式就可以解决这个问题, 在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都 共用的同一个返回栈,也就解决了共享活动实例的问题。

Android学习记录——2.活动

6.活动的最佳实现

6.1 知晓当前是在哪一个活动

这个技巧将教会你如何根据程序当前的界面就能判断出这是哪一个活动。可能你会觉得挺纳 闷的,我自己写的代码怎么会不知道这是哪一个活动呢?很不幸的是,在你真正进入到企业之后, 更有可能的是接手一份别人写的代码,因为你刚进公司就正好有一个新项目启动的概率并不高。 阅读别人的代码时有一个很头疼的问题,就是当你需要在某个界面上修改一些非常简单的东西 时,却半天找不到这个界面对应的活动是哪一个。学会了本节的技巧之后,这对你来说就再也不是难题了。

创建一个普通的类BaseActivity,并在其onCreate()方法中加入这行代码:

Log.d("BaseActivity", getClass().getSimpleName());      

需要让BaseActivity成为该项目中所有活动的父类。修改所有活动的继承结构,让它们不再继承自 AppCompatActivity, 而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中所有活动的现有功能并不受影响,它们仍然完全继承了 Activity中的所有特性。

现在,每当我们进入到一个活动的界面,该活动的类名就会被打印出来(在logcat中),这样我们就可以时时刻刻知晓当前界面对应的是哪一个活动了。

6.2 随时随地退出程序

如果目前你手机的界面还停留在一些深层活动中,你会发现当前想退出程序是非常不方便的, 可能需要连按多次Back键才行。按Home键只是把程序挂起,并没有退出程序。其实这个问题就足以引起你的思考,如果我们的程序需要一个注销或者退出的功能该怎么办呢?必须要有一个随时随地都能退出程序的方案才行。

其实解决思路也很简单,只需要用一个专门的集合类对所有的活动进行管理就可以了,下面就来实现一下。

Android学习记录——2.活动

在活动管理器中,我们通过一个List来暂存活动,然后提供了一个addActivity()方法用 于向List中添加一个活动,提供了一个removeActivity()方法用于从List中移除活动,最后提供了一个finishAll()方法用于将List中存储的活动全部销毁掉。

接下来修改BaseActivity中的代码,如下所示:

Android学习记录——2.活动

在 BaseActivity 的 onCreate()方法中调用了 ActivityCollector 的 addActivity()方法,表明将当前正在创建的活动添加到活动管理器里。然后在BaseActivity中重写onDestroy()方法,并调用了 ActivityCollector的removeActivity()方法,表明将一个马上要销毁的活 动从活动管理器里移除。

从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector. finishAll ()方法就可以了。

当然你还可以在销毁所有活动的代码后面再加上杀掉当前进程的代码,以保证程序完全退出,杀掉进程的代码如下所示:

android.os.Process.killProcess(android.os.Process.myPid());      

6.3 启动活动的最佳写法