精通 Activity 四大启动模式

624 阅读8分钟

前言

Activity 具有4种启动模式,分别是:standard、singleTop、singleTask和singleInstance。

我们应该根据需求来为每个 Activity 指定合适的启动模式,可以在 AndroidManifest 清单文件中通过给 <activity> 标签指定 android:launchMode 属性来选择启动模式。理解这些启动模式对构建用户体验良好的应用来说至关重要。

standard

standard 是 Activity 默认的启动模式。

Android 使用返回栈(Task)来管理 Activity 实例,在 standard 启动模式下,每启动一个新的 Activity,它就会被压入当前任务的返回栈中,并且处于栈顶。系统不会关心这个 Activity 是否已经在返回栈中存在,每次启动都会创建一个新的 Activity 实例。

我们现在通过代码体验一下 standard 模式。当前项目有三个 Activity,分别是:FirstActivitySecondActivityThirdActivity,每个 Activity 对应的布局文件中都只有一个按钮。

修改 FirstActivityonCreate() 方法:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("FirstActivity", "onCreate, instance: $this, task: $taskId")
    
    // 使用 ViewBinding 进行视图绑定
    binding = FirstLayoutBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)

    binding.button1.setOnClickListener {
        val intent = Intent(this, FirstActivity::class.java)
        startActivity(intent)
    }
}

在按钮1的点击事件中,我们在 FirstActivity 的基础上再次启动了 FirstActivity

重新运行程序,连续点击两次按钮,可以看到 Logcat 日志信息:

D/FirstActivity: onCreate, instance: com.example.FirstActivity@a461f05, task: 671
D/FirstActivity: onCreate, instance: com.example.FirstActivity@a4a54c4, task: 671
D/FirstActivity: onCreate, instance: com.example.FirstActivity@b18f308, task: 671

此时返回栈中有三个 FirstActivity 的实例,都在同一个任务中。当你要退出应用时,你要按三次返回键才可以退出。

standard 模式的原理图:

standard 模式任务栈示意图

应用场景:

大多数普通的 Activity,都希望跳转到该页面时,是全新、干净的状态。

singleTop

standard (默认) 启动模式会使得同一个 Activity 被多次创建,即使它已经在栈顶。

为了解决这个问题,我们可以使用 singleTop 启动模式。当以 singleTop 模式启动新 Activity 时,如果发现当前返回栈的栈顶正好是要启动的 Activity 的实例,系统就会直接复用栈顶的这个 Activity 实例。

此时并不会创建新的 Activity 实例,onCreate() 方法也就不会被调用了,但这个已存在的 Activity 实例的 onNewIntent(intent: Intent) 方法会被调用,并将新的启动 Intent 对象作为参数传递进来。

onNewIntent() 方法的主要作用有两点:

  1. 接收新的启动 Intent 对象传递的数据

  2. 我们可以根据新传递的数据来更新界面或是执行对应逻辑

如果目标 Activity 不在栈顶,此时会创建新的 Activity 实例并调用其 onCreate() 方法。

还是通过代码来理解。修改 FirstActivity 的启动模式为 singleTop

<activity
    android:name=".FirstActivity"
    android:exported="true"
    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>

重写 FirstActivity 中的 onCreate()onNewIntent() 方法:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("FirstActivity", "onCreate, instance: ${this}, task: $taskId")
    binding = FirstLayoutBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // 处理初始 Intent 的数据
    handleIntentData(intent)

    binding.button1.setOnClickListener {
        val intent = Intent(this, FirstActivity::class.java)
        intent.putExtra("data_payload", "From Self: ${System.currentTimeMillis()}")
        startActivity(intent)
    }
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    Log.d("FirstActivity", "onNewIntent, instance: $this")

    // 更新当前 Activity 所拥有的 intent,这样后续 getIntent() 方法才会返回最新的Intent对象
    setIntent(intent)

    // 处理新的Intent数据
    handleIntentData(intent)
}

private fun handleIntentData(intent: Intent?) {
    // 处理 Intent 对象携带的数据
    val data = intent?.getStringExtra("data_payload") ?: "Initial or no data"
    Log.d("FirstActivity","Handling data: $data")
}

