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

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/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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值