入职小白随笔之Android四大组件——活动详解(activity)

推荐Android入门书籍:第一行代码(郭霖)

google官方文档地址:https://developer.android.google.cn/guide/components/activities#CoordinatingActivities

Activity

活动是什么?

Activity是最容易吸引用户的地方,它是一种可以包含用户界面的组件,主要用于和用户进行交互,比如执行拨打电话、拍摄照片、发送电子邮件或是查看地图等操作。一个应用程序中可以包含0个或者多个活动,但不包含任何活动的应用程序很少见,谁也不想让自己的应用永远无法被用户看到吧?

活动的基本用法

创建活动

要创建活动,必须新建一个类继承自Activity(Android系统提供的一个活动基类,我们项目中所有的活动都必须继承它或者它的子类才能拥有活动的特性),你需要知道,项目中的任何活动都应该重写Activity的onCreate()方法。这里创建我们第一个活动命名为FirstActivity,布局文件名为first_layout。


public class FirstActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

可以看到,onCreate()方法非常简单,就是调用了父类的onCreate()方法。后续,我们还可以在其中加入很多自己的逻辑。

创建和加载布局文件

Android程序的设计讲究逻辑和视图分离最好是每一个活动都能够对应着一个布局,布局就是用来显示界面内容的。通过XML文件的方式来编辑布局,这里创建一个名为first_layout.xml的文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
</LinearLayout>

现在我们来为这个布局添加一个按钮(Button)。

<?xml version="1.0" encoding="utf-8"?>
<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/button_1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 1" />

</LinearLayout>

可以看到Button内部增加了几个属性。其中,android:id是给当前元素定义了一个唯一的标志符,之后可以在代码中对该元素进行操作;使用@+id/id_name这种语法在XML文件中定义了一个id;随后android:layout_width指定了当前元素的宽度这里的match_parent表示让当前元素和父元素一样宽;android:layout_height指定了当前元素的高度,这里使用了wrap_content表示当前元素的高度只要能够刚好包含里面的内容就可以了;android:text指定了元素中显示的文字内容。这些知识熟能生巧,现在看不明白没有关系,看的多了自然而然就会得心应手。现在Button已经添加完毕。前面我们说过,Android讲究逻辑与视图分离,那么定义好的视图怎么加载到逻辑中了?这里需要用到setContentView()方法来给当前的活动加载一个布局文件,在setContentView()方法中我们一般会传入一个布局文件的id。项目中添加的任何资源都会在R文件中生成一个相应的资源id,因此,我们刚刚创建的XML布局文件的id就已经添加到R文件中了,我们只需要调用R.layout.first_layout就可以得到first_layout.xml布局的id,然后将这个值传入setContentView()方法中即可。

在AndroidManifest文件中注册活动

所有的活动都要在AndroidManifest.xml中进行注册才能生效,在Android Studio中编写时,它已经帮助我们完成了注册的任务:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.activitytest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".FirstActivity"
    </application>

</manifest>

可以看到活动的注册放在application标签内部,通过activity标签来对活动进行注册。在标签中我们使用了android:name来指定具体注册哪一个活动,细心的你会疑惑,这里的.FirstActivity是什么意思呢?其实这只不过是com.example.activitytest.FirstActivity的缩写而已。由于在最外层的标签中已经通过package属性指定了程序的包名是com.example.activitytest,因此在注册活动时这一部分就可以省略了,直接使用.FirstActivity就足够了。
不过,仅仅是这样注册了活动,我们的程序还是不能运行的,因为还没有为程序配置主活动,程序并不知道首先启动哪个活动。配置主活动的方法很简单,就是在标签的内部加入intent-filter标签,并且在这个标签里面添加action android:name="android.intent.action.MAIN" 和category android:name="android.intent.category.LAUNCHER" 这两句声明即可。
另外,我们还可以使用android:label指定活动中标题栏的内容,标题栏是显示在活动最顶部的,需要注意的是,给主活动指定的label不仅会成为标题栏的内容,还会成为启动器(Launcher)中应用程序显示的名称

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.activitytest">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".FirstActivity"
            android:label="This is FirstActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>   
    </application>
</manifest>

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

使用Intent在活动之间穿梭

在启动其中点击应用的图标只会进入到该应用的主活动,那么怎样才能由主活动跳转到其他活动呢?是不是很有意思、很好奇,那么我们现在就来一起看看吧!