重新运行程序,在 FirstActivity 界面多次点击按钮,查看 Logcat 日志信息:

D/FirstActivity: onCreate, instance: com.example.FirstActivity@1585749, task: 672
D/FirstActivity: Handling data: Initial or no data
D/FirstActivity: onNewIntent, instance: com.example.FirstActivity@1585749
D/FirstActivity: Handling data: From Self: 1748544103773
D/FirstActivity: onNewIntent, instance: com.example.FirstActivity@1585749
D/FirstActivity: Handling data: From Self: 1748544104271

可以看到 onCreate() 方法只被调用了一次,后续不管你点击多少次按钮,都不会再创建新的 FirstActivity 实例。因为在当前 singleTop 的启动模式下,FirstActivity 已经处于栈顶,会直接复用该实例,并调用其 onNewIntent() 方法,更新 Activity 所持有的 Intent,并处理新传递过来的数据。

此时,我们只需按下一次返回键,就能够退出应用。

FirstActivity 未处于栈顶时,还是会创建新的 FirstActivity 实例的。

我们修改 FirstActivity 中按钮的点击事件,让它启动 SecondActivity

binding.button1.setOnClickListener {
    val intent = Intent(this, SecondActivity::class.java)
    startActivity(intent)
}

然后再修改 SecondActivity 中按钮的点击事件,让它启动 FirstActivity

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("SecondActivity", "onCreate, instance: ${this}, task: $taskId")

    // 标准视图绑定
    binding = SecondLayoutBinding.inflate(layoutInflater)
    val linearLayout = binding.root
    setContentView(linearLayout)

    binding.button2.setOnClickListener {
        val intent = Intent(this, FirstActivity::class.java)
        intent.putExtra("data_payload", "From SecondActivity: ${System.currentTimeMillis()}")
        startActivity(intent)
    }
}

现在重新运行,在 FirstActivity 界面点击按钮,会进入 SecondActivity,然后在 SecondActivity 界面点击按钮,又会重新进入 FirstActivity。日志打印信息如下:

D/FirstActivity: onCreate, instance: com.example.noactivity.FirstActivity@9387a7c, task: 674
D/FirstActivity: Handling data: Initial or no data
D/SecondActivity: onCreate, instance: com.example.noactivity.SecondActivity@f0a32fb, task: 674
D/FirstActivity: onCreate, instance: com.example.noactivity.FirstActivity@d07df97, task: 674
D/FirstActivity: Handling data: From SecondActivity: 1748544249222

可以看到系统创建了两个不同的 FirstActivity 实例,因为在 SecondActivity 启动 FirstActivity 时,此时栈顶是 SecondActivity,而不是 FirstActivity,所以会创建一个新的 FirstActivity 实例。

现在退出应用,也需要按三下。第一下退回到 SecondActivity,第二下回到第一个 FirstActivity,第三下退出应用。

singleTop 模式的原理图:

singleTop 模式任务栈示意图(栈顶复用或非栈顶新建)

应用场景:

搜索时,如果当前已经是搜索的结果页了,则可以复用并通过 onNewIntent() 更新界面。

singleTask

使用 singleTop 模式可以解决栈顶 Activity 重复创建的问题,但如果要启动的 Activity 没有在栈顶,还是会创建新实例。

这时就可以借助 singleTask 模式,它可以确保 Activity 在其指定的任务 (taskAffinity) 中只有一个实例。

默认情况下,taskAffinity 是包名,我们可以在清单文件中通过 <activity> 标签的 <android:taskAffinity> 属性来指定 Activity 属于哪个任务。

<activity
    android:name=".FirstActivity"
    android:exported="true"
    android:launchMode="singleTask"
    android:taskAffinity="xx.xx.xx"
    android:label="This is FirstActivity">

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

