前言
虽然了解activity的四种启动模式,但是在一些复杂场景下,各种启动模式会出现的现象,以及现象的原因并不清楚,再加上个taskAffinity launchMode clearTaskOnLaunch 这些参数会使得更加懵逼。所以根据在实际应用中遇到的问题总结一下。
主要内容
要讲启动模式需要从Task ,taskAffinity 以及launchMode,还有标签四个方面入手,看这四个之前的关联以及影响。Task
task跟activity的启动息息相关,因为activity启动后都是放在task里面进行管理的,task的数据结构是stack的,先进后出,新创建的activity放在task的顶部,如下图打开ActivityA->activityB->activityC:task的特点:
- activity的集合
- 以栈的形式对activity进行管理(back stack)
- task里面至少包含一个activity
- 新创建的activity放在栈顶。
- 每一个task都有称为Affinity的name。
TaskAffinity
taskAffinity是activity可以在manifest文件里面设置的属性.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.coroutinescopedemo">
<application android:allowBackup="true">
<activity
android:name=".MainActivity"
android:taskAffinity="hanking.edu">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
复制代码
用来确定启动的activity属于哪个task,或者确定task的名称。具体的功能如下:
用来决定持有activity的task是哪个。
默认情况下一个app里面的activity都有相同的affinity值(package name)
task的affinity值由触发创建task的activity的affinity值决定。(也被称为root activity)
taskAffinity用来确定activity所在栈的名字,是不是任何时候都会生效?看下默认情况下的两个activity设置不同的affinity会发生什么情况。
1、给activity设置task affinity 如下创建了activityA和activityB,其中给activityB设置了task Affinity值为com.something.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myApp">
<application
android:allowBackup="true"
android:theme="@style/AppTheme">
<activity android:name=".ActivityA">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ActivityB"
android:taskAffinity="com.something" />
</application>
</manifest>
复制代码
流程:打开activityA 从activityA跳转到ActivityB,然后打印task的情况。 通过adb shell dumpsys activity activities | sed -En -e '/Running activities/,/Run #0/p'打印情况如下
adb shell dumpsys activity activities | sed -En -e '/Running activities/,/Run #0/p'
Running activities (most recent first):
TaskRecord{5938ae7 #1669 A=com.example.coroutinescopedemo U=0 StackId=287 sz=2}
Run #1: ActivityRecord{5d93c09 u0 com.myApp/.ActivityB t1669}
Run #0: ActivityRecord{5ce5f59 u0 com.myApp/.ActivityA t1669}
复制代码
有上面流程可以知道,activity默认启动情况下加task affinity属性并不会新建task,也不会改变task名称,task的名称和taskRoot的activity中设置的task affinity值一致,如果没设置默认就是包名,这里taskRoot Activity是activityA。
2、添加FLAG_ACTIVITY_NEW_TASK 由上面可见,默认模式下就算给activity添加了taskAffinity属性也不会多创建一个task,原因是这个taskAffinity应该和FLAG_ACTIVITY_NEW_TASK一起使用才会创建新task。
val i = Intent(this, ActivityB::class.java)
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(i)
复制代码
在ActivityA中启动ActivityB的时候加上Intent.FLAG_ACTIVITY_NEW_TASK的flag,再尝试从ActivityA打开ActivityB。
Running activities (most recent first):
TaskRecord{59b097b #1675 A=com.something U=0 StackId=293 sz=1}
Run #0: ActivityRecord{5d93041 u0 com.example.myApp/.ActivityB t1675}
Running activities (most recent first):
TaskRecord{59b09a0 #1674 A=com.example.myApp U=0 StackId=292 sz=1}
Run #0: ActivityRecord{5d2de41 u0 com.example.myApp/.ActivityA t1674}
复制代码
由上面的信息可以看到有两个task,ActivityB所在的task 名称是com.something, stackId=293, ActivityA所在的task名称是com.example.myApp stackId=292 思考:加上Intent.FLAG_ACTIVITY_NEW_TASK的tag后由于启动了一个新的task,这时候退到任务管理器可以看到activityA和activityB所在的task,如果这个时候点开activityB再点击返回还会返回到activityA吗? 答案是不会?因为从activityA打开activityB后再切到后台,这个时候这两个activity的task都属于background状态,再打开activityB的task,activityB的task属于foreground task,返回会直接返回到桌面。
activity启动模式
activity的启动模式一般分为以下四种,四种模式的特点如下:Standard:
activity默认的启动模式,在standard模式下每次打开一个activity的时候都会生成一个新的实例。 如下已经有了A,B,C,D在stack中,再启动B,B是standard模式。 A →B→ C→D 启动B, A → B → C→D→ B 可以看到会再次生成B的实例,并放到栈顶。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myApp">
<application
android:allowBackup="true"
android:theme="@style/AppTheme">
<activity android:name=".ActivityA">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ActivityB"
android:taskAffinity="com.something" />
</application>
</manifest>
复制代码
val i = Intent(this, ActivityB::class.java)
startActivity(i)
复制代码
在标准模式下启动ActivityA->ActivityB->activityB如下图: standard模式看起来非常简单,每次生成activity实例并放在栈顶,但是当standard模式和flag一起使用的时候又会产生很多不一样的效果。 1、standard+Intent.FLAG_ACTIVITY_NEW_TASK 按照下面的方式启动activity
启动 Activity A
ActivityA 启动 ActivityB (no flags)
ActivityB 启动 Activity A
ActivityA 启动 Activity B (with flag NEW_TASK)
ActivityB 启动 ActivityA
复制代码
由上图可知当activityA启动activityB,并且此时intent加上NEW_TASK标签时,会生成一个新的task com.something,并且activityB作为taskRootActivity,此时activityB再启动activityA,activityA也会在com.something的栈上生成实例。
SingleTop
如果需要打开的activity的实例已经处于当前栈顶,那么会复用当前栈顶的activity,不会重新创建activity,但是会通过调用onNewIntent().所以如果需要刷新页面数据,就要在onNewIntent().进行处理。如果栈顶的activity和需要打开的activity不相同,那么会重新创建一个activity,并进栈。 假设栈里面已经有A ,B,C几个activity, . A →B →C 如果需要再打开activity B那么如下: A →B →C →B 如果这个时候再调用打开activity B会直接复用栈顶的B,并且调用B的onNewIntent()方法,栈如下 A →B →C →B
Step 1: Launch A -> A
Step 2: Launch B -> A-B
Step 3: Launch C -> A-B-C
Step 4: Launch B -> A-B-C-B
Step 5: Launch B -> A-B-C-B
复制代码
singleTop总结一句:就是复用栈顶activity。
SingleTask
如果栈中存在这个Activity的实例就会复用这个Activity,不管它是否位于栈顶,复用时,会将它上面的Activity全部出栈,并且会回调该实例的onNewIntent方法。其实这个过程还存在一个任务栈的匹配,因为这个模式启动时,会在自己需要的任务栈中寻找实例,这个任务栈就是通过taskAffinity属性指定。如果这个任务栈不存在,则会创建这个任务栈。
Step 1: Launch A -> A
Step 2: Launch B -> A-B
Step 3: Launch C -> A-B-C
Step 4: Launch B -> A-B
Step 5: Launch B -> A-B*
复制代码
如上当栈中有A-B-C此时再启动B,会遍历栈,然后找到B,将B上面的C出栈。
singleInstance
singleInstance模式下的Activity会单独占用一个Task栈,具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的Activity实例,除非这个特殊的任务栈被销毁了。以singleInstance模式启动的Activity在整个系统中是单例的,如果在启动这样的Activiyt时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。 假设有三个activity:A,B,C,B是singleInstance的,
Step 1: Launch A -> Task 1: A
Step 2: Launch B -> Task 1: A
Task 2: B // Visible to the user
Step 3: Launch C -> Task 1: A-C // Visible to the user
Task 2: B
Step 4: Launch B -> Task 1: A-C
Task 2: B* // Visible to the user
复制代码
FLAG_ACTIVITY_NEW_TASK
Start the activity in a new task. If a task is already running for the activity you are now starting, that task is brought to the foreground with its last state restored and the activity receives the new intent in onNewIntent().
复杂场景分析
上面都是简单的场景,如果选择更加复杂的场景,又会出现意想不到的现象。1、task切换后打不开activity
- 启动 ActivityA
- Activity A 启动 Activity B
- Activity B 启动 Activity C with FLAG_NEW_TASK
- Activity C 启动 Activity D
- 用户切换到 com.myApp
- Activity B 启动 Activity C with FLAG_NEW_TASK
上面有个比较奇怪的现象activityB启动activityC,activityC 启动activityD,都是new_task方式,C,D,都是在com.something的task里面,这里正常,但是activityB,再次调用activityC的时候却没有启动activityC。 2、task切换后打开多个activity
1\. 启动 Activity A
2\. Activity A 启动 Activity B
3\. Activity B 启动 Activity C with FLAG_NEW_TASK
4\. Activity C 启动 Activity D
5\. User switches to com.myApp
6\. Activity B 启动 Activity D with FLAG_NEW_TASK
复制代码
上面流程启动activity的,task是什么情况? 看上图,最后当activityB其次启动activityD的时候又创建了一个activityD,
当activityB启动activityD的时候为什么会新创建一个activityD的实例? 这里重新创建activityD的原因是:通过activityC创建activityD的intent和通过activityB创建activityD的intent的不一样导致的。也就是说只有当intent的一样时才不会创建多个实例。
startActivityForResult 异常
activityA是singleInstance,activityB是standard模式,当activityA调用startActivityForResult打开activityB时,activityA收到的回调函数onActivityResult能接受activityB的返回结果吗? 正常情况下activityA通过startActivityForResult打开activityB时的流程如下:
- activityA 重写onActivityResult接受activityB中的返回值。
- activityB通过setResult确定返回值。
3.当activityB返回时activityA的onActivityResult才会被回调。
但是当activityA定义为singleInstance时通过startActivityForResult打开activityB时如下:因为activityA是singleInstance的所以独享一个task,当activityA打开activityB时,也创建一个新的task,但是onActivityResult是立马就回调,不是activityB finish的时候回调的。这是什么原因? 注意:当一个activity 调用startActivityForResult打开另一个activity当时另一个activity在另一个创建的task里面的时候,onActivityResult就会立刻回调。