Activity(三)—— Activity的启动模式

Activity(三)—— Activity的启动模式Activity 的启动模式 1Activity 的 LaunchModeAc 为什么需要启动模式 在默认情况下 当我们多次启动同一个 Activity 的时候 系统会创建多个实例并把它们一一放入任务栈中 当点击 bac

大家好,欢迎来到IT知识分享网。

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 键才能退出程序。

standard

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。
    singleTask

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 键,这时所有返回栈都已经空了,也就自然退出了程序。

singleInstance

上面介绍了 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个应用ABA启动了B的一个Activity C,然后按Home键回到桌面上,然后再单击B的桌面图标,这个时候斌故事启动了B的主Activity,而是重新显示了已经被应用A启动的Activity C,或者说,CA的任务栈转移到了B的任务栈中。可以这么理解,由于A启动了C,这个时候C只能运行在A的任务栈中,但是C属于B应用,正常情况下,它的TaskAffinity值肯定不可能和A的任务栈相同(因为包名不同)。所以,当B被启动后,B会创建自己的任务栈,这个时候系统发现C原本想要的任务栈已经被创建了,所以就把CA的任务栈中转移过来了。

如何给Activity指定启动模式呢?有两种方法,第一种是通过AndroidManifestActivity指定启动模式,如下所示:

<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); 

启动标准模式的Activity

启动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); }); 

多次启动singleTask的Activity

虽然多次启动MainActivity,但是它只有一个实例在任务栈中。

adb shell dumpsys检测AndroidActivity任务栈

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)); } 

从下面的日志中可以看出,尽管启动了2MainActivity,但MainActivity并没有重建,只是暂停了一下,然后调用了onNewIntent,接着调用onResume就又继续了:

onNewIntent方法

去掉android:launchMode="singleTask",同样的多次点击:

标准模式的Activity

我们再将SecondActivityThirdActivity都设成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> 

首先,从理论上分析这个问题,先假设MainActivityASecondActivityBThirdActivityCAstandard模式,按照规定,AtaskAffinity值继承自ApplicationtaskAffinity,而Application默认taskAffinity为包名,所以AtaskAffinity为包名。由于在XML中为BC指定了taskAffinity和启动模式,所以BCsingleTask模式且有相同的taskAffinitycom.cah.taskA启动B的时候,按照singleTask的规则,这个时候需要为B重新创建一个任务栈com.cah.taskB再启动C,按照singleTask的规则,由于C所需要的任务栈(和B为同一个任务栈)已经被B创建,所以无需再创建新的任务栈,这个时候系统只是创建C的实例后将C入栈了。接着C再启动AAstandard模式,所以系统只是创建C的实例后将C入栈了。接着C再启动AAstandard模式,所以系统会为它创建一个新的实例并加到启动它的那个Activity的任务栈中,由于C启动了A,所以A会渐入C的任务栈并未愈栈顶。这个时候已经有两个任务栈了,一个是名字为包名的任务栈,里面只有一个A,另一个是名字为com.cah.task的任务栈,里面的ActivityBCA。接下来,A再启动B,由于BsingleTaskB需要回到任务栈的栈顶,由于栈的工作模式为“后进先出”,B想要回到栈顶,只能是CA出栈。所以,到这里就很好理解了,如果再按back键,B就出栈了,B所在的任务栈已经不存在了,这个时候只能是回到后台任务栈并把A显示出来。注意这个A是后台任务栈的A,不是com.cah.task任务栈的A,接着再继续back,就回到桌面了。

因此,singleTask模式的Activity切换到栈顶会导致在它之上的栈内的Activity出栈。

通过adb shell dumpsys activity activities来验证,省略中间的过程,直接看C启动A的状态:

标准模式下的Activity跳转

标准模式下的Activity跳转

可以看到目前总共有2个任务栈,com.cah.taskcom.cah.androidtest,然后再从A中启动B

在这里插入图片描述

在任务栈com.cah.task中只剩下B了,CA都已经出栈了,这个时候再按back就回到桌面了。

Activity.onNewIntent()方法什么时候执行?

如果IntentActivity处于任务栈的顶端,也就是说之前打开过的Activity,现在处于onPauseonStop状态的话,其它应用再发送Intent的话,执行顺序为:onNewIntent (-> onRestart) -> onStart -> onResume

Activity A已经启动过,处于当前应用的Activity堆栈中,当Activity ALaunchModeSingleTop时,如果Activity A在栈顶,且现在要再启动Activity A,这时会调用onNewIntent()方法;

Activity ALaunchModeSingleInstanceSingleTask时,如果已经Activity A已经在堆栈中,那么此时再次启动 会调用onNewIntent()方法;

2 ActivityFlags

ActivityFlags(标记位)可以设定Activity的启动模式,比如FLAG_ACTIVITY_NEW_TASKFLAG_ACTIVITY_SINGLE_TOP,还有的标记位可以影响Activity的运行状态,比如FLAG_ACTIVITY_CLEAR_TOPFLAG_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/
https://zhuanlan.zhihu.com/p/
https://zhuanlan.zhihu.com/p/
https://www.pianshen.com/article//
https://www.jianshu.com/p/bb0f3df62501
https://blog.csdn.net/wangjia55/article/details/

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/143416.html

(0)
上一篇 2025-05-03 19:00
下一篇 2025-05-03 19:15

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信