每当以 singleTask 模式启动新 Activity 时,系统会检查 taskAffinity 指定的任务中是否存在该 Activity 的实例。

  • 如果存在会直接复用该实例(所在的任务也会来到前台),并将在这个 Activity 之上的所有其他 Activity 移出栈中,然后这个已存在的 Activity 实例的 onNewIntent(Intent intent) 方法会被调用,onCreate() 方法不会执行;

  • 不存在,会查看taskAffinity 指向的任务是否存在,如果存在,会在该任务中创建新的 Activity 实例并压栈;不存在,会创建新任务,并将此 Activity 压入新任务中,此时该 Activity 是根 Activity。

修改 FirstActivity 的启动模式为 singleTask。然后在 FirstActivity 中重写 onRestart() 方法,并打印日志:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("FirstActivity", "onCreate, instance: $this, task: $taskId")
    binding = FirstLayoutBinding.inflate(layoutInflater)
    setContentView(binding.root)
    
    handleIntentData(intent) // 处理初始Intent

    binding.button1.setOnClickListener {
        val intent = Intent(this, SecondActivity::class.java)
        startActivity(intent)
    }
}

override fun onRestart() {
    super.onRestart()
    Log.d("FirstActivity", "onRestart, instance: $this")
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    Log.d("FirstActivity", "onNewIntent, instance: $this")
    setIntent(intent)
    handleIntentData(intent)
}

private fun handleIntentData(intent: Intent?) {
    val data = intent?.getStringExtra("source_info") ?: "Initial or no data"
    Log.d("FirstActivity", "Handling data: $data")
}

SecondActivity 中重写 onDestroy() 方法,并打印日志:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("SecondActivity", "onCreate, instance: $this, task: $taskId")
    binding = SecondLayoutBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.button2.setOnClickListener {
        val intent = Intent(this, FirstActivity::class.java)
        intent.putExtra("source_info", "From SecondActivity")
        startActivity(intent)
    }
}

override fun onDestroy() {
    super.onDestroy()
    Log.d("SecondActivity", "onDestroy, instance: $this")
}

现在重新运行,在 FirstActivity 界面点击按钮,会进入 SecondActivity,然后在 SecondActivity 界面点击按钮,又会重新进入 FirstActivity。日志打印信息如下:

D/FirstActivity: onCreate, instance: com.example.FirstActivity@1585749, task: 676
D/FirstActivity: Handling data: Initial or no data
D/SecondActivity: onCreate, instance: com.example.SecondActivity@d20ac2c, task: 676
D/FirstActivity: onRestart, instance: com.example.FirstActivity@1585749
D/FirstActivity: onNewIntent, instance: com.example.FirstActivity@1585749
D/FirstActivity: Handling data: From SecondActivity
D/SecondActivity: onDestroy, instance: com.example.SecondActivity@d20ac2c

可以看到,和前面不同的是,当从 SecondActivity 再次启动 FirstActivity 时,并不会创建一个新的 FirstActivity 的实例,而是复用之前的 FirstActivity 实例。并且 SecondActivity 因为在 FirstActivity 的上面会被销毁,FirstActivityonRestart()onNewIntent() 方法会被调用。

此时只需按一下返回键即可退出程序。

singleTask模式的原理图:

singleTask 模式任务栈示意图

应用场景:

应用的主界面,比如微信的主界面。确保用户总能回到同个主界面实例,并清除其他的临时页面。

singleInstance

singleInstance 模式是最为特殊和复杂的一个,使用 singleInstance 模式的 Activity,会创建一个新的返回栈(任务)来管理这个 Activity,这个新返回栈中只有这一个 Activity 实例,并且后续要启动这个 Activity,都只会复用这个唯一实例。

当这个已存在的 Activity 被再次启动时,它的 onNewIntent(Intent intent) 方法会被调用,同样,onCreate() 方法不会执行。

这么做的意义是什么?

因为 Activity 是允许被其他程序调用的,如果要让其他程序和当前程序共享同一个 Activity 的实例,就需要有一个单独的返回栈来管理这个 Activity,这样不管是哪个应用程序来访问这个 Activity,都可以共用同一个返回栈了,也就解决了共享同一个 Activity 的实例的问题。

还是通过代码来理解,修改 SecondActivity 的启动模式为 singleInstance,在 FirstActivityonCreate() 方法中,打印了当前返回栈的id,并启动 SecondActivity

