【Android】Activity的生命周期

Activity的生命周期

1.返回栈

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

2.Activity状态

每个Activity在其生命周期中最多可能会有4种状态。

Android Activity 的生命周期是其运行过程中所经历的一系列状态。每个 Activity 在其生命周期中可能会经历以下几种状态:

  1. 活跃状态(运行状态)(Active or Running) :
    1. 这是 Activity 的正常运行状态。它处于用户界面的最顶层,并且能够接收用户的输入。
    2. 系统最不愿意回收的就是处于运行状态的Activity,因为这会带来非常差的用户体验。
  2. 暂停状态(Paused) :
    1. 当一个新的 Activity 部分遮挡了当前 Activity,或者当前 Activity 调用了 sleep 方法时,它就会进入暂停状态。在这种状态下,Activity 仍然可见,但是无法接收用户输入。
    2. 当一个Activity不再处于栈顶位置,但仍然可见时,Activity就进入了暂停状态。你可能会觉得,既然Activity已经不在栈顶了,怎么会可见呢?这是因为并不是每一个Activity都会占满整个屏幕,比如对话框形式的Activity只会占用屏幕中间的部分区域。处于暂停状态的Activity仍然是完全存活着的,系统也不愿意回收这种Activity(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种Activity。
  3. 停止状态(Stopped) :
    1. Activity 被另一个 Activity 完全遮挡时,它就会进入停止状态。在这种状态下,Activity 不再可见,但是它的状态和成员信息仍然被保留。
    2. 系统仍然会为这种Activity保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。
  4. 非可见状态(销毁状态)(Invisible) :
    1. 当系统需要为 Activity 显示一个透明的活动,如对话框,或者 Activity 调用了 finish() 方法后,它就会进入非可见状态。在这种状态下,Activity 对用户完全不可见,系统可能会随时将其销毁
    2. 一个Activity从返回栈中移除后就变成了销毁状态。系统最倾向于回收处于这种状态的Activity,以保证手机的内存充足。

3.Activity的生存期

  1. onCreate(Bundle savedInstanceState) :
    1. 这是 Activity 生命周期中的第一个回调方法,用于进行 Activity 的初始化工作。当 Activity 第一次创建时被调用。
    2. 您可以在这里执行一次性的初始化操作,如设置布局和恢复保存的状态。
  2. onStart() :
    1. Activity 从停止状态变为可见状态时调用。
    2. 此时 Activity 对用户可见,但还没有进入前台。
  3. onResume() :
    1. Activity 进入前台并开始与用户交互时调用。这是 Activity 运行时的活跃状态,可以执行UI更新和用户交互。
  4. onPause() :
    1. Activity 部分被另一个 Activity 遮挡,即将失去焦点时调用。这是一个保存当前 Activity 状态的好时机,例如保存用户输入或应用状态。
    2. 我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity的使用。
  5. onStop() :
    1. Activity 不再可见时调用。这可能是因为另一个 Activity 完全覆盖了它,或者 Activity 调用了 finish() 方法。在这个方法中,您应该释放不需要的资源。
  6. onRestart() :
    1. Activity 从停止状态恢复到可见状态(运行状态)时调用。这通常发生在 Activity 被另一个 Activity 短暂覆盖后。
  7. onDestroy() :
    1. Activity 被销毁时调用。这是一个释放资源和取消注册广播接收器、停止服务等操作的好时机。
  8. onSaveInstanceState(Bundle outState) :
    1. 这个方法在 Activity 被系统销毁之前调用,但 Activity 被销毁并不是由于用户操作,如屏幕旋转或配置更改。您可以在这里保存 Activity 的状态,这些状态将在 Activity 重建时通过 onCreate(Bundle savedInstanceState) 方法传递。
  1. 创建(Creation) :
    1. 这个阶段由 onCreate(Bundle savedInstanceState) 方法开始,表示 Activity 正在被创建。如果 Activity 由于配置更改(如屏幕旋转)被系统销毁并重新创建,savedInstanceState 参数将携带之前的状态信息。这个阶段通常用于初始化 Activity,设置布局和恢复状态。
  2. 运行(Active) :
    1. 这个阶段包括 onStart()onResume() 两个回调方法。onStart() 表示 Activity 变得可见,但用户可能还不能与之交互。紧接着,onResume() 表示 Activity 已经准备好与用户交互,它现在处于前台并且是活跃状态。
  3. 暂停(Pause) :
    1. 这个阶段由 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") }
相关推荐
阿巴斯甜12 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴15 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android