全面总结之 Activity 篇

什么是 Activity?

说的通俗一点就是,我们在 App 上看到的每一个页面都是一个 Activity。

Activity 是 Android 常用的组件之一,用以实现和用户之间的交互。每个 Activity 都会获得一个用户绘制其用户界面的窗口,这个窗口通常会充满屏幕,但也有可能浮动在其他窗口之上。

创建一个 Activity 需要我们创建一个继承自 Activity 的子类,并且实现其各个方法,这个在后面会讲到。

在清单中声明 Activity

创建好 Activity 子类之后,这个 Activity 还并不能使用,需要在清单文件中声明它,将 <activity> 元素添加为 <application> 元素的子项。例如:

<manifest ... >
  <application ... >
      <activity android:name=".MainActivity" />
      ...
  </application ... >
  ...
</manifest >

这样一来,我们就可以访问这个 Activity 了,我们还可以在清单文件中为这个 Activity 指定标签、图标以及设置风格等等。这个在后面会讲到。

让 Activity 变成你要的样子

我们看到的各种各样形形色色的界面,都是由我们自己定义出来的。

通常使用的方法是借助 XML 文件来创建,一个 XML 文件通常由最少一个布局文件以及若干控件组成的。然后通过在 Activity 中调用 setContentView 方法将 XML 文件设置为 Activity 的 UI,当然我们也可以使用 Java 代码来创建,但不建议这样做,因为这样容易使得我们将我们的界面代码和逻辑代码混淆。

例如:

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

常见布局

常见的布局有如下几种:

  • LinearLayout:线性布局

    线性布局中所有的子项都是成垂直或水平排列,我们可以通过 android:orientation 属性来设置布局方向。线性布局遵守子视图之间的“边距”以及每个子视图的“重力”(右对齐、居中对齐、左对齐)

    线性布局还支持使用 android:layout_weight 属性为各个子视图分配权重。此属性根据视图应在屏幕上占据的空间量向视图分配“重要性”值。 权重值更大的视图可以填充父视图中任何剩余的空间。子视图可以指定权重值,然后系统会按照子视图声明的权重值的比例,将视图组中的任何剩余空间分配给子视图。 默认权重为零。

    线性布局常用属性:

    • android:padding:设置组件内边距,譬如文本控件中文字距离组件边框的距离。同样的还有 android:paddingLeft、android:paddingRight、android:paddingTop、android:paddingBottom、android:paddingStart、android:paddingEnd。

    • android:layout_margin:设置组件外边距,指该控件距离边父控件的边距。同样的还有 android:layout_marginLeft、android:layout_marginRight、android:layout_marginTop、android:layout_marginBottom、android:layout_marginStart、android:layout_marginEnd。

    • android:gravity:padding 是设置内边距,而 gravity 则是设置该组件中内容方向的,譬如文本组件中的字体是处于该组件的左边还是右边等。有 right、left、bottom、top、start、end、center 等几个常用值可选,这几个选项可以组合使用。

    • android:layout_gravity:margin 是设置组件外边距,而 layout_gravity 则是设置该组件在父组件中的位置。有 right、left、bottom、top、start、end、center 等几个常用值可选,这几个选项可以组合使用。

示例:

<?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:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:orientation="vertical" >
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/to" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/subject" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="top"
        android:hint="@string/message" />
    <Button
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="@string/send" />
</LinearLayout>

该布局显示如下:

