说说 Android 的活动组件(Activity)

Android 的活动是可以包含用户界面的组件,主要用于与用户进行交互。

1 手动创建活动

手动创建活动可以加深对活动的理解,所以这里我们手动创建一个活动。

1.1 创建空的活动

在 Android Studio 中新建一个新项目,模板选择 “Add No Activity”:

image.png

右击 “域名.应用名” 的包,New→Activity→Empty Activity,创建一个空的活动:

创建新的活动

勾选 Generate Layout File 会自动为 FirstActivity 创建一个对应的布局文件,勾选Launcher Activity 会自动将 FirstActivity 设置为当前项目的主活动,因为我们要手动创建,所以这里都不勾选。

项目中的任何活动都应该重写 Activity 的 onCreate() 方法,我们新创建的 FirstActivity 已经自动为我们这个方法啦:

public class FirstActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

1.2 创建并加载布局

现在手动创建一个布局文件,右击 res 目录→New→Directory,会弹出一个新建目录的窗口,这里先创建一个名为 layout 的目录。然后对着 layout 目录右键→New→Layout resource file,又会弹出一个新建布局资源文件的窗口,我们将这个布局文件命名为first_layout,根元素就默认选择为 LinearLayout:

新建布局文件

点击 OK 完成布局的创建,这时就会看到布局编辑器:


布局编辑器

可以在屏幕的中央区域预览当前的布局。在窗口的最下方有两个切换页签。Design 是当前的可视化布局编辑器,在这里不仅可以预览当前的布局,还可以通过拖放的方式来编辑布局。而 Text 则是通过 XML 文件的方式来编辑布局的。

Text 页签:

<?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>

可以看到,布局文件中已经有一个 LinearLayout 元素了,现在为它添加一个按钮:

<?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:text="按钮 1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

这里添加了一个 Button 元素:

  • android:id - 给当前的元素定义一个唯一标识符,之后可以在代码中对这个元素进行操作。
  • android:layout_width - 指定了当前元素的宽度,match_parent 表示让当前元素和父元素一样宽。
  • android:layout_height - 指定了当前元素的高度,wrap_content 表示当前元素的高度刚好包含里面的内容。
  • android:text - 指定了元素中显示的文字内容。

在右侧的 Preview 页面(如果不存在,请点击右侧工具栏的 Preview) 可以预览当前布局:


预览当前布局

现在在活动中加载这个布局。

在 FirstActivity 类的 onCreate()方法中加入布局:

public class FirstActivity extends AppCompatActivity {

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

这里调用了 setContentView() 方法来给当前的活动加载一个布局,这是通过 R 文件来获取布局资源 id。

1.3 在 AndroidManifest 文件中注册

所有的活动都要在 AndroidManifest.xml 中进行注册才能生效,实际上 FirstActivity 已经在 AndroidManifest.xml 中注册过啦:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.deniro.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"></activity>
    </application>

</manifest>

在<application>标签内注册声明活动,这里是通过 <activity> 标签来对活动进行注册的。

在 <activity> 标签中使用了 android:name 来指定具体注册哪一个活动,这里填入的.FirstActivity是 com.example.deniro.activitytest 的缩写。由于在最外层的
<manifest> 标签中已经通过 package 属性指定了程序的包名,因此在注册活动时这一部分就可以省略啦O(∩_∩)O哈哈~

现在为程序配置主活动,在 <activity> 标签的内部加入 <intent-filter> 标签,并在这个标签里添加 <action android:name="android.intent.action.MAIN"/><category android: name="android.intent.category.LAUNCHER" /> 这两句声明。

我们再使用 android:label 来指定活动中标题栏的内容,标题栏显示在活动的最顶部。给主活动指定的 label 不仅会成为标题栏中的内容,而且还会成为启动器(Launcher)中应用程序的显示名称。

<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="第一个活动">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

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

运行程序

在界面的最顶部是一个标题栏,里面显示着我们刚才在注册活动时指定的标题名称。标题栏的下面就是在布局文件 first_layout.xml 中编写的界面,里面包含我们刚刚定义的按钮。

1.4 使用 Toast 提醒方式

Toast 是 Android 系统提供的一种提醒方式,在程序中可以使用它将一些内容短小的信息通知给用户,这些信息会在一段时间后自动消失,所以不会占用屏幕空间。

使用刚才活动中的按钮作为弹出 Toast 的触发点:

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

