盒子
盒子
文章目录
  1. 测试demo
  2. 查看任务栈
  3. standard
  4. singleTop
  5. singleTask
  6. singleInstance

Android温故而知新 - launchMode

Activity有四种launchMode,android官方文档的介绍如下:

启动模式 多个实例? 注释
“standard” 是 默认值。系统始终会在目标任务中创建新的 Activity 实例并向其传送 Intent。
“singleTop” 有条件 如果目标任务的顶部已存在一个 Activity 实例,则系统会通过调用该实例的 onNewIntent() 方法向其传送 Intent,而不是创建新的 Activity 实例。
“singleTask” 否 系统在新任务的根位置创建 Activity 并向其传送 Intent。 不过,如果已存在一个 Activity 实例,则系统会通过调用该实例的 onNewIntent() 方法向其传送 Intent,而不是创建新的 Activity 实例。
“singleInstance” 否 与“singleTask””相同,只是系统不会将任何其他 Activity 启动到包含实例的任务中。 该 Activity 始终是其任务唯一仅有的成员。

测试demo

我们通过两个Activity之间的跳转和任务栈打印来理解launchMode的作用。

这两个Activity使用同一个布局, TextView用来打印任务栈的id和activity实例,然后是两个按钮,分别用来启动两个activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<Button
android:id="@+id/gotoFirstActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="goto first activity" />

<Button
android:id="@+id/gotoSecondActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="goto second activity" />

</LinearLayout>

Activty代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FirstActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout);

Utils.initActivity(this);
}

public void onClick(View view) {
Utils.onClick(this, view.getId());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SecondActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_layout);

Utils.initActivity(this);
}

public void onClick(View view) {
Utils.onClick(this, view.getId());
}
}

因为两个activity的代码很相似,所以我把它们放到了静态类Utils中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Utils {
public static void initActivity(Activity activity) {
TextView label = (TextView) activity.findViewById(R.id.label);
label.setText("task : " + activity.getTaskId() + " activity : " + activity);
}


public static void onClick(Context context, int id) {
Intent intent;
if (id == R.id.gotoFirstActivity) {
intent = new Intent(context, FirstActivity.class);
} else {
intent = new Intent(context, SecondActivity.class);
}
context.startActivity(intent);
}
}

查看任务栈

我们可以在adb shell中使用dumpsys activity可以看到任务栈。这个命令的打印会比较多,但是有两个部分是比较重要的。

一个是Recent tasks,这里可以看到各个任务栈的总体信息,如我们的demo在第0个任务栈,它的Task id 是483,包名是linjw.demo.launchmodedemo,栈里面有6个Activity

1
2
3
4
5
6
7
8
9
10
11
ACTIVITY MANAGER RECENT TASKS (dumpsys activity recents)
Recent tasks:
* Recent #0: TaskRecord{f69123d #483 A=linjw.demo.launchmodedemo U=0 sz=6}
* Recent #1: TaskRecord{b798a32 #487 A=com.android.systemui U=0 sz=0}
* Recent #2: TaskRecord{f874383 #486 A=com.tencent.mm U=0 sz=1}
* Recent #3: TaskRecord{d7bd900 #479 A=com.meizu.flyme.launcher U=0 sz=1}
* Recent #4: TaskRecord{8f5d797 #482 A=com.tencent.mobileqq U=0 sz=0}
* Recent #5: TaskRecord{fd99539 #481 A=android.task.stk.task U=0 sz=0}
* Recent #6: TaskRecord{7c9951e #480 A=com.android.incallui U=0 sz=1}
* Recent #7: TaskRecord{e32177e #478 A=com.zhihu.android U=0 sz=0}
* Recent #8: TaskRecord{7f094df #472 A=com.meizu.media.reader U=0 sz=0}