有关 LinearLayout 的每个子视图可用属性的详情,请参阅 LinearLayout.LayoutParams

  • RelativeLayout:相对布局

    相对布局是一个用相对位置来显示子组件的布局形式。每个子组件的位置可以相对与相邻元素来指定(如相对与另一个组件的左边或底边),或者相对于父相对布局区域位置来指定(如底部对齐,中央偏左)。

    相对布局常用的布局属性有:

    • android:layout_alignParentLeft
      和父布局的左对齐

    • android:layout_alignParentTop
      和父布局上部对齐

    • android:layout_alignParentRight
      和父布局的右对齐

    • android:layout_alignParentBottom
      和父布局的下部对齐

    • android:layout_centerInParent
      在父布局中居中显示

    • android:layout_above
      表示该控件在标准控件上方

    • android:layout_below
      表示该控件在标准控件下方

    • android:layout_toLeftOf
      表示该控件在标准控件左面

    • android:layout_ toRightOf
      表示该控件在标准控件右面

    • android:layout_alignRight
      表示该控件的右边缘和标准控件的右边缘对齐

    • android:layout_ alignLeft
      表示该控件的左边缘和标准控件的左边缘对齐

    • android:layout_alignTop
      表示该控件的上边缘和标准控件的上边缘对齐

    • android:layout_alignBottom
      表示该控件的下边缘和标准控件的下边缘对齐

示例:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="16dp"
    android:paddingRight="16dp" >
    <EditText
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/reminder" />
    <Spinner
        android:id="@+id/dates"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/times" />
    <Spinner
        android:id="@id/times"
        android:layout_width="96dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_alignParentRight="true" />
    <Button
        android:layout_width="96dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/times"
        android:layout_alignParentRight="true"
        android:text="@string/done" />
</RelativeLayout>

上述布局显示为:

有关 RelativeLayout 的每个子视图可用属性的详情,请参阅 RelativeLayout.LayoutParams

  • FrameLayout:框架布局

    框架布局没有任何定位方式,所有的控件都会摆放在布局的左上角。控件可以通过android:layout_gravity属性控制自己在父控件中的位置。

    示例:

    
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/firstView"
        android:text="第一层"
        android:textSize="50sp"
        android:textColor="#000000"/>
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/secondView"
        android:text="第二层"
        android:textSize="30sp"
        android:textColor="#ffff00"/>
    
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/thirdView"
        android:text="第三层"
        android:textSize="15sp"
        android:textColor="#ff00ff"
        android:gravity="right"/>
    </FrameLayout>
    

    该布局显示为:

    有关 FrameLayout 的每个子视图可用属性的详情,请参阅 FrameLayout.LayoutParams

  • TableLayout:表格布局

    TableLayout 运行我们使用表格的方式来排列控件,它的本质依然是线性布局。表格布局采用行、列的形式来管理控件,TableLayout 并不需要明确的声明包含多少行多少列,而是通过添加 TableRow、其他组件来控制表格的行数和列数。每次向 Table 中添加一个 TableRow,该 TableRow 就是一个表格行,TableRow 也是容器,因此它也可以不断的添加其他组件,每添加一个子组件该表格就增加一列。

    我们想要在左右两边都有一定的间距,并且所有组件都位于屏幕中间,这里就需要一些属性来配合达到这些效果。

    • android:collapseColumns="0"
      隐藏第0列

    • android:shrinkColumns="0"
      收缩第0列

    • android:stretchColumns="0"
      拉伸第0列

示例:


<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:stretchColumns="1">

    <TableRow>
        <TextView android:layout_height="wrap_content"
            android:text="账户:"/>
        <EditText
            android:id="@+id/usernameInput"
            android:layout_height="wrap_content"
            android:hint="输入要注册的用户名"/>
    </TableRow>

    <TableRow>
        <TextView android:layout_height="wrap_content"
            android:text="密码:"/>
        <EditText
            android:id="@+id/passwordInput"
            android:layout_height="wrap_content"
            android:hint="输入要密码"/>
    </TableRow>

    <TableRow>
        <Button
            android:id="@+id/loginButton"
            android:layout_height="wrap_content"
            android:layout_span="2"
            android:text="登录"/>
    </TableRow>
</TableLayout>

上述布局显示如下:

有关 TableLayout 的每个子视图可用属性的详情,请参阅 TableLayout.LayoutParams

  • GridLayout:网格布局

    网格布局是 Android4.0 新增的布局管理器,因此需要在 Android4.0 之后的版本才可以使用,之前的平台使用该布局的话,需要导入相应的支持库。