    //使用 Toast 提醒方式
    Button button1 = (Button) findViewById(R.id.button_1);
    button1.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            Toast.makeText(FirstActivity.this, "你点击了按钮 1", Toast.LENGTH_SHORT).show();
        }
    });
}

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

通过 Toast 的静态方法 makeText() 创建一个 Toast 对象,然后调用 show() 显示出来就可以了。

makeText() 方法需要传入 3 个参数:

  • 第一个参数 - Context,也就是 Toast 要求的上下文,由于活动本身就是一个Context 对象,因此这里直接传入即可。
  • 第二个参数 - 显示的文本内容。
  • 第三个参数 - 显示的时长,有两个内置常量 Toast.LENGTH_SHORT (2 s)和Toast.LENGTH_LONG(3.5 s)。
使用 Toast 提醒

1.5 使用 Menu

手机的屏幕空间非常有限,所以充分地利用屏幕空间,在手机界面设计中就显得非常重要。如果活动中有大量的菜单需要显示,那么可以使用 Menu。

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

新建 Menu 资源文件

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

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/add_item"
        android:title="新增"></item>
    <item
        android:id="@+id/remove_item"
        android:title="删除"></item>
</menu>

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

接着重新回到 FirstActivity 中重写 onCreateOptionsMenu() 方法,重写方法可以使用 Ctrl + O 快捷键(Mac系统是 control + O)

找到 onCreateOptionsMenu 方法
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

通过 getMenuInflater( )方法能够得到 MenuInflater 对象,再调用它的 inflate() 方法就可以给当前活动创建菜单了。inflate() 方法接收两个参数,第一个参数用于指定资源文件。第二个参数用于指定菜单项将添加到哪一个 Menu 对象中。然后给这个方法返回 true,表示允许创建的菜单显示出来。

最后定义菜单响应事件,在 FirstActivity 中重写 onOptionsItemSelected() 方法:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.add_item:
            Toast.makeText(this, "点击了【新增】按钮", Toast.LENGTH_SHORT).show();
            break;
        case R.id.remove_item:
            Toast.makeText(this, "点击了【删除】按钮", Toast.LENGTH_SHORT).show();
            break;
        default:
    }
    return true;
}

在 onOptionsItemSelected( )方法中,通过调用 item.getItemId( )来判断用户点击的是哪一个菜单项,然后给每个菜单项加入自己的处理逻辑。

重新运行程序,就会发现在标题栏的右侧多了一个三点的符号,这个就是菜单按钮:

菜单

菜单里的菜单项默认是不会显示,只有点击菜单按钮才会弹出具体内容。

1.6 销毁活动

按一下 Back 键就可以销毁当前的活动。也可以使用 Activity 类提供的 finish()
方法来销毁活动。

我们在 first_layout.xml 中新增一个 Button:

<Button
    android:id="@+id/button_destroy_activity"
    android:text="销毁活动"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

然后在 FirstActivity 中为这个按钮设置点击监听事件:

//销毁活动
(findViewById(R.id.button_destroy_activity)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        finish();
    }
});
销毁活动

点击【销毁活动】按钮,就会销毁当前活动啦O(∩_∩)O哈哈~

2 在活动之间穿梭的 Intent

2.1 显式 Intent

现在在项目中再创建一个活动 SecondActivity:

创建 SecondActivity

记得勾选 Generate Layout File,但不要勾选 Launcher Activity。

点击 Finish 完成创建,Android Studio 会自动生成 SecondActivity.java 和second_layout.xml 这两个文件。

修改 second_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>

然后在 AndroidManifest.xml 为 Android Studio 自动生成的 SecondActivity 加一个 label 标签:

<activity android:name=".SecondActivity"
            android:label="第二个活动"></activity>

SecondActivity 不是主活动,所以这里不需要配置 <intent-filter> 标签里的内容。现在我们使用 Intent 去启动这第二个活动。

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

Intent 有多个构造函数的重载,其中一个是 Intent(Context packageContext, Class<?> cls)。这个构造函数接收两个参数,第一个参数 Context 要求提供一个启动活动的上下文,第二个参数 Class 则是指定想要启动的目标活动,通过这个构造函数就可以构建出 Intent。Activity 类提供了一个 startActivity() 方法,这个方法是专门用于启动活动的,它接收一个 Intent 参数。