使用显式Intent

这里我们再创建一个活动,命名为SecondActivity,布局文件名为second_layout。同样,我们在second_layout中定义一个按钮,按钮上显示Button 2:


<?xml version="1.0" encoding="utf-8"?>
<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/button_2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button 2"
        />

</LinearLayout>

然后SecondActivity中的代码已经自动生成了一部分,我们保持默认不变就好,如下所示:


public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.second_layout);
    }
}

另外不要忘记,任何一个活动都需要在AndroidManifest.xml中注册,只不过这一工作Android Studio已经帮我们完成了:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.activitytest">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".FirstActivity"
            android:label="This is FirstActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".SecondActivity" />
        </activity>
    </application>
</manifest>

由于SecondActivity不是主活动,因此不需要配置标签里的内容。现在第二个活动也已经创建完成了,剩下的问题就是如何去启动这个创建的第二个活动。这里我们引入了一个新的概念:Intent
Intent是Android程序中各组件之间交互的一种重要的方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。它一般可被用于启动活动、启动服务以及发送广播等场景,这里服务和广播的概念我们还没有涉及,本篇的目光锁定在启动活动上。
Intent大致可以分为两种:显式Intent隐式Intent,我们按照顺序来,首先学习一下显式Intent如何来解决这一问题。
Intent有多种构造函数的重载,其中一个是Intent(Content packageContext, Class<?>cls)。这个构造函数接收两个参数,第一个参数Context要求提供一个启动活动的上下文,第二个参数Class则是指定了想要启动的目标活动,通过这个构造函数就可以构建出Intent的意图。那么,我们怎么去使用这个Intent呢?Activity类提供了一个startActivity()方法,这个方法是专门用于启动活动的,它接受一个Intent参数,这里我们将构建好的Intent传入startActivity()方法中就可以启动目标活动了。
修改FirstActivity中按钮的点击事件,代码如下:

public class FirstActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.first_layout);
        Button button1 = (Button) findViewById(R.id.button_1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(FirstActivity.this,SecondActivity.class);
                startActivity(intent);
            }
        });
    }
}

在活动中,可以通过findViewById()方法获取到在布局文件中定义的元素,这里我们传入R.id.button_1,来得到按钮的实例,这个值是刚才在first_layout.xml中通过android:id属性指定的。findViewById()方法返回的是一个View对象,我们需要向下转型将它转成Button对象。得到按钮的实例之后,我们通过调用setOnClickListener()方法为按钮注册一个监听器,点击按钮时就会执行监听器中的onClick()方法。因此,功能当然要在onClick()方法中编写。
我们首先构建了一个Intent,传入FirstActivity.this作为上下文,传入SecondActivity.class作为目标活动,这样我们的意图就很清楚了,即在FirstActivity活动的基础上打开SecondActivity活动。然后通过startActivity()方法来执行这个Intent。
这时,运行程序,在FirstActivity活动中点击按钮即可进入SecondActivity活动中了,那么怎么返回上一个活动呢?只需要按下back键(手机的返回键)就可以销毁当前活动,从而回到上一个活动。

使用隐式Intent

相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮助我们找出合适的活动去启动。
那么,什么是合适的活动呢?简单来说就是可以响应这个隐式Intent的活动。通过在标签下配置的内容,可以指定当前活动能够相应的action和category,打开AndroidManifest.xml,添加如下代码:


<activity android:name=".SecondActivity">
            <intent-filter>
                <action android:name="com.example.activitytest.ACTION_START" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

标签中我们指明了当前活动的可以响应com.example.activitytest.ACTION_START这个action,而标签则包含了一些附加信息,更精确的指明了当前活动能够响应的Intent中还能带有的category。只有action和category中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能相应该Intent
修改FirstActivity中按钮的点击事件,代码如下所示:


button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("com.example.activitytest.ACTION_START");
                startActivity(intent);
    }
});

可以看到,我们使用了Intent的另外一个构造函数,直接将action的字符串传了进去,表明我们想要启动能够响应com.example.activitytest.ACTION_START这个action的活动。那前面不是说要action和category同时匹配上才能相应吗?怎么没有看见哪里有指定category呢?这是因为android.intent.category.DEFAULT是一种默认的category,在调用startActivity()方法的时候会自动将这个category添加到Intent中。
这时,我们便完成了和显式Intent一样的功能。
每个Intent中只能指定一个action,但却能指定多个category。目前我们的Intent中只有一个磨人的category,那么我们再来增加一个吧。
修改FirstActivity中按钮的点击事件,代码如下所示:


button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("com.example.activitytest.ACTION_START");
                intent.addCategory("com.example.activitytest.MY_CATEGORY");
                startActivity(intent);
    }
});

可以调用Intent中的addCategory()方法来添加一个category,这里我们指定了一个自定义的category,值为com.example.activitytest.MY_CATEGORY。
这时当你重新运行程序时,在FirstActivity的界面点击一下按钮,你会发现,程序崩溃了!别紧张,其实大多数的崩溃问题都是很好解决的,只要你善于分析。在logcat界面查看错误日志,你会发现错误信息中提醒我们,没有任何一个活动可以响应我们的Intent,为什么呢?这是因为我们刚刚在Intent中新增了一个category,而SecondActivity的标签中并没有声明可以响应这个category,所以就出现了没有任何活动可以响应该Intent的情况。现在我们在标签中再添加一个category的声明,如下所示:


<activity android:name=".SecondActivity">
            <intent-filter>
                <action android:name="com.example.activitytest.ACTION_START" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="com.example.activitytest.MY_CATEGORY" />
            </intent-filter>
        </activity>

此时,再运行程序,就没有问题了。

活动的生命周期

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

返回栈

任务和返回栈

活动状态

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

  • 运行状态
    当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。
  • 暂停状态
    当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动,只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西 都会在用户体验方面带来不好的影响),只有在内存极低的情况下,系统才会考虑回收这种活动。
  • 停止状态
    当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其它地方需要内存时,处于停止状态的活动有可能被系统回收。
  • 销毁状态
    当一个活动从返回栈中移除后就变成了销毁状态。系统最倾向于回收这种状态的活动,从而保证收的内存充足。

活动的生存期

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

  • onCreate()
    首次创建 Activity 时调用。 您应该在此方法中执行所有正常的静态设置 — 创建视图、将数据绑定到列表等等。
  • onStart()
    在 Activity 即将对用户可见之前调用。
    如果 Activity 转入前台,则后接 onResume(),如果 Activity 转入隐藏状态,则后接 onStop()。
  • onResume()
    在 Activity 即将开始与用户进行交互之前调用。 此时,Activity 处于 Activity 堆栈的顶层,并具有用户输入焦点。始终后接 onPause()。
  • onPause()
    当系统即将开始继续另一个 Activity 时调用。 此方法通常用于确认对持久性数据的未保存更改、停止动画以及其他可能消耗 CPU 的内容,诸如此类。 它应该非常迅速地执行所需操作,因为它返回后,下一个 Activity 才能继续执行。如果 Activity 返回前台,则后接onResume(),如果 Activity 转入对用户不可见状态,则后接 onStop()。
  • onStop()
    在 Activity 对用户不再可见时调用。如果 Activity 被销毁,或另一个 Activity(一个现有 Activity 或新 Activity)继续执行并将其覆盖,就可能发生这种情况。如果 Activity 恢复与用户的交互,则后接 onRestart(),如果 Activity 被销毁,则后接 onDestroy()。
  • onDestroy()
    在 Activity 被销毁前调用。这是 Activity 将收到的最后调用。 当 Activity 结束(有人对 Activity 调用了 finish()),或系统为节省空间而暂时销毁该 Activity 实例时,可能会调用它。 您可以通过 isFinishing() 方法区分这两种情形。
  • onRestart()
    在 Activity 已停止并即将再次启动前调用。始终后接 onStart()。

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

  • 完整生存期
    发生在 onCreate() 调用与 onDestroy() 调用之间。您的 Activity 应在 onCreate() 中执行“全局”状态设置(例如定义布局),并释放 onDestroy() 中的所有其余资源。例如,如果您的 Activity 有一个在后台运行的线程,用于从网络上下载数据,它可能会在 onCreate() 中创建该线程,然后在 onDestroy() 中停止该线程。
  • 可见生存期
    发生在 onStart() 调用与 onStop() 调用之间。在这段时间,用户可以在屏幕上看到 Activity 并与其交互。 例如,当一个新 Activity 启动,并且此 Activity 不再可见时,系统会调用 onStop()。您可以在调用这两个方法之间保留向用户显示 Activity 所需的资源。 例如,您可以在 onStart() 中注册一个 BroadcastReceiver 以监控影响 UI 的变化,并在用户无法再看到您显示的内容时在 onStop() 中将其取消注册。在 Activity 的整个生命周期,当 Activity 在对用户可见和隐藏两种状态中交替变化时,系统可能会多次调用 onStart() 和 onStop()。
  • 前台生存期
    发生在 onResume() 调用与 onPause() 调用之间。在这段时间,Activity 位于屏幕上的所有其他 Activity 之前,并具有用户输入焦点。 Activity 可频繁转入和转出前台 — 例如,当设备转入休眠状态或出现对话框时,系统会调用 onPause()。 由于此状态可能经常发生转变,因此这两个方法中应采用适度轻量级的代码,以避免因转变速度慢而让用户等待。