GridLayout 的作用类似于HTML中的table标签,它把整个容器划分成 row * column个网格,每个网格都可以放置一个组件,也可以设置一个组件横跨多少列、多少行。

GridLayout 提供了 setRowCount(int) 和 setColumnCount(int) 方法来控制该网格的行数和列数。

示例:


<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:rowCount="6"
    android:columnCount="4"
    android:id="@+id/root">

    <!-- 定一个一个横跨四列的文本框,并设置该文本框的前景色、背景色等属性 -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_columnSpan="4"
        android:textSize="50sp"
        android:layout_marginLeft="4px"
        android:layout_marginRight="4px"
        android:padding="5px"
        android:layout_gravity="right"
        android:background="#eee"
        android:textColor="#000"
        android:text="0"
        />
    <!-- 定义一个横跨四列的按钮 -->
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_columnSpan="4"
        android:text="清除"/>

    <!-- 添加其他按钮 -->
    <Button
        android:text="7"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="8"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="9"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="/"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="4"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="5"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="6"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="*"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="1"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="2"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="3"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="-"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="0"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="."
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="+"
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>
    <Button
        android:text="="
        android:layout_rowWeight="1"
        android:layout_columnWeight="1"/>

</GridLayout>

上述布局显示为:

有关 GridLayout 的每个子视图可用属性的详情,请参阅 GridLayout.LayoutParams

常见控件

Android 常用控件太多,因为篇幅原因,这里不一一介绍,详情请参阅 用户界面

style 文件美化控件

我们可以通过使用 style 和 theme 来对我们的布局以及组件变得更加漂亮。

例如我们需要将一个 Activity 设置为窗口型,只需要在清单文件中为其设置 android:theme 属性:

android:theme="@style/Base.Theme.AppCompat.Dialog"

详情请参阅:样式和主题

如何启动 Activity

每个 App 都是由若干个 Activity 组成的,通过 Activity 之间相互跳转来完成我们的服务。

有两种方法可以启动另一个 Activity:

  • startActivity(Intent intent)

  • startActivityForResult(Intent intent, int requestCode)

有关 Intent 的知识,请参阅:Intent 和 Intent 过滤器

Activity 之间如何传值

传递值:

可以使用 putExtra() 方法向目标 Activity 传递数据,目标 Activity 通过 getXXXExtra() 方法来获取传递过来的值
发送端:

Intent intent = new Intent(MainActivity.this, ActivityOne.class);
intent.putExtra("str", "这是要传递的值");
startActivity(intent);

接收端:

Intent intent = getIntent();
String str = intent.getStringExtra("str");
text.setText(str);

传递对象:

Intent 可以传递 Serializable 或者 Parcelable 对象,需要传递的对象实现 Serializable 或 Parcelable 接口,就可以将对象传入到一个 Bundle 中,然后使用 intent 的 putExtra() 方法将对象传递过去,接收的 Activity 通过 getSerializableExtra 或者 getParcelableExtra 方法获得对象。
发送端:

Intent intent = new Intent(MainActivity.this, ActivityOne.class);
Person person = new Person(13, "张三");
startActivity(intent);

接收端:

Intent intent = getIntent();
Person person = (Person) intent.getSerializableExtra("person");
int age = person.getAge();
String name = person.getName();

text = (TextView) findViewById(R.id.text);
text.setText("age:" + age + ",name:" + name);

实体类:

public class Person implements Serializable {
    ......
}

向上一个Activity传递数据:

可以使用 startActivityForResutlt() 方法来启动另一个 Activity,而在另外一个 Activity 中,新建一个 Intent 将要传递的值使用 putExtra 方法存放到 intent 中,并且 setResult 为 RESULT_OK,这样,在第一个 Activity 的 onActivityResult 方法中就可以获取到返回来的值了。

MainActivity.java

public class MainActivity extends Activity {

    private Button button;
    private TextView tv;

    private String resultStr;

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