private var launchCounter = 0
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("FirstActivity", "onCreate, instance: $this, Task id is $taskId")
    
    binding = FirstLayoutBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
    
    binding.button1.setOnClickListener {
        launchCounter++
        val intent = Intent(this, SecondActivity::class.java)
        intent.putExtra("launch_source", "FirstActivity_Launch_$launchCounter")
        startActivity(intent)
    }
}

同样在 SecondActivityonCreate()onNewIntent() 中打印信息,并启动 ThirdActivity

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("SecondActivity", "onCreate, Task id: $taskId, instance: $this")

    binding = SecondLayoutBinding.inflate(layoutInflater)
    val linearLayout = binding.root
    setContentView(linearLayout)
    
    handleIntentData(intent) // 处理初始的 Intent 数据

    binding.button2.setOnClickListener {
        val intent = Intent(this, ThirdActivity::class.java)
        startActivity(intent)
    }
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    Log.d("SecondActivity", "onNewIntent, Task id: $taskId, instance: $this")
    setIntent(intent)
    handleIntentData(intent)
}

private fun handleIntentData(intent: Intent?) {
    val source = intent?.getStringExtra("launch_source") ?: "Initial or unknown source"
    Log.d("SecondActivity", "Handling data from: $source")
}

最后在 ThirdActivityonCreate() 方法中,打印当前返回栈的id:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("ThirdActivity", "onCreate, Task id is $taskId, instance: $this")

    binding = ThirdLayoutBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
}

现在重新运行程序,在 FirstActivity 界面点击按钮进入 SecondActivity,然后在 SecondActivity 界面点击按钮进入 ThirdActivity,查看 Logcat 日志信息:

D/FirstActivity: onCreate, instance: com.example.FirstActivity@1585749, Task id is 677
D/SecondActivity: onCreate, Task id: 678, instance: com.example.SecondActivity@c33f7df
D/SecondActivity: Handling data from: FirstActivity_Launch_1
D/ThirdActivity: onCreate, Task id is 677, instance: com.example.ThirdActivity@8446584

可以看到 SecondActivity 的 Task 与其他两个 Activity 不同,说明 SecondActivity 确实是由单独的返回栈来管理的。

如果现在返回到 FirstActivity,再次点击按钮启动 SecondActivity,日志会新增:

D/SecondActivity: onNewIntent, Task id: 678, instance: com.example.SecondActivity@c33f7df
D/SecondActivity: Handling data from: FirstActivity_Launch_2

这代表了 SecondActivity 实例被复用了,并且通过 onNewIntent() 方法接收到了新数据。

并且我们按下返回键会从 ThirdActivity 返回到 FirstActivity,再次按下,会返回到 SecondActivity,再按下,才会退出程序。

这是因为 FirstActivityThirdActivity 在同一个返回栈中,ThirdActivity 出栈后,自然就会显示 FirstActivity 的界面。然后 FirstActivity 出栈,此时当前的返回栈就空了,于是就显示了另一个返回栈的栈顶 Activity,也就是 SecondActivitySecondActivity 出栈后,所有的返回栈都空了,这时就会退出应用。

singleInstance模式原理图:

singleInstance 模式任务栈示意图(独立任务) singleInstance 模式返回行为示意图

应用场景:

希望全局唯一的服务界面,比如电话的来电界面、系统的闹钟界面。

Intent Flags 与 LaunchMode 的优先级

除了在 AndroidManifest.xml 中使用 launchMode 属性外,我们还可以在代码中通过 Intent 对象的 flags 来影响 Activity 的启动行为。比如这样:

val intent = Intent(this, SecondActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP

常见的标志枚举值有:Intent.FLAG_ACTIVITY_NEW_TASKIntent.FLAG_ACTIVITY_SINGLE_TOPIntent.FLAG_ACTIVITY_CLEAR_TOP。分别表示:在新任务中启动、复用处于栈顶的 Activity 实例、使要启动的 Activity 实例成为栈顶。

launchMode 定义的是 Activity 的默认行为,而 Intent Flags 可以在启动时更精细化的调整。如果同时设置了,那么 Intent Flag 的优先级更高。