为了方便我们更好的理解,Android官方提供了一张活动生命周期的示意图,如下:

图1 活动的生命周期

体验活动的生命周期

讲了这么多的理论知识,也是时候该实战一下了,下面我们将通过一个实例,让你可以更加直观地体验活动的生命周期。
这次我们不准备在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文件,将里面的代码替换成如下内容:


<?xml version="1.0" encoding="utf-8"?>
<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,用于显示一行文字。然后编辑dialog_layout.xml文件,将里面的代码替换成如下内容:


<?xml version="1.0" encoding="utf-8"?>
<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 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,重新定制主活动的布局,将里面的代码替换成如下内容:


<?xml version="1.0" encoding="utf-8"?>
<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中的代码,如下所示:


package com.example.activitylifecycletest;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends BaseActivity {

    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个回调方法中分别打印了一句话,这样就可以通过观察日志的方式来更加直观地理解活动的生命周期。
现在运行程序,观察logcat中的打印日志


com.example.activitylifetest D/MainActivity:onCreate
com.example.activitylifetest D/MainActivity:onStart
com.example.activitylifetest D/MainActivity:onResume

可以看到,当MainActivity第一次被创建时会依次执行onCreate()、onStart()和onResume()方法。然后点击第一个按钮,启动NormalActivity,此时打印信息为:


com.example.activitylifetest D/MainActivity:onPause
com.example.activitylifetest D/MainActivity:onStop

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


com.example.activitylifetest D/MainActivity:onRestart
com.example.activitylifetest D/MainActivity:onStart
com.example.activitylifetest D/MainActivity:onResume

由于之前MainActivity已经进入了停止状态,所以onRestart()方法会得到执行,之后又会依次执行onStart()和onResume()方法。注意此时onCreate()方法不会执行,因为MainActivity并没有重新创建。
然后再点击第二个按钮,启动DialogActivity,此时观察打印信息为:


com.example.activitylifetest D/MainActivity:onPause

可以看到,只有onPause()方法得到了执行,onStop()方法并没有执行,这是因为DialogActivity并没有完全遮挡住MainActivity,此时MainActivity只是进入了暂停状态,并没有进入停止状态。相应地,按下Back键返回MainActivity也应该只有onResume()方法会得到执行。最后在MainActivity按下Back键退出程序,打印信息为:


com.example.activitylifetest D/MainActivity:onPause
com.example.activitylifetest D/MainActivity:onStop
com.example.activitylifetest D/MainActivity:onDestroy

依次执行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中添加如下代码可以将临时数据进行保存:


@Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        String tempData = "Something you just typed";
        outState.putString("data_key",tempData);
    }

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


@Override
    protected 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);
        }
        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);
            }
        });
    }

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

活动的启动模式

活动的启动模式对我们来说是个全新的概念,在实际项目中我们应该根据特定的需求为每一个活动指定恰当的启动模式。启动模式一共分为4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给标签指定android:launchMode属性来选择启动模式。下面我们来逐个进行学习。

standard

standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。因此,到目前为止我们写过的所有活动都是使用的standard模式。在standard模式下,没当启动一个新的活动,它就会在返回栈中存在,每次启动都会创建该活动的一个新的实例。

singleTop

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

singleTask

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

singleInstance

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

posted @ 2019-09-25 16:01  Dennis、Cui  阅读(1045)  评论(2编辑  收藏  举报