        button = (Button) findViewById(R.id.button);
        tv = (TextView) findViewById(R.id.tv);

        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                // 直接指定跳转目标,是为显式跳转
                Intent intent = new Intent(MainActivity.this, ActivityOne.class);
                startActivityForResult(intent, 1);
            }
        });
    }

    @Override
    protected void onRestart() {
        // TODO Auto-generated method stub
        super.onRestart();
        tv.setText(resultStr);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // TODO Auto-generated method stub
        super.onActivityResult(requestCode, resultCode, data);
        resultStr = data.getStringExtra("str");
    }
}

ActivityOne.java

public class ActivityOne extends Activity {

    private EditText et;
    private Button bt;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_one);
        et = (EditText) findViewById(R.id.et);
        bt = (Button) findViewById(R.id.bt);

        bt.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                String str = et.getText().toString();
                Intent intent = new Intent();
                intent.putExtra("str", str);
                setResult(RESULT_OK,intent);
                finish();
            }
        });
    }
}

Activity 的生命周期

在理解 Activity 的生命周期之前,我们需要先了解一下 Activity 的三种状态:

  • 运行状态(Running):此时 Activity 位于屏幕前台并具有用户焦点(有人也称之为继续状态)

  • 暂停状态(Stop):另一个 Activity 位于屏幕前台并且具有用户焦点,只是没有完全遮盖,也就是说该 Activity 仍然可见,只是没有用户焦点而已。暂停状态下的 Activity 处于完全活动状态,但在内存极度不足的情况下,可能会被系统回收。

  • 停止状态(Finish):该 Activity 被另一个 Activity 完全遮盖,它对用户不可见,在别的地方需要内存时可能会被系统回收掉。


有以下六个回调方法贯穿整个 Activity 的生命周期:

  • onCreate(Bundle savedInstanceState)

首次创建 Activity 时调用。在该方法中我们通常会执行所有正常的静态设置 —— 创建视图、将数据绑定到列表等等。

系统向该方法传递一个 Bundle 对象,其中包含 Activity 的上一状态,不过前提是你已经捕获了该状态。

  • onStart()

在 Activity 即将对用户可见之前调用该方法,在该方法通常也执行一些数据绑定业务。

  • onResume()

在 Activity 即将对用户可见之前调用。

因为在调用该方法之后,界面开始展现给用户,所以一般在该方法中执行刷新列表动作,保证用户看到的是最新的信息。

  • onPause()

在即将开始另一个 Activity 之前调用。 此方法通常用于确认对持久性数据的未保存更改、停止动画以及其他可能消耗 CPU 的内容,诸如此类。 它应该非常迅速地执行所需操作,因为它返回后,下一个 Activity 才能继续执行。

  • onStop()

在 Activity 对用户不再可见时调用。如果 Activity 被销毁,或另一个 Activity(一个现有 Activity 或新 Activity)继续执行并将其覆盖,就可能发生这种情况。

  • onDestroy()

在 Activity 被销毁前调用。这是 Activity 将收到的最后调用。 当 Activity 结束(有人对 Activity 调用了 finish()),或系统为节省空间而暂时销毁该 Activity 实例时,可能会调用它。

可以通过 isFinishing() 方法区分这两种情形。

  • 也有材料会将 onRestart() 方法纳入到 Activity 的生命周期中,在 Activity 已停止并即将再次启动前调用该方法。

用一张官方的图来展示 Activity 的整个生命周期:


我们通过代码来看一下,在 MainActivity 中有两个按钮,点击按钮分别跳转到另个 Activity :一个正常的 Activity,一个窗口型的 Activity,并在每个 Activity 的每个回调方法中打了 Log,我们看一下执行操作时的 Log:

进入 App:

MainActvity 的 onCreate 方法执行了
MainActvity 的 onStart 方法执行了
MainActvity 的 onResume 方法执行了

点击跳转到正常 Activity 的按钮:

点击了跳转到完整Activity按钮
MainActvity 的 onPause 方法执行了
FullActivity 的 onCreate 方法执行了
FullActivity 的 onStart 方法执行了
FullActivity 的 onResume 方法执行了
MainActvity 的 onStop 方法执行了

点击返回按钮,退回到 MainActivity:

FullActivity 的 onPause 方法执行了
MainActvity 的 onRestart 方法执行了
MainActvity 的 onStart 方法执行了
MainActvity 的 onResume 方法执行了
FullActivity 的 onStop 方法执行了
FullActivity 的 onDestroy 方法执行了

点击跳转到窗口 Activity 的按钮:

点击了跳转到弹窗Activity按钮
MainActvity 的 onPause 方法执行了
DialogActivity 的 onCreate 方法执行了
DialogActivity 的 onStart 方法执行了
DialogActivity 的 onResume 方法执行了

点击返回按钮,退回到 MainActivity:

DialogActivity 的 onPause 方法执行了
MainActvity 的 onResume 方法执行了
DialogActivity 的 onStop 方法执行了
DialogActivity 的 onDestroy 方法执行了

点击 HOME 键,回到桌面:

MainActvity 的 onPause 方法执行了
MainActvity 的 onStop 方法执行了

再重新进入到 App 中:

MainActvity 的 onRestart 方法执行了
MainActvity 的 onStart 方法执行了
MainActvity 的 onResume 方法执行了

点击返回按钮,退出 App:

MainActvity 的 onPause 方法执行了
MainActvity 的 onStop 方法执行了
MainActvity 的 onDestroy 方法执行了

Activity 特殊生命周期

当我们翻转屏幕的时候,正常情况下,Activity 会销毁,然后再重新创建。

打开 App:

MainActvity 的 onCreate 方法执行了
MainActvity 的 onStart 方法执行了
MainActvity 的 onResume 方法执行了

翻转屏幕:

MainActvity 的 onPause 方法执行了
MainActvity 的 onStop 方法执行了
MainActvity 的 onDestroy 方法执行了
MainActvity 的 onCreate 方法执行了
MainActvity 的 onStart 方法执行了
MainActvity 的 onResume 方法执行了

但是我们可以通过设置该 Activity 的 android:configChanges 属性来改变这个特性,在运行时发生配置更改时,默认情况下会关闭 Activity 然后将其重新启动,但使用该属性声明配置将阻止 Activity 重新启动。 Activity 反而会保持运行状态,并且系统会调用其 onConfigurationChanged() 方法。

可以在清单文件中将该属性的值设置为 orientation,如果应用是面向 API 13 或更高级别(按照 minSdkVersion 和 targetSdkVersion 属性所声明的级别),则还应声明 "screenSize" 配置。

设置了该属性之后,翻转屏幕就不会销毁 Activity 重新创建了,而是直接调用该 Activity 的 onConfigurationChanged():

MainActvity 的 onCreate 方法执行了
MainActvity 的 onStart 方法执行了
MainActvity 的 onResume 方法执行了
onConfigurationChanged 方法执行了

保存 Activity 状态

当 Activity 暂停或停止时,Activity 的状态会得到保留。当 Activity 暂停或停止时,Activity 对象仍保留在内存中,因此,用户在 Activity 内所中的任何更改都会得到保留,这样一来,当 Activity 返回前台时,这些更改仍然存在。

当系统为了恢复内存而销毁某项 Activity 时,Activity 对象也会被销毁,因此系统在继续 Activity 时根本无法让其状态保持完好,而是必须在用户返回 Activity 时重建 Activity 对象。但用户并不知道系统销毁 Activity 后又对其进行了重建,因此他们很可能认为 Activity 状态毫无变化。 在这种情况下,您可以实现 onSaveInstanceState() 方法对有关 Activity 状态的信息进行保存,以确保有关 Activity 状态的重要信息得到保留。