在 first_layout.xml 中再增加一个按钮:

<Button
    android:id="@+id/button_to_second"
    android:text="启动第二个活动"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

然后在 FirstActivity 类中为这个按钮添加点击监听事件:

//启动第二个活动
(findViewById(R.id.button_to_second)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(FirstActivity.this,SecondActivity.class));
    }
});

首先构建出了一个 Intent,传入 FirstActivity.this 作为上下文,传入SecondActivity.class 作为目标活动,最后通过 startActivity() 方法来执行这个Intent。

重新运行程序,在FirstActivity的界面点击一下【启动第二个活动】按钮,就会启动
SecondActivity 这个活动。

第一个活动
第二个活动

按下 Back 键就可以销毁当前活动,从而就回到上一个活动了。

使用这种方式来直接启动活动,所以称之为显式 Intent。

2.2 隐式 Intent

隐式 Intent 指定了一系列更为抽象的 action 和 category 等信息,然后交由系统去分析这个 Intent,从而找出合适的活动去启动。

在 AndroidManifest.xml 中,我们为 SecondActivity 添加 <intent-filter>,指定当前活动能够响应的 action 和 category:

<activity
    android:name=".SecondActivity"
    android:label="第二个活动">
    <intent-filter>
        <action android:name="deniro.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

</activity>

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

我们又添加了一个按钮(代码就不贴啦),然后为这个按钮添加点击事件:

//启动第二个活动(隐式)
(findViewById(R.id.button_to_second_hide)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent("deniro.ACTION_START"));
    }
});
第一个活动
第二个活动

每个 Intent 只能指定一个 action,但却能指定多个 category。

再添加了一个按钮,然后为这个按钮添加点击事件:

//启动第二个活动(隐式、带 category)
(findViewById(R.id.button_to_second_hide_category)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent("deniro.ACTION_START");
        intent.addCategory("net.deniro.MY_CATEGORY");
        startActivity(intent);
    }
});

然后在 AndroidManifest.xml 中,新增一个名为 “net.deniro.MY_CATEGORY” 的
Category:

<activity
    android:name=".SecondActivity"
    android:label="第二个活动">
    <intent-filter>
        <action android:name="deniro.ACTION_START" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="net.deniro.MY_CATEGORY" />
    </intent-filter>

</activity>

重新运行程序:

第一个活动
第二个活动

2.3 使用隐式 Intent 启动其它活动

使用隐式 Intent,不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得 Android 在多个应用程序之间实现功能共享成为可能。比如应用程序中需要展示一个网页,我们没有必要自己实现一个浏览器,只需要调用系统的浏览器来打开这个网页就可以啦O(∩_∩)O哈哈~

再添加了一个按钮,然后为这个按钮添加点击事件:

//打开网页
(findViewById(R.id.button_open_web)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse("http://www.douban.com"));
        startActivity(intent);
    }
});

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

第一个活动
打开网页

可以在 <intent-filter> 标签中配置一个 <data >标签,用于更精确地指定当前活动能够响应什么类型的数据。<data> 标签有这些属性:

属性名 说明
android:scheme 指定协议,如 http。
android:host 指定主机名或域名。
android:port 指定端口。
android:path 指定主机名和端口之后的部分。
android:mimeType 指定可以处理的数据类型,可以使用通配符。

只有 <data> 标签中指定的内容和 Intent 中携带的 Data 完全一致时,当前活动才能够响应这个 Intent。比如只需要指定 android:scheme 为 http,就可以响应所有的
http 协议的 Intent 了。

现在我们新建一个活动,让它响应所有的 http 协议的 Intent。

HttpActivity 活动类:

public class HttpActivity extends AppCompatActivity {

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

在 AndroidManifest.xml 中,配置这个活动:

<activity android:name=".HttpActivity"
    android:label="http 活动"
    >
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:scheme="http"/>
    </intent-filter>

</activity>

在 HttpActivity 的 <intent-filter> 中配置了当前活动能够响应的 action 是Intent.ACTION_VIEW 的常量值, category 指定了默认的值,在 <data> 标签中通过 android:scheme 指定了数据的协议为 http 协议,这样 HttpActivity 就跟浏览器一样,能够响应一个打开网页的 Intent 了。

在 FirstActivity 的界面中点击按钮,系统自动弹出了一个列表,显示了目前能够响应这个 Intent 的所有程序。选择 Browse r还会像之前一样打开浏览器,并显示豆瓣的主页,而如果选择了 “http 活动”,则会启动 HttpActivity。

响应 Intent 的程序列表

我们还可以指定很多其他协议,比如 geo 表示显示地理位置、tel 表示拨打电话。现在让我们试试调用系统拨号界面吧。

为新的按钮添加点击事件:

//打开拨号界面
(findViewById(R.id.open_dial_interface)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_DIAL);
        intent.setData(Uri.parse("tel:10000"));
        startActivity(intent);
    }
});
拨号界面

2.4 传递数据给下一个活动

Intent 中提供了一系列 putExtra() 方法的重载,可以想要传递的数据暂存在 Intent 中,启动另一个活动后,只需要把这些数据再从 Intent 中取出即可。

现在我们把数据从 FirstActivity 活动传递给 SecondActivity 活动。

FirstActivity :

//启动第二个活动
(findViewById(R.id.button_to_second)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
        intent.putExtra("key","value");//设置数据
        startActivity(intent);
    }
});

putExtra( )方法接收两个参数,第一个参数是键,第二个参数才是真正要传递的数据。

SecondActivity:

public class SecondActivity extends AppCompatActivity {
    private static final String TAG = "SecondActivity";

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

        //取数据
        String data = getIntent().getStringExtra("key");
        Log.d(TAG, "onCreate: " + data);
    }
}

这里由于我们传递的是字符串,所以使用 getStringExtra() 方法来获取传递的数据。如果传递的是整型数据,则使用 getIntExtra() 方法;如果传递的是布尔型数据,则使用 getBooleanExtra() 方法,以此类推。

运行后,查看日志:

02-15 08:32:10.037 3856-3856/com.example.deniro.activitytest D/SecondActivity: onCreate: value

2.5 返回数据给上一个活动

Activity 中有一个 startActivityForResult() 方法也可以启动活动,而且这个方法在当前活动销毁后能够返回一个结果给上一个活动。

startActivityForResult() 方法接收两个参数,第一个参数是 Intent,第二个参数是请求码,用于在之后的回调中判断数据的来源。

我们现在演示从 SecondActivity 返回数据给 FirstActivity:

修改 FirstActivity 的按钮点击事件:

(findViewById(R.id.button_to_second_hide)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivityForResult(new Intent("deniro.ACTION_START"), 1);
    }
});

这里使用了 startActivityForResult() 方法来启动SecondActivity,请求码必须唯一!

现在在 SecondActivity 中为按钮注册点击事件,并在点击事件中添加返回数据的逻辑:

public class SecondActivity extends AppCompatActivity {
    private static final String TAG = "SecondActivity";

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

        //取数据
        String data = getIntent().getStringExtra("key");
        Log.d(TAG, "onCreate: " + data);

        //返回第二个活动
        (findViewById(R.id.return_first)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.putExtra("return_key", "return_value");
                setResult(RESULT_OK, intent);
                finish();
            }
        });
    }
}

这里的 setResult() 方法用于返回数据给上一个活动。setResult() 方法接收两个参数,第一个参数用于向上一个活动返回处理结果,一般使用 RESULT_OK 或 RESULT_CANCELED 这两个值;第二个参数会把带有数据的 Intent 传递回去。

使用 startActivityForResult() 方法启动活动后,如果这个活动被销毁,那么系统会回调上一个活动的 onActivityResult() 方法,所以我们在 FirstActivity 中重写这个方法来得到返回的数据:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case 1:
            if (resultCode == RESULT_OK) {
                String returnData = data.getStringExtra("return_key");
                Log.d(TAG, "onActivityResult: " + returnData);
            }
            break;
        default:
            break;
    }
}

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

打印日志:

02-15 08:56:21.623 22971-22971/com.example.deniro.activitytest D/FirstActivity: onActivityResult: return_value

如果用户在 SecondActivity 中并不是通过点击按钮,而是通过按下 Back 键回到 FirstActivity 的,这时就要通过重写 onBackPressed() 方法来实现数据返回啦。

