本文翻译自android官方文档,结合自己测试,整理如下。
概述
一个Activity是能够提供屏幕的应用程序组件,用户可以通过该屏幕和程序进行交互。每个activity都被赋予一个窗口用于绘画用户接口,该窗口通常来说会覆盖整个屏幕,但是有时也会比屏幕小,显示在其它窗口的上面。
一个程序通常有多个activities构成,而且需要一个main
avtivity作为程序的入口当用户第一次启动该程序时。每一个activity都能够启动其它activities,当新的activity启动时,前一个activity将进入stopped
状态,并保存在back栈中。当启动新的activity时,该activity会压入栈顶并显示给用户。back栈满足栈的后进先出原则,因此当用户点击BACK
键时,当前显示的activity就会弹出栈顶(此时activity被销毁),栈中的下一个activity成为栈顶,并显示在屏幕上。
Activity有自己的生命周期,包括:创建,运行,停止,销毁等,系统给我们提供了一些生命周期方法可以使我们在合适的生命周期中进行相关的操作。例如当activity停止时,我们需要释放资源等。
下面我们就详细讨论如何创建,使用,管理activity。
创建Activity
为了创建activity,我们必须继承Activity类或者它的子类。在实现类中,我们必须覆盖相关的生命周期回调方法,这些回调方法是由系统在activity生命周期状态转换时被调用的。两个最重要的回调方法:
onCreate()
该方法是在创建Activity时被调用,通常我们需要在这里初始化一些必要的组件。更重要的是,我们必须调用setContentView()
设置activity和用户交互的布局文件。onPause()
当用户离开activity,但是此时activity可见时,系统会调用该方法。此时我们需要保存用户会话中任何需要保存的数据,这是由于用户可能不在回到该activity中。
其它生命周期方法后续将详细介绍。
实现用户接口
activity用户接口是由视图的层次结构提供的,每一个视图控制activity窗口上的一部分用于和用户进行交互,例如一个视图可能是一个按钮。
系统提供了很多设计和组织布局的视图,控件(Widgets
)就是这样的视图,例如:按钮,文本域,选择框等等。布局(Layouts
)是抽象类ViewGroup的实现类,例如:线性布局,相对布局等。同样你也可以继承View和ViewGroup实现自己的控件和布局。
定义布局最常用的方法是在xml文件中定义。通过这种方法, 布局设计可以和代码分开管理。我们可以通过setContentView()
设置activity的布局文件,该方法需要指定布局文件的ID。当然,我们也可以在activity代码中使用ViewGroup创建自己的视图,然后通过setContentView()
设置,该方法接收一个View参数。
在manifest文件中声明Activity
若要启动Activity,需在AndroidManifest.xml声明Activity,代码如下:
<manifest ... >
<application ... >
<activity android:name=".ExampleActivity" />
...
</application ... >
...
</manifest >
在标签<activity>
中属性android:name
是必须的,它指定activity的类名(通常manifest中指定了包名,因此这里只需要.ClassName
),当然在<activity>
还有其他的属性,这里先不讲。
一旦发布程序的话,我们不应该再改变activity类名,这样可能导致一些功能出错。
使用intent filters
在标签<activity>
可以通过子标签<intent-filter>
指定intent过滤器,该intent过滤器声明其它组件如何启动该activity。
当通过Android SDK工具创建第一个activity时,系统会自动生成一个带<intent-filter>
的activity,如下:
<activity android:name=".ExampleActivity" android:icon="@drawable/app_icon">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
上面的<intent-filter>
内容就是一个标准的启动程序时第一个显示activity的intent过滤器,即有该过滤器的activity表示程序的入口activity。因此在一个应用程序中只有一个包含main
action和launcher
category的<intent-filter>
。
若你不希望其它程序启动你的程序的话,就不用设置任何的intent过滤器。在程序内部我们可以显示地指定intent内容。
关于如何设置显示和隐式intent以及它的各个属性,我会专门用一章翻译官方文档内容,若有兴趣的话,请持续关注我的博客。
启动Activity
我们可以通过startActivity()
启动另外一个activity,该方法传递一个Intent对象,用于描述启动满足条件的activity。系统根据Intent对象的要求进行选择,满足条件的activity都会呈现给用户,即使是不在一个应用程序中的activity也可以启动。
当我们启动自己的应用程序内部的activity时,应该使用显式启动,这样能够减少解析Intent对象的过程,提高性能。显式启动需要在Intent对象中指定要启动的类名,如:
Intent intent = new Intent(this, SignInActivity.class);
startActivity(intent);
然而,有时候我们的应用程序可能想要执行一些其他的任务,如发送邮件,短信等等。这种情况下我们的程序不具备这样的activities,因此我们可以使用设备上拥有该功能的其他程序的activities。这就需要使用隐式Intent方式。当有多个activities满足条件时,系统将选择权交给用户,用户来选择一个合适的activity启动。例如,发送邮件的intent设置如下:
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, recipientArray);
startActivity(intent);
在putExtra()
中传递的是收件人邮箱。当完成邮件后,我们的activity又重新启动。
启动带返回结果的activity
有时候,我们可能希望从启动的activity返回结果,这种情况下可以通过startActivityForResult()
启动带返回结果的activity。而为了接收返回结果,我们可以实现回调方法onActivityResult()
处理返回结果。当带有返回结果的activity处理完之后通过setResult()
将结果发送到启动它的activity中的onActivityResult()
。
startActivityForResult()
接收两个参数:
- Intent对象;
- int型请求码,用于区别哪次请求。
onActivityResult()
传递三个参数:
- int型请求码,和
startActivityForResult()
方法中第二个参数一致; - int型返回码,在将要启动的activity中的
setResult()
设置,表示返回结果是否成功; - Intent对象,表示返回结果数据
setResult()
接收两个参数:
- int型返回码,用于判断是否请求成功,可以使用系统提供值如:Activity.RESULT_OK;
- Intent对象,带有返回结果数据。
关于Activity之间通信的更多内容有兴趣的可以参考我之前的文章:Activity之间通信
关闭activity
通过调用finish()
方法可以关闭一个activity,也可以使用finishActivity()
关闭之前启动过的activity。通过这种方法关闭的activity,不再执行activity的各个生命周期方法(除了onDestroy()
),而是直接在调用这两个方法之后执行onDestroy()
销毁activity。
注意:更多的情况下,我们不应该使用这些方法关闭activity,这是因为activity有自己的生命周期,系统统一管理。我们自己关闭的话会对预期的用户体验产生不利的影响,只有在极端情况下才使用这些方法来关闭activity。
管理activity生命周期
实现回调方法来管理activity的生命周期是开发健壮和灵活的应用程序的关键。
Activity本质上有三种状态:
- Resumed/Running。activity在屏幕最前端,处于栈的最顶端,此时它处于可见并可和用户交互的运行状态。
- Paused。当Activity被另一个透明或者Dialog样式的Activity覆盖时的状态。此时它依然与窗口管理器保持连接,系统继续维护其内部状态,所以它仍然可见,但它已经失去了焦点故不可与用户交互。或称为暂停状态。在系统极度缺少内存的情况下会被杀死。
- Stopped。当Activity被另外一个Activity完全覆盖(activity处于后台),失去焦点并不可见,或称为停止状态。处于该状态的activity仍然是存活的,系统继续维护其内部状态,但是不再与窗口管理器保持连接。当系统需要内存时,该状态的activity会被杀死。
若一个activity进入paused或stopped状态,则系统可以调用finish()
结束它,或者通过杀死它的进程。下次再启动时,则不会保存内部状态。
也可以认为activity还有第四种状态:Killed。 Activity被系统杀死回收或者没有被启动时处于Killed状态。或称为死亡状态。
当一个Activity实例被创建、销毁或者启动另外一个Activity时,它在这四种状态之间进行转换,这种转换的发生依赖于用户程序的动作。
实现生命周期方法
当activity在以上4种状态之间转换时,就会调用相关的方法。我们可以通过覆盖这些方法来处理合适的工作。生命周期方法如下:
public class ExampleActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// activity被创建
}
@Override
protected void onStart() {
super.onStart();
// activity将要可见
}
@Override
protected void onResume() {
super.onResume();
// activity可见,执行后进入resumed状态
}
@Override
protected void onPause() {
super.onPause();
// 另一个activity获得焦点,执行后进入paused状态
}
@Override
protected void onStop() {
super.onStop();
// activity不可见,执行后进入stopped状态。
}
@Override
protected void onDestroy() {
super.onDestroy();
// activity将要销毁
}
}
注意覆盖这些方法时必须首先调用父类对应的方法,如上。
通过实现以上方法,我们可以监控activity生命周期中的以下三个嵌套时间段:
- entire lifetime
这个时间段在onCreate()
和onDestroy()
之间,也就是涉及整个生命周期。通常需要在onCreate()
设置初始化内容,在onDestroy()
释放系统资源。例如在activity中启动线程的话,一般要在onCreate()
启动,在onDestroy()
结束线程。 - visible lifetime
这个时间段在onStart()
和onStop()
之间。在这个时间段,我们能看到activity界面并能和它进行交互。例如调用onStop()
方法后该activity不再可见。在这个时间段我们能够给activity提供资源,用于处理和用户的交互。系统可能会多次调用这两个方法,原因在于activity会频繁地处于显示和隐藏状态。 - foreground lifetime
这个时间段在onResume()
和onPause()
之间。在这个时间段,activity处于其它任何activity之上,完全可见,并获得焦点。activity可能通过调用onPause()
方法频繁进出这个时间段。正是因为这个原因,要避免在onResume()
和onPause()
处理耗时的操作,减少用户等待的时间。
在android中,Activity的生命周期交给系统统一管理,所有的Activity 都是平等的。下图给出了生命周期处理过程,包含了Activity生命周期的七大方法。
Activity实例是由系统自动创建,并在不同的状态期间回调相应的方法。下面具体描述了以上七种方法。
方法 | 描述 | 之后是否能被杀死 | 下一步调用方法 |
---|---|---|---|
onCreate() | activity被创建时调用,一般可以在该方法中初始化基本信息,例如初始化UI组件activity。该方法传递一个Bundle对象,该对象保存activity之前的状态,即保存该Activity被销毁之前的信息(后续有详细介绍)。 | 否 | onStart() |
onRestart() | 在onStop() 之后调用,表示当进入stopped状态的activity重新启动。 | 否 | onStart() |
onStart() | activity将要可见前调用,若activity进入前台则调用onResumed() ;若隐藏则调用onStop() | 否 | onResume() 或onStop() |
onResume() | activity将要和用户交互前调用,处于栈顶位置,并处于运行状态(Resumed) | 否 | onPause() |
onPause() | 系统将要启动其它activity时调用,通常用于提交永久性保存数据,停止其它消耗CPU的操作等。它将尽可能地快速被执行,因为只有它执行完之后下一个activity才会启动。若activity返回前台则调用onResume() ;若activity不再可见则调用onStop() | 是 | onResume() 或onStop() |
onStop() | 当activity完全不可见时调用。原因可能是系统已经销毁,或者被另一个activity完全覆盖。若activity将要返回和用户交互的话,调用onRestart() ;若activity将要销毁则调用onDestroy() | 是 | onRestart() 或onDestroy() |
onDestroy() | 在activity被销毁之前。这是最后一个生命周期方法。调用原因可能有如下两个:调用finish() ,或系统暂时销毁该activity的实例节省空间。 | 是 | 无 |
第三列表明系统是否会在对应的方法执行完杀死带有该activity的进程,而不是继续执行activity其它的生命周期方法。onPause()
, onStop()
和onDestroy()
标记为是。onPause()
是第一个可能被杀死的情况,这个方法也是activity所在的进程被杀死之前保证被执行的最后一个方法(若系统内存严重不足的话,后两个方法不一定被执行)。因此,我们需要在onPause()
方法中保存需要永久保存的数据,但是必须选择哪些信息是必须保存的,这是因为,若在该方法中执行耗时的操作,则会阻止另一个activity进入前台,并会降低用户体验。
注意:activity被杀死也并不是完全按照上面的情况,也有可能在系统内存严重不足的情况下被杀死,这一部分将在另外一个章节详细介绍,有兴趣的可以持续关注一下我的博客。
保存activity状态
在前面也提到了activity在paused或stopped状态时,系统会保存它的内部状态。这是因为Activity对象仍然被保存在内存,包括所有的信息,成员变量以及状态。因此,当activity从paused或stopped状态转换到resumed状态时,这些状态自动恢复,而不用我们进行恢复。
然而,若因为系统内存不足而销毁Activity时,系统不能再简单地根据它的状态而重新启动它,而是必须重新创建activity。但是用户可能不知道系统已经销毁activity,仍然想从刚才的状态打开activity。这种情况下,我们就要保证用户的重要信息不能丢失,必须保存起来,可以通过另外的回调方法onSaveInstanceState()
实现保存。
在activity可能被系统销毁(非用户主动,例如1.按下HOME键时;2.按下电源按键(关闭屏幕)时;3.被另一个Activity实例覆盖时;4.屏幕切换或其它配置变化时)时,该Activity就会回调onSaveInstanceState()
保存当前UI状态,当然你也可以在这里保存临时数据。该方法接收一个Bundle对象,我们可以使用该对象保存键值对,例如使用该对象的putString()
,putInt()
等。然后,若系统杀死了该activity或应用程序进程,同时用户又重新回到该activity的话,系统就会将保存的Bundle对象传递给onCreate()
和onRestoreInstanceState()
。使用这两个中的任何一个都能将数据恢复onCreate()
不能自动恢复UI状态,而onRestoreInstanceState()
可以通过调用父类方法实现自动恢复)。当没有数据保存的时候Bundle传递的值为null(第一次创建activity就是这种情况)。下图就描述了上面的这种情况:
注意:
- 在activity销毁之前不能保证一定调用
onSaveInstanceState()
,因为有些情况下,没必要保存状态,例如用户明确指出销毁该activity(可以通过BACK键)。 - 若系统调用了
onSaveInstanceState()
,则肯定在onStop()
之前调用,可能在onPause()
之前调用。一般调用顺序为:
- 调用onPause()方法暂停当前Activity;
- 调用onSaveInstanceState()方法;
- 依次调用onStop(),onDestroy()销毁该Activity实例;
- 依次调用onCreate(),onStart()重新启动该Activity实例;
- 调用onRestoreInstanceState()方法恢复保存的数据;
- 调用onResume()启动。
onRestoreInstanceState()
不一定被调用。当Activity确实被系统销毁了才会回调onRestoreInstanceState()方法(重新实例化在onStart()之后)。而如果仅仅是停留在有这种可能性的情况下,则该方法不会被调用。
然而,即使我们没有实现onSaveInstanceState()
,activity的一些状态也会被保存,这是因为Activity类默认实现了该方法。默认情况下该方法实现了所有View对象的onSaveInstanceState()
,该方法和activity中的一样,也是用来保存对应View的状态。Android中大部分控件都实现了该方法,以便UI发生可见的变化时能够自动保存并在activity重新创建时恢复。例如EditText控件能够保存用户输入的任何信息,CheckBox能够保存是否被选中等。我们唯一需要做的就是给这些控件提供一个唯一的ID(使用`android:id属性),若不提供ID的话,系统不会保存该控件状态。
若我们不想让系统保存某个控件的状态,则可以设置android:saveEnabled
为false或调用setSaveEnabled()
方法。
尽管系统默认实现了onSaveInstanceState()
来保存关于UI的有用信息,但是我们通常仍然需要覆盖该方法来保存一些其他的默认不会被保存的额外信息。在覆盖该方法时我们首先要调用父类的方法,以便能够保存UI状态。同样在覆盖onRestoreInstanceState()
我们应该也先调用父类的方法来自动恢复UI状态。
注意:
- 由于
onSaveInstanceState()
不能保证肯定被执行,因此在这里保存的数据应该都是瞬时的UI状态,而不应该保存永久数据。对于永久数据应该在onPause()
中保存。
一个测试程序恢复数据能力的办法是切换设备屏幕。当屏幕被切换时,系统就会销毁activity并重新创建。仅此原因,完全恢复activity状态是非常重要的,原因很简单,用户经常会不断地切换屏幕。
处理配置变化
一些配置可能在程序运行时发生变化,如屏幕变化,键盘可见性,语言等。当发生配置变化时,系统就会重新创建运行的activity(系统调用onDestroy()
之后立刻调用onCreate()
)。这种设计的好处在于,当配置发生变化时,我们的程序可以自动适应新的配置,当然前提是我们提供系统可选择的资源,例如不同屏幕显示不同的布局文件。
处理这种保存和恢复数据的方法就是使用上一部分讨论过的onSaveInstanceState()
和onRestoreInstanceState()
(或onCreate()
)。
然而,有时候重启并恢复数据是非常耗时的,这样会造成用户体验下降,处理这种问题,有两种可选的方案:
- 在配置改变时保留一个对象
当配置发生变化时,我们运行重新启动activity,但是要保存一个状态对象给新的activity。 - 自己处理配置改变
当配置发生变化时,我们不允许activity重新启动,而是接受一个回调,我们可以在回调方法里更新必要的信息。
下面分别介绍这两种情况。
在配置改变时保留一个对象
如果允许重新启动activity的话,我们需要恢复大量的数据,重新建立网络连接等,这样完全的重新启动就会降低用户体验。同时,通过Bundle对象保存数据的话不能处理大的对象(如bitmaps)以及序列化反序列化等等,这些都会消耗大量的内存空间。在这样的情况下,我们可以通过Fragment缓解。该fragment可以包含状态对象的引用,方便我们恢复。
当由于配置发生变化而造成系统销毁我们的activity时,我们标记保存状态的fragments没有被销毁,因此,可以使用该fragment完成数据恢复。
为了在配置变化时,在fragment中保存数据,我们需要完成以下工作:
- 继承Fragment类,声明状态对象的引用;
- 在fragment被创建的时候调用
setRetainInstance(boolean)
; - 添加fragment到activity;
- 当activity被重启时,用FragmentManager检索该fragment,然后完成恢复。
例如,我们的fragment可以定义如下:
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
警告:虽然我们可以在fragment保存任何对象,但是我们绝对不能保存一个和activity关联的对象,例如:Drawable,Adapter,View或者其他和Context关联的对象。若这样做了,所有的View和初始的activity资源都会造成内存泄漏(应用程序一直持有它们,不能被GC回收)。
完成了fragment,下面我们可以向activity中添加该fragment,进而能够完成保存和恢复,例如:
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (RetainedFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
// 创建一个不带视图的fragment。。。。
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()
...
}
@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}
在上面的例子中,onCreate()
中添加一个fragment(同时保存activity的状态对象)或者恢复fragment里的数据。onDestroy()
中更新fragment保存的状态对象。
自己处理配置改变
若在某些特定的配置变化下,不需要更新数据的话,我们可以避免activity重新启动,那么我们可以声明activity将自己处理配置变化,阻止系统重新启动。
注意:自行处理配置改变的话将会非常复杂,系统不会为我们自动处理(但是UI状态不会被销毁,仍然显示)。这种方法应该是最后的选择,在大多数应用中不推荐使用。
为了声明activity处理配置改变,需要在manifest文件中的<activity>
中添加<android:configChanges>
属性,值表示我们处理哪些配置改变。最常用的两个是:orientation
和keyboardHidden
。前者在屏幕方向变化时阻止重启,后者在键盘改变时阻止重启。可以通过|
设置多个值。例如:
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
当设置的配置发生变化时,activity不会重新启动,而是调用`onConfigurationChanged(),该方法接收一个Configuration对象,用于指明新的配置,具体配置可以参考Configuration类。在调用该方法时,activity的资源对象自动更新来适应新的配置,因此我们很容易重新设置UI状态。
自android3.2(API13)开始,横竖屏切换也会导致屏幕大小改变。因此若想在屏幕发生变化时阻止重启,在<android:configChanges>
不仅需要添加orientation
,还需要添加screenSize
。
下面的例子实现了在配置发生变化时检查当前的设备方向:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}
在activity可以调用this.getResources().getConfiguration()
获取Configuration对象。Configuration成员变量的值都是static final int型,因此可以直接使用类调用。
大部分时候,我们不需要知道具体的配置变化,只要重新处理资源(对于该配置能够提供相应的选择)即可。例如,在资源更新时,我们可以通过setImageResoutce()
设置任何ImageViews,适应新的配置的资源就会自动使用。例如系统在layout-land和layout-port(在res目录下建立layout-land(横屏的layout)和layout-port(竖屏的layout)目录,相应的layout文件不变)目录下自动寻找合适布局。
当activity处理配置变化时,我们应该重新设置每一个UI控件(及不同的配置显示不同的布局),例如对于图片来说,要横竖屏改变。
协调activities
当一个activity启动另一个时,它们都经过了生命周期的切换。它们之间的切换时机是固定的,特别是在同一个进程中进行切换。下面是activity A切换到activity B的过程:
- A调用
onPause()
; - B依次调用
onCreate()
,onStart()
和onResume()
,现在B获得用户焦点; - 若A不再可见则调用
onStop()
。
这种可以预测的执行顺序可以方便我们管理activity切换之间的信息。
以窗口Dialog形式显示Activity
若要以窗口Dialog形式显示Activity则需在AndroidMainfast.xml文件的activity标签中添加android:theme
属性。