系统会先调用 onSaveInstanceState(),然后再使 Activity 变得易于销毁。系统会向该方法传递一个 Bundle,您可以在其中使用 putString() 和 putInt() 等方法以 key-value 对形式保存有关 Activity 状态的信息。然后,如果系统终止您的应用进程,并且用户返回您的 Activity,则系统会重建该 Activity,并将 Bundle 同时传递给 onCreate() 和 onRestoreInstanceState()。您可以使用上述任一方法从 Bundle 提取您保存的状态并恢复该 Activity 状态。如果没有状态信息需要恢复,则传递给您的 Bundle 是空值(如果是首次创建该 Activity,就会出现这种情况)。

如下图所示:

即使你什么都不做,也不实现 onSaveInstanceState(),Activity 类的 onSaveInstanceState() 默认也会恢复部分 Activity 状态。Android 框架中几乎每个小部件都会根据需要实现此方法,一边在重建 Activity 时自动保存和恢复对 UI 所做的任何可见更改,但是需要为小部件提供一个 ID。

由于 onSaveInstanceState() 的默认实现有助于保存 UI 的状态,因此如果您为了保存更多状态信息而替换该方法,应始终先调用 onSaveInstanceState() 的超类实现,然后再执行任何操作。 同样,如果您替换 onRestoreInstanceState() 方法,也应调用它的超类实现,以便默认实现能够恢复视图状态。

注:由于无法保证系统会调用 onSaveInstanceState(),因此您只应利用它来记录 Activity 的瞬态(UI 的状态)— 切勿使用它来存储持久性数据,而应使用 onPause() 在用户离开 Activity 后存储持久性数据(例如应保存到数据库的数据)。

任务栈

我们的 App 通常由若干个 Activity 组成,在各个 Activity 跳转来完成我们的业务,有时候还需要跳转到其他软件的 Activity 或者系统提供的 Activity(其实也是其他软件),例如用浏览器打开网站、发送电子邮件,Android 系统会将这些 Activity 保留在相同的任务中,以维护这种无缝的用户体验。