/**
 * 返回第一个活动
 */
private void returnFirstActivity() {
    Intent intent = new Intent();
    intent.putExtra("return_key", "return_value");
    setResult(RESULT_OK, intent);
    finish();
}

@Override
public void onBackPressed() {
    returnFirstActivity();
}

因为在多处用到了 “返回第一个活动” 的逻辑,所以这里把它抽象为一个方法,这可是一个推荐的实践方法哦O(∩_∩)O哈哈~

3 活动的生命周期

3.1 返回栈

Android 中的活动是可以层叠的。每启动一个新的活动,就会覆盖在原活动之上,然后点击
Back 键会销毁最上面的活动,下面的一个活动就会重新显示出来。

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

返回栈

3.2 活动状态

活动有 4 种状态。

  • 运行状态:活动位于栈顶时,就处于运行状态。
  • 暂停状态:当一个活动不再处于栈顶位置,但仍然可见时,就进入暂停状态。
  • 停止状态:当一个活动不再处于栈顶位置,并且完全不可见时,就进入停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。
    *销毁状态:当一个活动从栈中移除后就变成销毁状态。系统最倾向于回收处于这种状态的活动,从而保证手机内存充足。

3.3 活动生存期

Activity 类中定义了 7 个回调方法,覆盖了活动生命周期的每一个环节。

回调方法 说明
onCreate() 在活动第一次被创建时调用。一般在这个方法中完成活动的初始化操作,比如加载布局、绑定事件等。
onStart() 活动由不可见变为可见时调用。
onResume() 在活动准备好和用户进行交互时调用。此时的活动一定位于栈顶,并且处于运行状态。
onPause() 在系统准备去启动或者恢复另一个活动时调用。通常会在这个方法中将一些消耗的 CPU 资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,要不会影响到新的栈顶活动的使用。
onStop() 在活动完全不可见时调用。它和 onPause() 方法的区别在于,如果启动的新活动是一个对话框,那么 onPause() 方法会得到执行,而 onStop() 方法则不会。
onDestroy() 在活动被销毁前调用。
onRestart() 在活动由停止状态变为运行状态之前调用,即活动被重新启动咯。

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

  • 完整生存期:活动在 onCreate() 方法和 onDestroy() 方法之间所经历的,就是完整生存期。一般情况下,一个活动会在 onCreate() 方法中完成各种初始化操作,而在 onDestroy() 方法中完成释放内存的操作。
  • 可见生存期:活动在 onStart() 方法和 onStop() 方法之间所经历的,就是可见生存期。在可见生存期内,活动对于用户总是可见的,即便有可能无法和用户进行交互。可以通过这两个方法,合理地管理那些对用户可见的资源。比如在 onStart() 方法中对资源进行加载,而在
    onStop() 方法中对资源进行释放,从而保证处于停止状态的活动不会占用过多内存。
  • 前台生存期:在 onResume() 方法和 onPause( )方法之间所经历的就是前台生存期。在前台生存期内,活动总是处于运行状态的,此时的活动是可以和用户进行交互的。
活动的生命周期

3.4 体验活动的生命周期

我们创建两个子活动——NormalActivity 和 DialogActivity,这样更加直观地体验活动的生命周期。

注意:用 Android Studio 创建 DialogActivity 后,记得把它的继承类改为 Activity,否则会抛出
“You need to use a Theme.AppCompat theme (or descendant) with this activity.” 的错误。

public class DialogActivity extends Activity {

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

把 NormalActivity 的布局文件内容修改为:

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

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="正常的活动" />
</LinearLayout>

这里使用了一个 TextView,用于显示一行文字。

把 DialogActivity 的布局文件内容也做类似的修改,只是 android:text 的值设置不同。

现在修改 AndroidManifest.xml 的名为 DialogActivity 的 <activity> 标签的配置:

 <activity
            android:name=".DialogActivity"
            android:theme="@android:style/Theme.Dialog"></activity>

我们让 DialogActivity 使用对话框式的主题。

接着再 FirstActivity 的布局文件中加入两个按钮:

<Button
    android:id="@+id/start_normal_activity"
    android:text="启动正常的活动"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

<Button
    android:id="@+id/start_dialog_activity"
    android:text="启动对话框式的活动"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

一个用于启动 NormalActivity,一个用于启动 DialogActivity。

然后在 FirstActivity 类的 onCreate 方法中为两个按钮注册了点击事件:

//启动正常的活动
(findViewById(R.id.start_normal_activity)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(FirstActivity.this, NormalActivity.class));
    }
});


//启动对话框式的活动
(findViewById(R.id.start_dialog_activity)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(FirstActivity.this, DialogActivity.class));
    }
});

最后日志打印活动的 7 个回调方法:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate");
       ...
}

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

这样就可以通过观察日志的方式来更加直观地理解活动的生命周期。

运行程序:


FirstActivity 类

日志:

02-16 08:42:19.660 6658-6658/com.example.deniro.activitytest D/FirstActivity: onCreate
02-16 08:42:19.848 6658-6658/com.example.deniro.activitytest D/FirstActivity: onStart
02-16 08:42:19.850 6658-6658/com.example.deniro.activitytest D/FirstActivity: onResume

当 FirstActivity 第一次被创建时会依次执行 onCreate()、onStart() 和 onResume() 方法。

点击第一个按钮,启动 NormalActivity:

NormalActivity 界面

日志:

02-16 08:59:13.117 6658-6658/com.example.deniro.activitytest D/FirstActivity: onPause
02-16 08:59:13.832 6658-6658/com.example.deniro.activitytest D/FirstActivity: onStop

由于 NormalActivity 会把 MainActivity 完全遮挡住,所以 onPause() 和 onStop() 方法都会得到执行。

现在按下 Back 键返回 FirstActivity:

02-16 09:04:07.297 6658-6658/com.example.deniro.activitytest D/FirstActivity: onRestart
02-16 09:04:07.297 6658-6658/com.example.deniro.activitytest D/FirstActivity: onStart
02-16 09:04:07.298 6658-6658/com.example.deniro.activitytest D/FirstActivity: onResume

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

现在点击第二个按钮,启动 DialogActivity:

DialogActivity 对话框

日志:

02-16 09:42:08.588 14402-14402/com.example.deniro.activitytest D/FirstActivity: onPause

我们可以看到,这时只执行了 onPause() 方法,onStop() 方法并没有执行,因为 DialogActivity 并没有完全遮挡 FirstActivity,所以此时的 FirstActivity 只是进入了暂停状态。

按下 Back 键返回 FirstActivity 也只执行了 onResume() 方法:

02-16 09:46:36.373 14402-14402/com.example.deniro.activitytest D/FirstActivity: onResume

最后在 FirstActivity 按下 Back 键退出程序:

02-16 09:47:16.327 14402-14402/com.example.deniro.activitytest D/FirstActivity: onPause
02-16 09:47:17.382 14402-14402/com.example.deniro.activitytest D/FirstActivity: onStop
02-16 09:47:17.382 14402-14402/com.example.deniro.activitytest D/FirstActivity: onDestroy

现在对于活动的生命周期,清楚了吧O(∩_∩)O哈哈~

3.5 系统回收活动

当一个活动进入停止状态,是有可能被系统回收的。假设应用中有一个活动 A,用户在活动 A
的基础上启动了活动 B,活动 A 就进入停止状态,这个时候由于系统内存不足,将活动 A 回收,然后用户按下 Back 键返回活动 A,这时还是会显示活动 A ,只是不会执行 onRestart() 方法,而是会执行活动 A 的 onCreate( )方法,因为活动 A 被重新创建了一次。

但如果活动 A 中存在临时数据和状态,而这时活动 A 被系统回收了,,这种情况是会严重影响用户体验的。

Activity 中提供了onSaveInstanceState() 回调方法,它会在活动被回收之前被调用,因此我们可以通过这个方法来解决活动被回收时需要保存临时数据的场景。

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

在 FirstActivity 中保存临时数据:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("key", "临时数据");
}

在 FirstActivity 中的 onCreate 方法取出临时数据:

//取出临时数据
if (savedInstanceState != null) {
    String tempData = savedInstanceState.getString("key");
    Log.d(TAG, "onCreate: " + tempData);
}

取出值之后再做相应的恢复操作即可,比如可将文本内容重新赋值到文本输入框上。