在它的下面可以看到里面的具体的activity,并且可以看到叫起它的Intent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
Stack #1:
Task id #483
TaskRecord{f69123d #483 A=linjw.demo.launchmodedemo U=0 sz=6}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10100000 cmp=linjw.demo.launchmodedemo/.FirstActivity }
Hist #5: ActivityRecord{786c71e u0 linjw.demo.launchmodedemo/.FirstActivity t483}
Intent { cmp=linjw.demo.launchmodedemo/.FirstActivity }
ProcessRecord{245852c 7994:linjw.demo.launchmodedemo/u0a61}
Hist #4: ActivityRecord{6d5da5d u0 linjw.demo.launchmodedemo/.SecondActivity t483}
Intent { cmp=linjw.demo.launchmodedemo/.SecondActivity }
ProcessRecord{245852c 7994:linjw.demo.launchmodedemo/u0a61}
Hist #3: ActivityRecord{90b9f88 u0 linjw.demo.launchmodedemo/.FirstActivity t483}
Intent { cmp=linjw.demo.launchmodedemo/.FirstActivity }
ProcessRecord{245852c 7994:linjw.demo.launchmodedemo/u0a61}
Hist #2: ActivityRecord{1de66e u0 linjw.demo.launchmodedemo/.SecondActivity t483}
Intent { cmp=linjw.demo.launchmodedemo/.SecondActivity }
ProcessRecord{245852c 7994:linjw.demo.launchmodedemo/u0a61}
Hist #1: ActivityRecord{17dd5b1 u0 linjw.demo.launchmodedemo/.FirstActivity t483}
Intent { cmp=linjw.demo.launchmodedemo/.FirstActivity }
ProcessRecord{245852c 7994:linjw.demo.launchmodedemo/u0a61}
Hist #0: ActivityRecord{9f195f8 u0 linjw.demo.launchmodedemo/.FirstActivity t483}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10100000 cmp=linjw.demo.launchmodedemo/.FirstActivity }
ProcessRecord{245852c 7994:linjw.demo.launchmodedemo/u0a61}
...

例如Hist #0这个Activity是从桌面点击应用图标进入的,所以它的Intent带android.intent.category.LAUNCHER这个category和FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY(0x00100000)、 FLAG_RECEIVER_FOREGROUND(0x10000000)这两个Flag,而其他的Activity都是我们通过点击按钮叫起的,我们的Intent里面没有带任何的category和Flag,所以只有cmp标识是哪个Activity。

我们可以通过Intent#setFlags(int) 设置Flag、Intent#addCategory(String) 添加category

有时候可以通过这个命令查看Activity的启动模式结合各种launchMode的作用来定位一些bug

standard

standard是默认的启动模式,在没有配置android:launchMode的时候就会默认用这种启动模式,当然也可以显示指定为standard。

它的特点是每次都会启动一个新的Activity实例,我们连续点击第一个按钮两次然后再连续点击两次返回键,截图如下:

任务栈状态图如下:

singleTop

当launchMode是singleTop的时候,如果task栈的栈顶Activity和将要启动的Activity是同一个Activity的话,就不会再启动第二个Activity。我们将FirstActivity设为singleTop,在启动demo之后无论按第一个按钮多少次,任务栈里面都只会有一个FirstActivity。

任务栈状态图如下:

但是当任务栈的栈顶Activity和将要启动的Activity不是同一个Activity的时候,就会启动新的Activity,并将它压入栈顶而不管栈里面还有没有这个Activity:

任务栈状态图如下:

singleTask

我们将FirstActivity和SecondActivity的launchMode都设置为singleTask,启动demo之后先按“GOTO SECOND ACTIVITY”再按“GOTO FIRST ACTIVITY”,截图如下:

我们可以看到在SecondActivity中启动FirstActivity,结果就返回了第一个Activity。如果这个时候再按返回键就会推出应用。

singleTask的作用就是在任务栈中寻找将要启动的Activity,如果找到的话就将它上面的Activity都弹出栈,直到它成为栈顶。

任务栈状态图如下:

singleInstance

官方文档的介绍是:

与“singleTask””相同,只是系统不会将任何其他 Activity 启动到包含实例的任务中。 该 Activity 始终是其任务唯一仅有的成员。

就是说系统会为singleInstance Activity单独创建一个任务栈,这个任务栈里是这个Activity独占的,不会再压入其他的Activity。而且它是系统唯一的,当singleInstance Activity已经存在于系统的某一任务栈中,就会直接跳到那个任务栈的Activity中,而不会新启动一个Activity。

我们将FirstActivity设为standard, SecondActivity设为singleInstance。启动demo之后先按“GOTO SECOND ACTIVITY”再按“GOTO FIRST ACTIVITY”。然后再一直按返回键到退出应用。截图如下:

任务栈状态图如下:

启动应用之后先点击“GOTO SECOND ACTIVITY”,这个时候系统会新建一个任务栈(Task 20)来放SecondActivity

在SecondActivity中再启动FirstActivity,因为Task 20这个任务栈是SecondActivity独占的。所以不会在这个任务栈压入其他Activity,而会回到原来的任务栈上(Task 19)。又因为FirstActivity的launchMode是standard,所以不管原来的栈里面有没有FirstActivity,都会压入一个新的FirstActivity。

这个时候再按返回键就不是回到SecondActivity了,因为它在其他的任务栈里面,要先将当前任务栈清空。

这个时候按返回键会将当前的Activity弹出栈,于是就跳到了一开始的FirstActivity。之后再按返回键,因为Task 19这个任务栈空了,就会去到SecondActivity的栈,于是就去到了SecondActivity。最后再按返回键就会退出应用了。

要再次提醒需注意的是singleInstance的Activity是系统唯一的,也就是说你在demo这里启动了这个SecondActivity的SecondActivity,然后按home键回到桌面去启动其他应用,从其他应用再启动一个SecondActivity也是去到原来的那个SecondActivity