1 Activity 的 LaunchMode
Activity 为什么需要启动模式?在默认情况下,当我们多次启动同一个 Activity 的时候,系统会创建多个实例并把它们一一放入任务栈中,当点击 back 键,会发现这些 Activity 会一一回退。任务栈是一种“后进后出”的栈结构,这个比较好理解,每按一下 back 键就会有一个 Activity 出栈,直到栈空为止,当栈中无任何 Activity 的时候,系统就会回收这个任务栈。这里有一个问题:多次启动同一个 Activity,系统重复创建多个实例。Android 在设计的是欧也考虑到了这个问题,所以它提供了启动模式来修改系统的默认行为。目前有四种启动模式:standard、singleTop、singleTask 和 singleInstance。
以下是各种启动模式的含义:
standard:
标准模式,这也是系统的默认模式。每次启动一个 Activity 都会重现创建一个新的实例,不管这个实例是否已经存在。 被创建的实例的声明周期符合典型情况下 Activity 的生命周期。这是一种典型的多实例实现,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个 Activity,那么这个 Activity 就运行在启动它的那个 Activity 所在的栈中。比如 Activity A 启动了 Activity B(B 是标准模式),那么 B 就会进入到 A 所在的栈中。
通过实践来体会一下 standard 模式,如下所示:
class MainActivity : AppCompatActivity() {
private val TAG = "CAH"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "standard: $this")
button.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
}
// CAH: standard: com.example.kotlintest.MainActivity@669e9d2
// CAH: standard: com.example.kotlintest.MainActivity@c094efd
// CAH: standard: com.example.kotlintest.MainActivity@bc2fa67
从打印信息中可以看出,每点击一次按钮,就会创建出一个新的 MainActivity 实例。此时返回栈中也会存在 3 个 MainActivity 的实例,因此你需要连按 3 次 Back 键才能退出程序。
singleTop:
通知消息打开页面
栈顶复用模式。在这种模式下,如果新 Activity 已经位于任务栈的栈顶,那么此 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的 onNewIntent 方法会被回调,通过此方法的参数可以取出当前请求的信息。 需要注意的是,这个 Activity 的 onCreate、onStart 不会被系统调用,因为它并没有发生改变。如果新的 Activity 实例已经存在但是不位于栈顶,那么新 Activity 仍然会重新创建。举个例子,假设目前栈内的情况为 ABCD,其中 ABCD 为四个 Activity,A 位于栈底,D 位于栈顶,这个时候假设要再次启动 D,如果 D 的启动模式为 singleTop,那么栈内的情况仍然是 ABCD;如果 D 的启动模式为 standard,那么由于 D 被重新创建,导致栈的情况就变为 ABCDD。
修改 AndroidManifest.xml 中 MainActivity 的启动模式,如下所示:
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
重新运行程序,查看 Logcat,如下所示:
//CAH: standard: com.example.kotlintest.MainActivity@669e9d2
但是之后不管点击多少次按钮都不会再有新的打印信息出现,因为目前 MainActivity 已经处于返回栈的栈顶,每当想要再启动一个 MainActivity 时,都会直接使用栈顶的 Activity,因此,MainActivity 只会有一个实例,仅按一次 Back 键就可以退出程序。不过当 MainActivity 并未处于栈顶位置时,再启动 MainActivity 还是会创建新的实例的。
singleTask:
主界面
栈内复用模式。这是一种单例模式,在这种模式下,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,和 singleTop 一样,系统也会回调其 onNewIntent。 具体一点,当一个具有 singleTask 模式的 Activity 请求启动后,比如 Activity A,系统首先会寻找是否存在 A 想要的任务栈,如果不存在,就重新创建一个任务栈,然后创建 A 的实例后把 A 放到栈中。如果存在 A 所需要的任务栈,这时需要看 A 是否在栈中有实例存在,如果有实例存在,那么系统会把 A 调到栈顶并调用它的 onNewIntent 方法,如果实例不存在,就创建 A 的实例并把 A 压入栈中。举例说明:
- 比如目前任务栈 S1 中的情况为 ABC,这个时候 Activity D 以 singleTask 模式请求启动,其所需要的任务栈为 S2,由于 S2 和 D 的实例均不存在,所以系统会先创建任务栈 S2,然后再创建 D 的实例并将其入栈到 S2。
- 另外一种情况,假设 D 所需要的任务栈为 S1,其他情况如上面例子所示,那么由于 S1 已经存在,所以系统会直接创建 D 的实例并将其入栈到 S1。
- 如果 D 所需要的任务栈为 S1,并且当前任务栈 S1 的情况为 ADBC,根据栈内复用的原则,此时 D 不会重新创建,系统会把 D 切换到栈顶并调用其 onNewIntent 方法,同时由于 singleTask 默认具有clearTop 的效果,会导致栈内所有 D 上面的 Activity 全部出栈,由于最终 S1 中的情况为 AD。
singleInstance:
呼叫来电页面
单实例模式。这是一种加强的 singleTask 模式,它除了具有 singleTask 模式的所有特性之外,还加强了一点,那就后,是具有此种模式的 Activity 只能单独地位于一个任务栈中, 换句话说,比如 Activity A 是 singleInstance 模式,当 A 启动后,系统会为它创建一个新的任务栈,然后 A 独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的 Activity ,除非这个独特的任务栈被系统销毁了。
修改 AndroidManifest.xml 中 SecondActivity 的启动模式:
<activity
android:name=".SecondActivity"
android:launchMode="singleInstance"
android:exported="true" />
先将 SecondActivity 的启动模式指定为 singleInstance,然后修改 FirstActivity.onCreate() 方法的代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(TAG, "MainActivity—task id is $taskId")
button.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
}
这里在 onCreate() 方法中打印了当前返回栈的 id。注意上述代码中的 taskId 实际上调用 的是父类的 getTaskId() 方法。然后修改 SecondActivity.onCreate() 方法的代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_normal)
Log.e(TAG, "SecondActivity—task id is: $taskId")
button.setOnClickListener {
val intent = Intent(this, ThirdActivity::class.java)
startActivity(intent)
}
}
最后修改 ThirdActivity.onCreate() 方法的代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third)
Log.e("CAH", "ThirdActivity—task id is: $taskId")
}
现在重新运行程序,在 FirstActivity 界面点击按钮进入SecondActivity,然后在
重新运行程序,在 FirstActivity 界面点击按钮进入 SecondActivity,然后在 SecondActivity 界面点击按钮进入 ThirdActivity。查看 Logcat 中的打印信息,如图所示:
// CAH: FistActivity—task id is 2606
// CAH: SecondActivity—task id is: 2607
// CAH: ThirdActivity—task id is: 2606
可以看到,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 键,这时所有返回栈都已经空了,也就自然退出了程序。
上面介绍了 4 种启动模式,这里需要指出一种情况,假设目前有 2 个任务栈,前台任务栈的情况为 AB,而后台任务栈的情况为 CD,这里假设 CD 的启动模式均为 singleTask。现在请求启动 D,那么整个后台任务栈都会被切换到前台,这个时候整个后退列表变成了 ABCD。当用户按 Back 键返回的时候,列表中的 Activity 会一一出栈,如果不是请求启动 D 而且启动 C,那么情况就不一样了,如下所示:
另外一个问题是,在 singleTask 启动模式中,多次提到某个 Activity 所需的任务栈,什么是 Activity 所需要的任务栈呢?这里要从一个参数说起:TaskAffinity,可以翻译为任务相关性。这个参数标识了一个 Activity 所需要的任务栈的名字,默认情况下,所有 Activity 所需要的任务栈的名字为应用的包名。当然,也可以为每个 Activity 都单独制定 TaskAffinity 属性,这个属性值必须不能和包名相同,否则就相当于没有指定,TaskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用,在其他情况下没有意义。另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的 Activity 位于暂停状态,用户可以通过切换将后台任务栈再次调到前台。
当 TaskAffinity 和 singleTask 启动模式配对使用的时候,它是具有该模式的 Activity 的目前任务栈的名字,待启动的 Activity 会运行在名字和 TaskAffinity 相同的任务栈中。
当 taskAffinity 和 allowTaskReparenting 结合的时候,这种情况比较复杂,会产生特殊的效果。当一个应用 A 启动了应用 B 的某个 Activity 后,如果这个 Activity 的 allowTaskReparenting 属性为 true 的话,那么当应用 B 被启动后,此 Activity 会直接从应用 A 的任务栈转移到应用 B 的任务栈中。
再具体点,比如现在有2
个应用A
和B
,A
启动了B
的一个Activity C
,然后按Home
键回到桌面上,然后再单击B
的桌面图标,这个时候斌故事启动了B
的主Activity
,而是重新显示了已经被应用A
启动的Activity C
,或者说,C
从A
的任务栈转移到了B
的任务栈中。可以这么理解,由于A
启动了C
,这个时候C
只能运行在A
的任务栈中,但是C
属于B
应用,正常情况下,它的TaskAffinity
值肯定不可能和A
的任务栈相同(因为包名不同)。所以,当B
被启动后,B
会创建自己的任务栈,这个时候系统发现C
原本想要的任务栈已经被创建了,所以就把C
从A
的任务栈中转移过来了。
如何给Activity
指定启动模式呢?有两种方法,第一种是通过AndroidManifest
为Activity
指定启动模式,如下所示:
<activity
android:name=".MainActivity"
android:launchMode="singleTask">
</activity>
另一种情况是通过在Intent
中设置标志位来为Activity
指定启动模式,比如:
Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
这两种方式都可以为Activity
指定启动模式,但是二者还是有区别的。首先,优先级上,第二种方式的优先级要高于第一种,当两种同时存在时,以第二种方式为准;其次,上述两种方式在限定范围上有所不同,比如,第一种方式无法直接为Activity
设定FLAG_ACTIVITY_CLEAR_TOP
标识,而第二种方法无法为Activity
指定singleInstance
模式。
下面通过一个例子来体验启动模式的使用效果。
下面是启动标准模式的SecondActivity
:
Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.putExtra("time", System.currentTimeMillis());
startActivity(intent);
启动singleTask
模式的MainActivity
:
多次重复启动MainActivity
:
findViewById(R.id.skip).setOnClickListener(v -> {
Intent intent = new Intent();
intent.setClass(MainActivity.this, MainActivity.class);
intent.putExtra("time", System.currentTimeMillis());
startActivity(intent);
});
虽然多次启动MainActivity
,但是它只有一个实例在任务栈中。
adb shell dumpsys
检测Android
的Activity
任务栈
adb shell dumpsys activity
---------------查看ActvityManagerService
所有信息
adb shell dumpsys activity activities
----------查看Activity
组件信息
adb shell dumpsys activity services
-----------查看Service
组件信息
adb shell dumpsys activity providers
----------产看ContentProvider
组件信息
adb shell dumpsys activity broadcasts
--------查看BraodcastReceiver
信息
adb shell dumpsys activity intents
--------------查看Intent
信息
adb shell dumpsys activity processes
---------查看进程信息
对于onNewIntent
方法:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.e(TAG, "onNewIntent, time = " + intent.getLongExtra("time", 0));
}
从下面的日志中可以看出,尽管启动了2
次MainActivity
,但MainActivity
并没有重建,只是暂停了一下,然后调用了onNewIntent
,接着调用onResume
就又继续了:
去掉android:launchMode="singleTask"
,同样的多次点击:
我们再将SecondActivity
和ThirdActivity
都设成singleTask
并指定它们的taskAffinity
属性为com.cah.task
,注意这个taskAffinity
属性的值为字符串,且中间必须含有包名分隔符.
,然后做如下操作,在MainActivity
中单击按钮启动SecondActivity
,在SecondActivity
中单击按钮启动ThirdActivity
,在ThirdActivity
中单击又启动MainActivity
,最后在MainActivity
中单击按钮启动SecondActivity
,再按back
键,然后看到的是哪个Activity
?答案是回到桌面。
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:configChanges="screenLayout"
android:launchMode="singleTask"
android:taskAffinity="com.cah.task">
</activity>
<activity
android:name=".ThirdActivity"
android:configChanges="screenLayout"
android:launchMode="singleTask"
android:taskAffinity="com.cah.task">
</activity>
首先,从理论上分析这个问题,先假设MainActivity
为A
,SecondActivity
为B
,ThirdActivity
为C
。A
为standard
模式,按照规定,A
的taskAffinity
值继承自Application
的taskAffinity
,而Application
默认taskAffinity
为包名,所以A
的taskAffinity
为包名。由于在XML
中为B
和C
指定了taskAffinity
和启动模式,所以B
和C
是singleTask
模式且有相同的taskAffinity
值com.cah.task
。A
启动B
的时候,按照singleTask
的规则,这个时候需要为B
重新创建一个任务栈com.cah.task
。B
再启动C
,按照singleTask
的规则,由于C
所需要的任务栈(和B
为同一个任务栈)已经被B
创建,所以无需再创建新的任务栈,这个时候系统只是创建C
的实例后将C
入栈了。接着C
再启动A
,A
是standard
模式,所以系统只是创建C
的实例后将C
入栈了。接着C
再启动A
,A
是standard
模式,所以系统会为它创建一个新的实例并加到启动它的那个Activity
的任务栈中,由于C
启动了A
,所以A
会渐入C
的任务栈并未愈栈顶。这个时候已经有两个任务栈了,一个是名字为包名的任务栈,里面只有一个A
,另一个是名字为com.cah.task
的任务栈,里面的Activity
为BCA
。接下来,A
再启动B
,由于B
是singleTask
,B
需要回到任务栈的栈顶,由于栈的工作模式为“后进先出”,B
想要回到栈顶,只能是CA
出栈。所以,到这里就很好理解了,如果再按back
键,B
就出栈了,B
所在的任务栈已经不存在了,这个时候只能是回到后台任务栈并把A
显示出来。注意这个A
是后台任务栈的A
,不是com.cah.task
任务栈的A
,接着再继续back
,就回到桌面了。
因此,singleTask
模式的Activity
切换到栈顶会导致在它之上的栈内的Activity
出栈。
通过adb shell dumpsys activity activities
来验证,省略中间的过程,直接看C
启动A
的状态:
可以看到目前总共有2
个任务栈,com.cah.task
和com.cah.androidtest
,然后再从A
中启动B
:
在任务栈com.cah.task
中只剩下B
了,C
、A
都已经出栈了,这个时候再按back
就回到桌面了。
Activity.onNewIntent()
方法什么时候执行?
如果IntentActivity
处于任务栈的顶端,也就是说之前打开过的Activity
,现在处于onPause
、onStop
状态的话,其它应用再发送Intent
的话,执行顺序为:onNewIntent (-> onRestart) -> onStart -> onResume
。
Activity A
已经启动过,处于当前应用的Activity
堆栈中,当Activity A
的LaunchMode
为SingleTop
时,如果Activity A
在栈顶,且现在要再启动Activity A
,这时会调用onNewIntent()
方法;
当Activity A
的LaunchMode
为SingleInstance
,SingleTask
时,如果已经Activity A
已经在堆栈中,那么此时再次启动 会调用onNewIntent()
方法;
2 Activity
的Flags
Activity
的Flags
(标记位)可以设定Activity
的启动模式,比如FLAG_ACTIVITY_NEW_TASK
和FLAG_ACTIVITY_SINGLE_TOP
,还有的标记位可以影响Activity
的运行状态,比如FLAG_ACTIVITY_CLEAR_TOP
和FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
等。以下介绍介个常用的标记位。
FLAG_ACTIVITY_NEW_TASK
这个标记位的作用是为Activity
指定singleTask
启动模式,其效果和在xml
中指定该启动模式相同。
FLAG_ACTIVITY_SINGLE_TOP
这个标记位的作用是为Activity
指定singleTop
启动模式,其效果和在xml
中指定该启动模式相同。
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的Activity
,当它启动时,在同一个任务栈中所有位于它上面的Activity
都要出栈。这个模式一般需要和FLAG_ACTIVITY_NEW_TASK
配合使用,在这种情况下,被启动的ACTIVITY
的实例如果已经存在,那么系统就会调用它的onNewIntent
。如果被启动的Activity
采用standard
模式启动,那么它连同它之上的Activity
都要出栈,系统会创建新的Activity
实例并放入栈顶。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有这个标记位的Activity
不会出现在历史Activity
的列表中,当某些情况下我们不希望用户通过历史列表回到我们这个Activity
的时候这个标记比较游泳。它等同于在xml
中指定Activity
的属性android:excludeFromRecents = true
。
3 总结
3.1 Application 能不能启动 Activity?
能,但是只能以 FLAG_ACTIVITY_NEW_TASK 的模式启动,否则会报错。
以下是通过标准模式启动 Activity,程序崩溃:
这是因为启动 standard 模式的 Activity 默认会进入启动它的 Activity 所属的任务栈中,但是由于非 Activity 类型的 Context(如 ApplicationContext)并没有所谓的任务栈,这就出错了。
崩溃信息中提到了 FLAG_ACTIVITY_NEW_TASK 的 Flag。因此,解决这跟问题的方法以 FLAG_ACTIVITY_NEW_TASK 启动 Activity,这样在启动 Activity 的时候会给它创建一个新的任务栈。
不论是 application.startActivity(Intent(this, MainActivity::class.java)) 还是 applicationContext.startActivity(Intent(this, MainActivity::class.java)) 调用的都是 Context.startActivity(Intent):
public class Application extends ContextWrapper implements ComponentCallbacks2 {
}
public class ContextWrapper extends Context {
Context mBase;
@Override
public void startActivity(Intent intent) {
mBase.startActivity(intent);
}
}
public abstract class Context {
public abstract void startActivity(@RequiresPermission Intent intent);
}
class ContextImpl extends Context {
@Override
public void startActivity(Intent intent) {
warnIfCallingFromSystemProcess();
startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P) // 1
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}
}
从源码中可以看出,如果启动模式不是 FLAG_ACTIVITY_NEW_TASK 就会抛出异常。
以下是正确的启动方式:
val intent = Intent(this, MainActivity::class.java);
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
applicationContext.startActivity(intent)
// 或者
application.startActivity(intent)
参考
https://cloud.tencent.com/developer/article/1356506
https://zhuanlan.zhihu.com/p/67451239
https://zhuanlan.zhihu.com/p/157842441
https://www.pianshen.com/article/70371941935/
https://www.jianshu.com/p/bb0f3df62501
https://blog.csdn.net/wangjia55/article/details/38536171