做个实验,使用 Activity 启动一个打电话的操作,当进入拨号界面时候,使用 adb shell dumpsys activity 来查看 Activity 任务栈,部分结果如下:

  Stack #7:

    Task id #593

      TaskRecord{dfbdf4a #593 A=com.android.dialer U=0 sz=1}

      Intent { act=android.intent.action.DIAL dat=tel:13333333333 flg=0x10000000 cmp=com.android.dialer/.DialtactsActivity }

        Hist #0: ActivityRecord{436303 u0 com.android.dialer/.DialtactsActivity t593}

          Intent { act=android.intent.action.DIAL dat=tel:13333333333 flg=0x10000000 cmp=com.android.dialer/.DialtactsActivity }

          ProcessRecord{64fbd6d 9079:com.android.dialer/u0a5}

    Task id #592

      TaskRecord{f82cebb #592 A=com.li_xyz.activitytest U=0 sz=2}

      Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.li_xyz.activitytest/.MainActivity }

        Hist #1: ActivityRecord{715a914 u0 com.android.packageinstaller/.permission.ui.GrantPermissionsActivity t592}

          Intent { act=android.content.pm.action.REQUEST_PERMISSIONS flg=0x800000 pkg=com.android.packageinstaller cmp=com.android.packageinstaller/.permission.ui.GrantPermissionsActivity (has extras) }

        Hist #0: ActivityRecord{a94b6ee u0 com.li_xyz.activitytest/.MainActivity t592}

          Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.li_xyz.activitytest/.MainActivity }

          ProcessRecord{ecd4c89 7991:com.li_xyz.activitytest/u0a105}

    Running activities (most recent first):

      TaskRecord{dfbdf4a #593 A=com.android.dialer U=0 sz=1}

        Run #1: ActivityRecord{436303 u0 com.android.dialer/.DialtactsActivity t593}

      TaskRecord{f82cebb #592 A=com.li_xyz.activitytest U=0 sz=2}

        Run #0: ActivityRecord{a94b6ee u0 com.li_xyz.activitytest/.MainActivity t592}

    mResumedActivity: ActivityRecord{436303 u0 com.android.dialer/.DialtactsActivity t593}

可以看到 id 为 7 的 Stack 中,有一个 id 为 593 的 Task,一个 id 为 592 的 Task,593 中 DialtactsActivity 用于拨号,592 中 GrantPermissionsActivity 用于权限方面,MainActivity 是我们的 App 的 Activity。

任务是指在执行特定操作时与用户交互的一系列 Activity,这些 Activity 按照各自的打开顺序排列在堆栈(也就是返回栈)中。

当前一个 Activity 启动另一个 Activity 时,该新 Activity 会被推送到堆栈顶部,成为焦点。前一个 Activity 仍保留在堆栈中,只是处于停止状态。Activity 停止时,系统会保持其用户界面的当前状态。用户点击返回按钮,当前 Activity 会从堆栈顶部弹出(该 Activity 被销毁),而前一个 Activity 恢复执行。

堆栈中的 Activity 只会执行简单的出栈入栈,永远不会重新排列。

下图展示了任务中的每个新的 Activity 如何向返回栈添加项目,用户按“返回”按钮时,当前 Activity 随即被销毁,而前一个 Activity 恢复执行。:

任务是一个有机整体,当用户开始新任务或者通过“主页”按钮转到主屏幕时

后台可以同时运行多个任务。但是如果用户同时运行多个后台任务,则系统可能会开始销毁后台 Activity,以回收内存资源,从而导致 Activity 状态丢失

由于返回栈中的 Activity 永远不会重新排列,因此如果应用允许用户从多个 Activity 中启动特定的 Activity,则会创建该 Activity 的新实例并推入堆栈中(而不是将 Activity 的任一先前实例置于顶部)。因此,应用中的一个 Activity 可能会多次实例化(即使 Activity 来自不同的任务)。因此,如果用户使用“返回”按钮向后导航,则会按 Activity 每个实例的打开顺序显示这些实例(每个实例的 UI 状态各不相同),如果你不希望多次实例化一个 Activity,可以修改此行为。

启动模式

启动模式允许定义 Activity 的新实例如何与当前任务管理,我们可以通过两种不同的方法定义启动模式:

  • 在清单文件中配置启动模式
    在清单文件中声明 Activity 时候,可以使用 <activity> 元素的 launchMode 属性为 Activity 指定启动模式,可以分配给 launchMode 属性的启动模式共有四种:

standard

这种方式是默认的,系统在启动 Activity 的任务中创建 Activity 的新实例并向其传递 Intent。Activity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例。

singleTop

如果当前任务的顶部已经存在 Activity 的一个实例,则系统会调用该实例的 onNewIntent() 方法向其传入 Intent,而不是创建 Activity 的新实例。

Acticity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例(但前提是位于返回栈顶部的 Activity 并不是 Activity 的现有实例)。

注:为某个 Activity 创建新实例时,用户可以按“返回”按钮返回到前一个 Activity。 但是,当 Activity 的现有实例处理新 Intent 时,则在新 Intent 到达 onNewIntent() 之前,用户无法按“返回”按钮返回到 Activity 的状态。

singleTask

系统创建新任务并实例化位于新任务底部的 Activity。但是如果该 Activity 的一个实例已存在与一个单独的任务中,则系统会通过调用现有实例的 onNewIntent() 方法向其传入 Intent,而不是创建新实例。一次只能存在 Activity 的一个i额实例。

singleInstance

与 singleTask 相同,只是系统不会将任何其他 Activity 启动到包含实例的任务中。该 Activity 始终是其任务唯一仅有的成员;由此 Activity 启动的任何 Activity 均在单独的任务中打开。

使用 launchMode 属性为 Activity 指定的启动模式可以由 Intent 附带的 Activity 启动标志代替。

  • 使用 Intent 标志配置启动模式

启动 Activity 时,你可以通过传递给 startActivity() 的 Intent 中加入响应的标志,修改 Activity 与其任务默认关联方式。可用于修改默认行为的标志包括:

FLAG_ACTIVITY_NEW_TASK

在新任务中启动 Activity。如果已为正在启动的 Activity 运行任务,则该任务会赚到前台并恢复其最后状态,同时 Activity 会在 onNewIntent() 中收到新 Intent。

其行为和在清单文件中将 launchMode 设置为 singleTask 效果相同。

FLAG_ACTIVITY_SINGLE_TOP

如果正在启动的 Activity 是当前 Activity(位于返回栈的顶部),则 现有实例会接收对 onNewIntent() 的调用,而不是创建 Activity 的新实例。

其行为和在清单文件中将 launchMode 设置为 singleTop 效果相同。

FLAG_ACTIVITY_CLEAR_TOP

如果正在启动的 Activity 已在当前任务中运行,则会销毁当前任务顶部的所有 Activity,并通过 onNewIntent() 将此 Intent 传递给 Activity 已恢复的实例(现在位于顶部),而不是启动该 Activity 的新实例。
产生这种行为的 launchMode 属性没有值。

FLAG_ACTIVITY_CLEAR_TOP 通常与 FLAG_ACTIVITY_NEW_TASK 结合使用。一起使用时,通过这些标志,可以找到其他任务中的现有 Activity,并将其放入可从中响应 Intent 的位置。

注:如果指定 Activity 的启动模式为 "standard",则该 Activity 也会从堆栈中移除,并在其位置启动一个新实例,以便处理传入的 Intent。 这是因为当启动模式为 "standard" 时,将始终为新 Intent 创建新实例。

清理返回栈

如果用户长时间离开任务,则系统会清除所有 Activity 的任务,根 Activity 除外。 当用户再次返回到任务时,仅恢复根 Activity。系统这样做的原因是,经过很长一段时间后,用户可能已经放弃之前执行的操作,返回到任务是要开始执行新的操作。

您可以使用下列几个 Activity 属性修改此行为:

  • alwaysRetainTaskState

    如果在任务的根 Activity 中将此属性设置为 "true",则不会发生刚才所述的默认行为。即使在很长一段时间后,任务仍将所有 Activity 保留在其堆栈中。

  • clearTaskOnLaunch

    如果在任务的根 Activity 中将此属性设置为 "true",则每当用户离开任务然后返回时,系统都会将堆栈清除到只剩下根 Activity。 换而言之,它与 alwaysRetainTaskState 正好相反。 即使只离开任务片刻时间,用户也始终会返回到任务的初始状态。

  • finishOnTaskLaunch

    此属性类似于 clearTaskOnLaunch,但它对单个 Activity 起作用,而非整个任务。 此外,它还有可能会导致任何 Activity 停止,包括根 Activity。 设置为 "true" 时,Activity 仍是任务的一部分,但是仅限于当前会话。如果用户离开然后返回任务,则任务将不复存在。

Activity 使用小技巧

知晓当前是在哪一个 Activity

创建一个基类,然后重写其 onCreate 方法,在其中打入 Log:

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

然后我们的所有 Activity 都继承自该基类即可。

随时随地退出程序

创建一个集合用来管理 Activity:

public class ActivityCollector {
    public static List<Activity> activities = new ArrayList<Activity>();

    public static void addActivity(Activity activity){
        activities.add(activity);
    }

    public static void removeActivity(Activity activity){
        activities.remove(activity);
    }

    public static void finishAll(){
        for (Activity activity:activities) {
            if(!activity.isFinishing()){
                activity.finish();
            }
        }
    }
}

然后创建一个 Activity 的基类:

public class BaseActivity extends AppCompatActivity{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("BaseActivity", getClass().getSimpleName());
        ActivityCollector.addActivity(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ActivityCollector.removeActivity(this);
    }
}

这样,不管在哪里,我们只需要调用 ActivityCollector.finishAll() 方法,就可以关闭所有的 Activity 退出程序了。

参考资料

  • 《第一行代码 - Android》 - 郭霖

  • 《疯狂 Android 讲义》 - 李刚

  • Android 官方文档

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1