4 启动模式

活动的启动模式一共有 4 种,分别是standard、singleTop、singleTask 和 singleInstance,可以 在AndroidManifest.xml 中通过给 <activity> 标签指定 android:launchMode 属性来选择启动模式。

4.1 standard 模式

standard 是活动默认的启动模式。在 standard 模式下,每启动一个新的活动,它就会在栈中入栈,并处于栈顶的位置。系统不会判断这个活动是否已经在栈中存在,每次启动都会创建该活动的一个新的实例。

在 FirstActivity 中新增一个按钮并在 onCreate 方法中添加点击事件,然后打印出这个类的 ID:

//启动对话框式的活动
(findViewById(R.id.standard)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(FirstActivity.this, FirstActivity.class));
    }
});

Log.d(TAG, "this: "+this.toString());

我们在 FirstActivity 中启动 FirstActivity。

重新运行程序,然后在 FirstActivity 界面连续点击多次按钮:

standard 模式下的打印日志

可以看出,每点击一次按钮就会创建出一个新的 FirstActivity 实例。如果点击 back,则会返回到上一个的 FirstActivity 实例。

standard 模式下的同一个活动

4.2 singleTop 模式

在 singleTop 模式下,启动活动时,如果发现栈顶已经是该活动,则直接使用它,而不会再创建新的活动实例。

修改 AndroidManifest.xm 中 FirstActivity 的启动模式为 singleTop:

<activity
    android:name=".FirstActivity"
    android:label="第一个活动"
    android:launchMode="singleTop"
    >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

重新运行程序,日志中创建了一个 FirstActivity 实例:


singleTop 模式下的日志

之后不管点击多少次按钮都不会再有新的日志出现,因为目前 FirstActivity 已经处于栈顶,每当想要再启动 FirstActivity 活动时都会直接使用栈顶的活动,因此这时的 FirstActivity也只会有一个实例,也仅需按一次 Back 键就可以退出程序。

但是当 FirstActivity 并未处于栈顶时,这时再启动 FirstActivity,还是会创建新的实例的。

下面让我们来验证一下。

我们在 SecondActivity 中记录日志并新增一个按钮,最后为它新增点击事件:

 Log.d(TAG, "this: " + this.toString());
....
//返回到第一个活动(启动第一个活动的方式)
(findViewById(R.id.return_first_by_start)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        startActivity(new Intent(SecondActivity.this, FirstActivity.class));
    }
});

点击按钮后,启动 FirstActivity 活动。

重启程序,依次点击按钮,实现 FirstActivity → SecondActivity → FirstActivity:

singleTop 模式下的日志

可以看出,系统创建了两个不同的 FirstActivity 实例,这是因为在 singleTop 模式下只会判断栈顶的活动是否已存在。

singleTop 模式

4.3 singleTask 模式

当活动的启动模式为 singleTask 时,每次启动该活动,系统首先会在栈中检查是否已存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。

我们再来实验一下,首先修改 AndroidManifest.xml 中 FirstActivity 的启动模式为 singleTask:

android:launchMode="singleTask"

然后在 SecondActivity 中覆盖 onDestroy 方法:

@Override
protected void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "this:onDestroy");
}

重启程序,依次点击按钮,实现 FirstActivity → SecondActivity → FirstActivity:

singleTask 模式下的日志

从日志中可以看出,从 SecondActivity → FirstActivity 时,FirstActivity 是重启而不是重建一个新实例,而因为 SecondActivity 处于栈顶,所以直接被 “踢出” 栈(destroy)咯。

singleTask 模式

4.4 singleInstance 模式

singleInstance 模式的活动会启用一个新的栈来管理这个活动。想象这样的场景,假设程序中有一个活动允许其他程序调用,可以通过 singleInstance 模式实现其他程序和这个程序共享活动实例。

修改 AndroidManifest.xml 中 SecondActivity 的启动模式为 singleInstance:

android:launchMode="singleInstance"

使用 Android Studio 新增 ThirdActivity 活动,然后为 FirstActivity、SecondActivity 和 ThirdActivity 新增打印栈 ID 的日志:

 Log.d(TAG, "onCreate: Task id:" + getTaskId());

重启程序,依次点击按钮,实现 FirstActivity → SecondActivity → ThirdActivity:

singleInstance 模式下的日志

可以看出处于 SecondActivity 是存放在一个独立的栈里的。

这时当我们按下 Back 键返回时,会发现 ThirdActivity 直接返回到了 FirstActivity,然后再按下 Back 键又会返回到 SecondActivity,最后按下 Back 键才会退出程序。因为 FirstActivity 和
ThirdActivity 是存放在同一个栈里的,所以在 ThirdActivity 的界面按下 Back 键,ThirdActivity才会出栈,这时 FirstActivity 就到了栈顶,因此也就出现了从 ThirdActivity 直接返回到
FirstActivity 的现象。在 FirstActivity 界面再次按下 Back 键,这时当前的栈已经空了,于是就显示了另一个栈的栈顶活动,即 SecondActivity。最后再次按下 Back 键,这时所有的栈都已经空了,也就很自然退出了程序啦O(∩_∩)O~

singleInstance 模式

5 最佳实践

5.1 获知当前所处的活动

在老项目里,有时候需要在某个界面上修改一些东西时,却找不到这个界面对应的活动是哪一个。这时候,如果程序能够知道当前所处的界面是哪一个活动,将会是非常方便的哦O(∩_∩)O哈哈~

新建一个类 BaseActivity,继承 Activity,注意这只是一个普通的 Java 类:

public class BaseActivity extends Activity {
    private static final String TAG = "BaseActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate: "+getClass().getSimpleName());
    }
}

然后让所有的活动类都继承 BaseActivity。

现在重新运行程序,然后通过点击按钮,,实现 FirstActivity → SecondActivity → ThirdActivity,最后观察日志:

输出日志

每当进入一个活动界面,该活动的类名就会被打印出来,这样就可以知晓当前界面对应的是哪一个活动啦O(∩_∩)O哈哈~

5.2 随时退出

一般手机的界面有多个活动,如果这时停留在深层次的活动中,那么想要退出程序非常不方便,因为需要连按多次 Back 键才能退出。按 Home 键只是把程序挂起,并没有真正退出程序。

我们需要用一个专门的集合类对所有的活动进行统一管理。

public class ActivityManager {
    public static List<Activity> list = new ArrayList<>();

    /**
     * 新增活动
     *
     * @param activity
     */
    public static void add(Activity activity) {
        list.add(activity);
    }

    /**
     * 移除活动
     *
     * @param activity
     */
    public static void remove(Activity activity) {
        list.remove(activity);
    }

    /**
     * 结束所有活动
     */
    public static void finishAll() {
        for (Activity activity : list) {
            if (!activity.isFinishing()) {
                activity.finish();
            }
        }

        //停止进程
        android.os.Process.killProcess(android.os.Process.myPid());
    }


}

这里在销毁所有活动的代码后面又加上杀掉当前进程的逻辑,以保证程序完全退出。注意,killProcess() 方法只能用于杀掉当前程序的进程。

然后在 BaseActivity 的 onCreate 与 onDestroy 方法加入活动管理器的相应方法(新增或移除):

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate: " + getClass().getSimpleName());
    ActivityManager.add(this);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    ActivityManager.remove(this);
}

最后为想要直接退出程序的按钮,添加点击事件:

(findViewById(R.id.exit)).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ActivityManager.finishAll();
    }
});

5.3 封装启动活动

很多活动,可能需要启动参数,才能正确启动。

之前我们介绍的是通过 Intent 来传递启动参数的:

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("key", "value");//设置数据
startActivity(intent);

这本身并没有错,只是不够清晰。因为需要启动这个活动的开发者,都要把上述代码再写一遍。

我们可以为需要启动的活动添加一个静态的 “启动” 方法:

/**
 * 启动活动
 *
 * @param context
 * @param data    启动参数
 */
public static void start(Context context, String data) {
    Intent intent = new Intent(context, SecondActivity.class);
    intent.putExtra("param", data);
    context.startActivity(intent);
}

这样写的好处是一目了然,SecondActivity 所需要的数据在方法参数中已经全部体现出来了。现在只需要一行代码就可以启动 SecondActivity 啦:

 SecondActivity.start(FirstActivity.this, "value");

为每个活动编写类似的启动方法,这可是一个良好的编码习惯哟O(∩_∩)O哈哈~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容