Activity的生命周期
1.返回栈
其实Android是使用任务(task)来管理Activity的,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()方法去销毁一个Activity时,处于栈顶的Activity就会出栈,前一个入栈的Activity就会重新处于栈顶的位置。系统总是会显示处于栈顶的Activity给用户。
2.Activity状态
每个Activity在其生命周期中最多可能会有4种状态。
Android Activity
的生命周期是其运行过程中所经历的一系列状态。每个 Activity
在其生命周期中可能会经历以下几种状态:
- 活跃状态(运行状态)(Active or Running) :
- 这是
Activity
的正常运行状态。它处于用户界面的最顶层,并且能够接收用户的输入。 - 系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。
- 这是
- 暂停状态(Paused) :
- 当一个新的
Activity
部分遮挡了当前Activity
,或者当前Activity
调用了sleep
方法时,它就会进入暂停状态。在这种状态下,Activity
仍然可见,但是无法接收用户输入。 - 当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种Activity。
- 当一个新的
- 停止状态(Stopped) :
- 当
Activity
被另一个Activity
完全遮挡时,它就会进入停止状态。在这种状态下,Activity
不再可见,但是它的状态和成员信息仍然被保留。 - 系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
- 当
- 非可见状态(销毁状态)(Invisible) :
- 当系统需要为
Activity
显示一个透明的活动,如对话框,或者Activity
调用了finish()
方法后,它就会进入非可见状态。在这种状态下,Activity
对用户完全不可见,系统可能会随时将其销毁 - 一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的Activity,以保证手机的内存充足。
- 当系统需要为
3.Activity的生存期
onCreate(Bundle savedInstanceState)
:- 这是
Activity
生命周期中的第一个回调方法,用于进行Activity
的初始化工作。当Activity
第一次创建时被调用。 - 您可以在这里执行一次性的初始化操作,如设置布局和恢复保存的状态。
- 这是
onStart()
:- 当
Activity
从停止状态变为可见状态时调用。 - 此时
Activity
对用户可见,但还没有进入前台。
- 当
onResume()
:- 当
Activity
进入前台并开始与用户交互时调用。这是Activity
运行时的活跃状态,可以执行UI更新和用户交互。
- 当
onPause()
:- 当
Activity
部分被另一个Activity
遮挡,即将失去焦点时调用。这是一个保存当前Activity
状态的好时机,例如保存用户输入或应用状态。 - 我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。
- 当
onStop()
:- 当
Activity
不再可见时调用。这可能是因为另一个Activity
完全覆盖了它,或者Activity
调用了finish()
方法。在这个方法中,您应该释放不需要的资源。
- 当
onRestart()
:- 当
Activity
从停止状态恢复到可见状态(运行状态)时调用。这通常发生在Activity
被另一个Activity
短暂覆盖后。
- 当
onDestroy()
:- 当
Activity
被销毁时调用。这是一个释放资源和取消注册广播接收器、停止服务等操作的好时机。
- 当
onSaveInstanceState(Bundle outState)
:- 这个方法在
Activity
被系统销毁之前调用,但Activity
被销毁并不是由于用户操作,如屏幕旋转或配置更改。您可以在这里保存Activity
的状态,这些状态将在Activity
重建时通过onCreate(Bundle savedInstanceState)
方法传递。
- 这个方法在
- 创建(Creation) :
- 这个阶段由
onCreate(Bundle savedInstanceState)
方法开始,表示Activity
正在被创建。如果Activity
由于配置更改(如屏幕旋转)被系统销毁并重新创建,savedInstanceState
参数将携带之前的状态信息。这个阶段通常用于初始化Activity
,设置布局和恢复状态。
- 这个阶段由
- 运行(Active) :
- 这个阶段包括
onStart()
和onResume()
两个回调方法。onStart()
表示Activity
变得可见,但用户可能还不能与之交互。紧接着,onResume()
表示Activity
已经准备好与用户交互,它现在处于前台并且是活跃状态。
- 这个阶段包括
- 暂停(Pause) :
- 这个阶段由
onPause()
和onStop()
两个回调方法组成。onPause()
表示Activity
即将失去焦点,但用户不应该在这个时刻保存Activity
的持久状态,因为这个方法可能会被频繁调用。紧接着,onStop()
表示Activity
已经不可见,您可以在这个时刻释放不需要的资源。
- 这个阶段由
onRestart()
方法是特殊的,因为它既不与创建阶段对应,也不与运行或暂停阶段对应。它在 Activity
从停止状态变为可见状态时被调用,通常发生在 Activity
被另一个 Activity
短暂覆盖后,或者当 Activity
被系统销毁并需要重新启动时。
4.Activity被回收了怎么办
前面我们说过,当一个Activity进入了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次。
这样看上去好像一切正常,可是别忽略了一个重要问题:Activity A中是可能存在临时数据和状态的。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。
如果我们的应用出现了这种情况,是会比较影响用户体验的,所以得想想办法解决这个问题。其实,Activity中还提供了一个onSaveInstanceState()
回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。
onSaveInstanceState()
方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取值,第二个参数是真正要保存的内容。
Java
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}
数据是已经保存下来了,那么我们应该在哪里进行恢复呢?
细心的你也许早就发现,我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在Activity被系统回收之前,你通过onSaveInstanceState()方法保存数据,这个参数就会带有之前保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可。
修改MainActivity的onCreate()方法,如下所示:
Java
@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 is " + tempData);
}
}
使用Bundle保存和取出数据是不是有些似曾相识呢?没错!我们在使用Intent传递数据时也用的类似的方法。这里提醒一点,Intent还可以结合Bundle一起用于传递数据。首先我们可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标Activity之后,先从Intent中取出Bundle,再从Bundle中一一取出数据。
8.Activity的启动模式
在实际项目中我们应该根据特定的需求为每个Activity指定恰当的启动模式。
启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给标签指定android:launchMode
属性来选择启动模式。下面我们来逐个进行学习。
standard
standard是Activity默认的启动模式,在不进行显式指定的情况下,所有Activity都会自动使用这种启动模式。到目前为止,我们写过的所有Activity都是使用的standard模式。
经过上一节的学习,你已经知道了Android是使用返回栈来管理Activity的,在standard模式下,每当启动一个新的Activity,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的Activity,系统不会在乎这个Activity是否已经在返回栈中存在,每次启动都会创建一个该Activity的新实例。我们现在通过实践来体会一下standard模式,这次还是在ActivityTest项目的基础上修改。
首先关闭ActivityLifeCycleTest项目,打开ActivityTest项目。修改FirstActivity中onCreate()方法的代码,如下所示:
Java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("FirstActivity", this.toString());
setContentView(R.layout.first_layout);
Button button1 = findViewById(R.id.button1); // 确保你的按钮ID是button1
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, FirstActivity.class);
startActivity(intent);
}
});
}
singleTop
可能在有些情况下,你会觉得standard模式不太合理。Activity明明已经在栈顶了,为什么再次启动的时候还要创建一个新的Activity实例呢?别着急,这只是系统默认的一种启动模式而已,你完全可以根据自己的需要进行修改,比如使用singleTop模式。当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例。
我们还是通过实践来体会一下,修改AndroidManifest.xml中FirstActivity的启动模式,如下所示:
Java
<application ...>
<activity android:name=".FirstActivity"
android:launchMode="singleTop"
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>
但是之后不管你点击多少次按钮都不会再有新的打印信息出现,因为目前FirstActivity已经处于返回栈的栈顶,每当想要再启动一个FirstActivity时,都会直接使用栈顶的Activity,因此FirstActivity只会有一个实例,仅按一次Back可以退出程序。不过当FirstActivity并未处于栈顶位置时,再启动FirstActivity还是会创建新的实例的。
下面我们来实验一下,修改FirstActivity中onCreate()方法的代码
这次我们点击按钮后启动的是SecondActivity。然后修改SecondActivity中onCreate()方法的代码
我们在SecondActivity中添加了一行打印日志,并且在按钮点击事件里加入了启动FirstActivity的代码。现在重新运行程序,在FirstActivity界面点击按钮进入SecondActivity,然后在SecondActivity界面点击按钮,又会重新进入FirstActivity。
可以看到系统创建了两个不同的FirstActivity实例,这是由于在SecondActivity中再次启动FirstActivity时,栈顶Activity已经变成了SecondActivity,因此会创建一个新的FirstActivity实例。现在按下Back键会返回到SecondActivity,再次按下Back键又会回到FirstActivity,再按一次Back键才会退出程序。
singleTask
使用singleTop模式可以很好地解决重复创建栈顶Activity的问题,但是正如你在上一节所看到的,如果该Activity并没有处于栈顶的位置,还是可能会创建多个Activity实例的。那么有没有什么办法可以让某个Activity在整个应用程序的上下文中只存在一个实例呢?这就要借助singleTask模式来实现了。当Activity的启动模式指定为singleTask,每次启动该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果发现已经存在则直接使用该实例,并把在这个Activity之上的所有其他Activity统统出栈,如果没有发现就会创建一个新的Activity实例。
我们还是通过代码来更加直观地理解一下。修改AndroidManifest.xml中FirstActivity的启动模式:
Java
<activity android:name=".FirstActivity"
android:launchMode="singleTask"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
然后在FirstActivity中添加onRestart()方法,并打印日志:Log.d("FirstActivity", "onRestart")
最后在SecondActivity中添加onDestroy()方法,并打印日志:Log.d("SecondActivity", "onDestroy")
其实从打印信息中就可以明显看出,在SecondActivity中启动FirstActivity时,会发现返回栈中已经存在一个FirstActivity的实例,并且是在SecondActivity的下面,于是SecondActivity会从返回栈中出栈,而FirstActivity重新成为了栈顶Activity,因此FirstActivity的onRestart()方法和SecondActivity的onDestroy()方法会得到执行。现在返回栈中只剩下一个FirstActivity的实例了,按一下Back键就可以退出程序。
singleInstance
singleInstance模式应该算是4种启动模式中最特殊也最复杂的一个了,你也需要多花点工夫来理解这个模式。不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。
那么这样做有什么意义呢?想象以下场景,假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。
SecondActivity的Task id不同于FirstActivity和ThirdActivity,这说明SecondActivity确实是存放在一个单独的返回栈里的,而且这个栈中只有SecondActivity这一个Activity。
然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?
其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。
Activity的最佳实践
知晓当前是在哪一个Activity
这个技巧将教会你如何根据程序当前的界面就能判断出这是哪一个Activity。可能你会觉得挺纳闷的,我自己写的代码怎么会不知道这是哪一个Activity呢?然而现实情况是,在你进入一家公司之后,更有可能的是接手一份别人写的代码,因为你刚进公司就正好有一个新项目启动的概率并不高。阅读别人的代码时有一个很头疼的问题,就是当你需要在某个界面上修改一些非常简单的东西时,却半天找不到这个界面对应的Activity是哪一个。学会了本节的技巧之后,这对你来说就再也不是难题了。
我们还是在ActivityTest项目的基础上修改,首先需要新建一个BaseActivity类。右击com.example.activitytest包→New→Kotlin File/Class,在弹出的窗口中输入BaseActivity,创建类型选择Class
注意,这里的BaseActivity和普通Activity的创建方式并不一样,因为我们不需要让BaseActivity在AndroidManifest.xml中注册,所以选择创建一个普通的类就可以了。然后让BaseActivity继承自AppCompatActivity,并重写onCreate()方法
Java
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", this.getClass().getSimpleName());
}
}
我们在onCreate()方法中加了一行日志,用于打印当前实例的类名。我们先是获取了当前实例的Class对象,然后再调用simpleName获取当前实例的类名。
接下来我们需要让BaseActivity成为ActivityTest项目中所有Activity的父类
修改FirstActivity、SecondActivity和ThirdActivity的继承结构,让它们不再继承自AppCompatActivity,而是继承自BaseActivity。而由于BaseActivity又是继承自AppCompatActivity的,所以项目中所有Activity的现有功能并不受影响,它们仍然继承了Activity中的所有特性。现在重新运行程序,然后通过点击按钮分别进入FirstActivity、SecondActivity和ThirdActivity的界面,这时观察Logcat中的打印信息
现在每当我们进入一个Activity的界面,该Activity的类名就会被打印出来,这样我们就可以时刻知晓当前界面对应的是哪一个Activity了。
随时随地退出程序
如果目前你手机的界面还停留在ThirdActivity,你会发现当前想退出程序是非常不方便的,需要连按3次Back键才行。按Home键只是把程序挂起,并没有退出程序。如果我们的程序需要注销或者退出的功能该怎么办呢?看来要有一个随时随地都能退出程序的方案才行。其实解决思路也很简单,只需要用一个专门的集合对所有的Activity进行管理就可以了。
下面我们就来实现一下。新建一个单例类ActivityCollector作为Activity的集合,代码如下所示
Java
public class ActivityCollector {
private static ActivityCollector instance;
private ArrayList<Activity> activities;
private ActivityCollector() {
activities = new ArrayList<>();
}
public static synchronized ActivityCollector getInstance() {
if (instance == null) {
instance = new ActivityCollector();
}
return instance;
}
public void addActivity(Activity activity) {
activities.add(activity);
}
public void removeActivity(Activity activity) {
activities.remove(activity);
}
public void finishAll() {
for (Activity activity : activities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
activities.clear();
}
}
这里使用了单例类,是因为全局只需要一个Activity集合。
在集合中,我们通过一个ArrayList来暂存Activity,然后提供了一个addActivity()方法,用于向ArrayList中添加Activity;
提供了一个removeActivity()方法,用于从ArrayList中移除Activity;
最后提供了一个finishAll()方法,用于将ArrayList中存储的Activity全部销毁。
注意在销毁Activity之前,我们需要先调用activity.isFinishing来判断Activity是否正在销毁中,因为Activity还可能通过按下Back键等方式被销毁,如果该Activity没有正在销毁中,我们再去调用它的finish()方法来销毁它。
修改BaseActivity中的代码
Java
public class ActivityCollector {
private static ActivityCollector instance;
private List<Activity> activities = new ArrayList<>();
private ActivityCollector() {}
public static synchronized ActivityCollector getInstance() {
if (instance == null) {
instance = new ActivityCollector();
}
return instance;
}
public void addActivity(Activity activity) {
activities.add(activity);
}
public void removeActivity(Activity activity) {
activities.remove(activity);
}
public void finishAll() {
for (Activity activity : activities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
activities.clear();
}
}
在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的Activity添加到集合里。
然后在BaseActivity中重写onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明从集合里移除一个马上要销毁的Activity。
从此以后,不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()方法就可以
android.os.Process.killProcess(android.os.Process.myPid())
killProcess()方法用于杀掉一个进程,它接收一个进程id参数,我们可以通过myPid()方法来获得当前程序的进程id。需要注意的是,killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序。
启动Activity的最佳写法
启动Activity的方法相信你已经非常熟悉了,
首先通过Intent构建出当前的"意图",
然后调用startActivity()或startActivityForResult()方法将Activity启动起来,
如果有数据需要在Activity之间传递,也可以借助Intent来完成。
在真正的项目开发中经常会出现对接的问题。比如SecondActivity并不是由你开发的,但现在你负责开发的部分需要启动SecondActivity,而你却不清楚启动SecondActivity需要传递哪些数据。
这时无非就有两个办法:一个是你自己去阅读SecondActivity中的代码,另一个是询问负责编写SecondActivity的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决上面的窘境。修
改SecondActivity中的代码,如下所示:
Java
public class SecondActivity extends BaseActivity {
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1); // 修复了方括号为正常字符
intent.putExtra("param2", data2);
context.startActivity(intent);
}
}
我们在SecondActivity 中添加了一个actionstart()方法,在这个方法中完成了Intent的构建,
另外所有SecondActivity中需要的数据都是通过actionStart()方法的参数传递过来的,然后把它们存储到Intent中,最后调用 startActivity()方法启动 SecondActivity。
现在只需要一行代码就可以启动SecondActivity
button1.setOnClickListener { SecondActivity.actionStart(this, "data1", "data2") }
context.startActivity(intent);
}
}
我们在SecondActivity 中添加了一个actionstart()方法,在这个方法中完成了Intent的构建,
另外所有SecondActivity中需要的数据都是通过actionStart()方法的参数传递过来的,然后把它们存储到Intent中,最后调用 startActivity()方法启动 SecondActivity。
现在只需要一行代码就可以启动SecondActivity
button1.setOnClickListener { SecondActivity.actionStart(this, "data1", "data2") }